Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 21s
The boot.wim now carries WinPE-NetFx/PowerShell (collector), growing the image ~0.4GB,
and each build persists a ~5GB ISO to C:\silvermetal\out. On the single-volume runner
that accumulation starved oscdimg ('Insufficient disk space'). Clear prior output +
stale smbuild work dirs at job start so free space self-heals each run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
229 lines
11 KiB
YAML
229 lines
11 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: Free disk space
|
|
shell: pwsh
|
|
run: |
|
|
# Each successful build persists a ~5 GB ISO to C:\silvermetal\out, and the
|
|
# boot.wim now carries WinPE-NetFx/PowerShell (bigger image). On the single-volume
|
|
# runner that accumulation starves oscdimg ("Insufficient disk space"). Clear the
|
|
# prior build output + any stale work dir before building so space self-heals.
|
|
$before = [math]::Round((Get-PSDrive C).Free/1GB,1)
|
|
Remove-Item 'C:\silvermetal\out\*' -Recurse -Force -ErrorAction SilentlyContinue
|
|
Get-ChildItem 'C:\gitea-runner\workspace' -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
$t = Join-Path $_.FullName 'tmp\smbuild'
|
|
if (Test-Path $t) { Remove-Item $t -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
$after = [math]::Round((Get-PSDrive C).Free/1GB,1)
|
|
Write-Host " C: free ${before}GB -> ${after}GB (cleared prior ISO output + stale smbuild)"
|
|
|
|
- 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: Ensure Windows ADK WinPE add-on
|
|
shell: pwsh
|
|
run: |
|
|
# build.ps1 (Invoke-ForceLegacySetup) calls Add-WindowsPackage with the
|
|
# WinPE_OCs cabs, which only exist if the ADK WinPE add-on is installed.
|
|
$ocs = 'C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\WinPE_OCs'
|
|
if (Test-Path $ocs) {
|
|
Write-Host 'ADK WinPE add-on (WinPE_OCs) already present.'; exit 0
|
|
}
|
|
Write-Host 'Installing ADK WinPE add-on...'
|
|
# Prefer winget; fall back to the WinPE add-on web installer.
|
|
$ok = $false
|
|
try { winget install --id Microsoft.ADKPEAddon -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=2289981' -OutFile "$env:TEMP\adkwinpesetup.exe"
|
|
Start-Process "$env:TEMP\adkwinpesetup.exe" -ArgumentList '/quiet','/norestart','/features','OptionId.WindowsPreinstallationEnvironment' -Wait
|
|
}
|
|
# The WinPE collector is a required, core feature of this image, so a missing
|
|
# WinPE_OCs dir is a hard build gate (fail fast with a clear message).
|
|
if (-not (Test-Path $ocs)) { throw 'ADK WinPE add-on install failed (WinPE_OCs missing)' }
|
|
|
|
- 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: |
|
|
# Windows ships Pester 3.x; force Pester 5 (the tests use v5 syntax).
|
|
Set-PSRepository PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
|
|
if (-not (Get-Module -ListAvailable Pester | Where-Object { $_.Version -ge [version]'5.0.0' })) {
|
|
Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser -Force -SkipPublisherCheck
|
|
}
|
|
Get-Module Pester | Remove-Module -Force -ErrorAction SilentlyContinue
|
|
Import-Module Pester -MinimumVersion 5.0 -Force
|
|
Write-Host "Using Pester $((Get-Module Pester).Version)"
|
|
# v5 configuration object — avoids the v3/-Output param ambiguity.
|
|
$cfg = New-PesterConfiguration
|
|
$cfg.Run.Path = 'windows/tests/Branding.Tests.ps1'
|
|
$cfg.Run.PassThru = $true
|
|
$cfg.Output.Verbosity = 'Detailed'
|
|
$r = Invoke-Pester -Configuration $cfg
|
|
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 }}' `
|
|
-WorkDir "$env:RUNNER_TEMP\smbuild" `
|
|
-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)"
|
|
}
|