ci(windows): implement M2 ISO build + Gitea Windows-runner workflow
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 34s

Implement build.ps1 (M2): mount/extract the base ISO, offline-service
install.wim (inject GPD drivers if staged, debloat appx, bake SetupComplete.cmd
+ hardening modules into \Windows\Setup\Scripts), inject autounattend.xml,
oscdimg UEFI repack, emit SHA-256 + SBOM. Elevation + oscdimg guarded.

Add .gitea/workflows/build-iso-windows.yaml: runs on the self-hosted
silverlabs-runner-win (windows-latest), ensures ADK Deployment Tools, acquires
the base ISO from repo var SILVERMETAL_BASE_ISO_URL or a pre-staged path, builds,
validates the baked payload offline, uploads SBOM/SHA (+ISO on dispatch/tag),
attaches to a Gitea release on win-v* tags. Mirrors build-iso-linux.yaml.

Add tests/Assert-IsoStructure.ps1: the no-nested-virt CI gate - mounts the built
ISO + install.wim read-only and asserts autounattend.xml, SetupComplete.cmd, and
the hardening modules are correctly baked. Full QEMU boot+Verify is a follow-on.

Switch autounattend to Windows' native SetupComplete.cmd auto-run (SYSTEM, end
of setup) instead of a duplicate FirstLogonCommands call.

Untested until first runner execution (dev box is ARM64). All PS parse-clean;
autounattend XML + workflow YAML valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-08 18:11:05 +01:00
parent d58aa3ec17
commit 1c886deca3
5 changed files with 297 additions and 39 deletions

View File

@@ -0,0 +1,131 @@
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: Acquire base ISO
id: iso
shell: pwsh
env:
ISO_URL: ${{ vars.SILVERMETAL_BASE_ISO_URL }}
run: |
$staged = 'C:\silvermetal\base.iso'
if ($env:ISO_URL) {
$dst = "$env:RUNNER_TEMP\base.iso"
Write-Host "Downloading base ISO from repo variable URL..."
Invoke-WebRequest $env:ISO_URL -OutFile $dst
"path=$dst" >> $env:GITHUB_OUTPUT
} elseif (Test-Path $staged) {
Write-Host "Using pre-staged ISO: $staged"
"path=$staged" >> $env:GITHUB_OUTPUT
} else {
throw "No base ISO. Set repo variable SILVERMETAL_BASE_ISO_URL or stage it at $staged on the runner."
}
- 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" `
-SkipInputVerify
- name: Validate baked payload (offline assertions)
shell: pwsh
run: |
.\windows\tests\Assert-IsoStructure.ps1 -IsoPath "$env:RUNNER_TEMP\out\SilverMetal-Enhanced-Windows.iso"
- 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)"
}