Files
SilverMetal/.gitea/workflows/build-iso-windows.yaml
sysadmin 6aa963f024 docs(tests): document branding test suite + elevation requirement
ci(branding): run branding Pester suite before Build packed ISO step
2026-06-09 14:14:13 +01:00

181 lines
7.9 KiB
YAML

name: Build SilverMetal Enhanced - Windows ISO
# M2/M3. Builds the custom packed ISO on the self-hosted Windows runner
# (silverlabs-runner-win, labels windows-latest / windows-2025), validates the
# baked payload offline (no nested-virt needed), and on a tag attaches the ISO
# + SBOM + SHA256 to a Gitea release. Mirrors build-iso-linux.yaml's structure.
#
# Base ISO: the licensed/eval Windows 11 IoT Enterprise LTSC media is an INPUT,
# never committed. Provide it via the repo variable SILVERMETAL_BASE_ISO_URL
# (downloaded at build time) OR pre-stage it on the runner at C:\silvermetal\base.iso.
#
# Reproducibility scope (iso-builder.md §5): pinned inputs + SBOM + SHA, NOT
# bit-identical on Windows -- so this is single-build, not the Linux double-build gate.
on:
push:
branches: [main]
paths:
- 'windows/**'
- '.gitea/workflows/build-iso-windows.yaml'
tags:
- 'win-v*'
pull_request:
branches: [main]
paths:
- 'windows/**'
- '.gitea/workflows/build-iso-windows.yaml'
workflow_dispatch:
concurrency:
group: build-iso-windows-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: windows-latest
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Ensure Windows ADK (oscdimg)
shell: pwsh
run: |
$deploy = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools'
if ((Get-Command oscdimg.exe -EA SilentlyContinue) -or (Test-Path $deploy)) {
Write-Host 'ADK Deployment Tools already present.'; exit 0
}
Write-Host 'Installing ADK Deployment Tools...'
# Prefer winget; fall back to the ADK web installer.
$ok = $false
try { winget install --id Microsoft.WindowsADK -e --accept-source-agreements --accept-package-agreements --silent; $ok = $true } catch {}
if (-not $ok) {
# NOTE: fwlink id is ADK-version-specific; update if the channel rolls.
Invoke-WebRequest 'https://go.microsoft.com/fwlink/?linkid=2289980' -OutFile "$env:TEMP\adksetup.exe"
Start-Process "$env:TEMP\adksetup.exe" -ArgumentList '/quiet','/norestart','/features','OptionId.DeploymentTools' -Wait
}
if (-not (Test-Path $deploy)) { throw 'ADK Deployment Tools install failed.' }
- name: Setup .NET 9 SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Install MAUI workload
shell: pwsh
run: |
Write-Host "dotnet $(dotnet --version)"
Write-Host 'Installing/repairing MAUI workload (idempotent)...'
dotnet workload install maui
Write-Host 'MAUI workload ready.'
- name: Build + test SilverOS Welcome
shell: pwsh
run: |
dotnet test windows/welcome/SilverOS.Welcome.sln -c Release
- name: Acquire base ISO
id: iso
shell: pwsh
env:
ISO_SRC: ${{ vars.SILVERMETAL_BASE_ISO_URL }} # HTTP(S) URL or UNC/local path
SMB_USER: ${{ secrets.SILVERMETAL_ISO_SHARE_USER }}
SMB_PASS: ${{ secrets.SILVERMETAL_ISO_SHARE_PASS }}
run: |
$dst = "$env:RUNNER_TEMP\base.iso"
$staged = 'C:\silvermetal\base.iso'
if ($env:ISO_SRC -match '^(?i)https?://') {
Write-Host 'Downloading base ISO (HTTP)...'
Invoke-WebRequest $env:ISO_SRC -OutFile $dst
} elseif ($env:ISO_SRC) {
# UNC or local path. The runner is SYSTEM; if the share needs auth,
# provide repo secrets SILVERMETAL_ISO_SHARE_USER/_PASS and we map it.
if ($env:ISO_SRC -like '\\*' -and $env:SMB_USER) {
$root = [regex]::Match($env:ISO_SRC, '^\\\\[^\\]+\\[^\\]+').Value
Write-Host "Authenticating to $root"
cmd /c "net use `"$root`" /user:$env:SMB_USER $env:SMB_PASS" | Out-Null
}
if (-not (Test-Path -LiteralPath $env:ISO_SRC)) { throw "ISO path not reachable by the runner (SYSTEM): $env:ISO_SRC" }
Write-Host 'Copying base ISO from path...'
Copy-Item -LiteralPath $env:ISO_SRC -Destination $dst -Force
} elseif (Test-Path $staged) {
Write-Host "Using pre-staged ISO: $staged"
Copy-Item -LiteralPath $staged -Destination $dst -Force
} else {
throw "No base ISO. Set repo variable SILVERMETAL_BASE_ISO_URL (URL or UNC/local path) or stage it at $staged."
}
"path=$dst" >> $env:GITHUB_OUTPUT
- name: Test branding module (Pester)
shell: pwsh
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
if (-not (Get-Module -ListAvailable Pester | Where-Object Version -ge '5.0.0')) {
Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser -Force -SkipPublisherCheck
}
$r = Invoke-Pester windows/tests/Branding.Tests.ps1 -PassThru -Output Detailed
if ($r.FailedCount -gt 0) { throw "$($r.FailedCount) branding test(s) failed" }
- name: Build packed ISO
shell: pwsh
run: |
.\windows\installer\build.ps1 `
-SourceIso '${{ steps.iso.outputs.path }}' `
-OutputIso "$env:RUNNER_TEMP\out\SilverMetal-Enhanced-Windows.iso"
- name: Validate baked payload (offline assertions)
shell: pwsh
run: |
.\windows\tests\Assert-IsoStructure.ps1 -IsoPath "$env:RUNNER_TEMP\out\SilverMetal-Enhanced-Windows.iso"
- name: Persist build output to stable path
shell: pwsh
run: |
# RUNNER_TEMP is per-job/ephemeral. Keep the latest validated build at a
# stable path so it can be retrieved (e.g. for VM boot-testing) out of band.
New-Item -ItemType Directory -Force 'C:\silvermetal\out' | Out-Null
Copy-Item "$env:RUNNER_TEMP\out\*" 'C:\silvermetal\out\' -Force
Get-ChildItem 'C:\silvermetal\out' | ForEach-Object { Write-Host $_.Name }
- name: Upload SBOM + SHA (always)
uses: actions/upload-artifact@v3
with:
name: silvermetal-windows-attestation-${{ github.run_id }}
path: |
${{ runner.temp }}/out/*.sbom.json
${{ runner.temp }}/out/*.sha256
if-no-files-found: warn
retention-days: 30
- name: Upload ISO (dispatch / tag only)
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v3
with:
name: silvermetal-windows-iso-${{ github.run_id }}
path: ${{ runner.temp }}/out/*.iso
if-no-files-found: error
retention-days: 14
- name: Attach to Gitea release (tag only)
if: startsWith(github.ref, 'refs/tags/')
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$api = "${{ github.server_url }}/api/v1"
$repo = "${{ github.repository }}"
$tag = "${{ github.ref_name }}"
$h = @{ Authorization = "token $env:GITHUB_TOKEN" }
$rel = try { Invoke-RestMethod -Headers $h "$api/repos/$repo/releases/tags/$tag" } catch { $null }
if (-not $rel) {
$body = @{ tag_name=$tag; name="SilverMetal Enhanced - Windows $tag"; body="Packed ISO + SBOM + SHA256. Reproducibility: pinned inputs + SBOM (not bit-identical)."; draft=$false; prerelease=$true } | ConvertTo-Json
$rel = Invoke-RestMethod -Method Post -Headers $h -ContentType 'application/json' -Body $body "$api/repos/$repo/releases"
}
Get-ChildItem "$env:RUNNER_TEMP\out\*" -Include *.iso,*.sbom.json,*.sha256 | ForEach-Object {
Write-Host "Attaching $($_.Name)"
Invoke-RestMethod -Method Post -Headers $h -ContentType 'application/octet-stream' `
-InFile $_.FullName "$api/repos/$repo/releases/$($rel.id)/assets?name=$($_.Name)"
}