name: Bootstrap script gates # Quality gate for the customer-facing PowerShell scripts. Devices execute them as SYSTEM # during enrollment, so a red suite must never ship. It runs in two roles: # * on its own, on every pull request and push to main that changes an input of these # checks (reports, does not block the merge button); # * called as the gates job that BLOCKS every signing or upload of bootstrap code: # publish-scripts.yml, build-agent.yml (stable releases), build-msi.yml. Those runs # show under the caller. # # Both path lists are the checks' complete inputs and stay identical. They cover every # push path of publish-scripts.yml, which leaves the check of a [skip publish] push to # this workflow. # # No concurrency group: it would also bind the runs called by the publishers, and a # cancel there would block a publish. on: push: branches: [main] paths: - 'scripts/Bootstrap/**' - 'scripts/CustomerSetup/**' - '.github/scripts/Publish-BootstrapScripts.ps1' - '.github/scripts/Get-Endpoints.ps1' - '.github/scripts/Invoke-BootstrapSystemPass.ps1' - '.github/scripts/Test-BootstrapAttacker.ps1' - '.github/workflows/bootstrap-script-gates.yml' # The integration tests run the generated OOBE script and the MSI's custom action; the # contract tests also read the MSI build. - 'src/Backend/AutopilotMonitor.Functions.Tests/TestData/oobe-bootstrap-golden.ps1' - 'src/Agent/AutopilotMonitor.BootstrapMsi/Package.wxs' - '.github/workflows/build-msi.yml' pull_request: branches: [main] paths: - 'scripts/Bootstrap/**' - 'scripts/CustomerSetup/**' - '.github/scripts/Publish-BootstrapScripts.ps1' - '.github/scripts/Get-Endpoints.ps1' - '.github/scripts/Invoke-BootstrapSystemPass.ps1' - '.github/scripts/Test-BootstrapAttacker.ps1' - '.github/workflows/bootstrap-script-gates.yml' - 'src/Backend/AutopilotMonitor.Functions.Tests/TestData/oobe-bootstrap-golden.ps1' - 'src/Agent/AutopilotMonitor.BootstrapMsi/Package.wxs' - '.github/workflows/build-msi.yml' workflow_dispatch: workflow_call: permissions: contents: read jobs: gates: name: Bootstrap guard tests (PS 5.1, ${{ matrix.os }}) strategy: fail-fast: false matrix: # Folder permissions and Windows PowerShell 5.1 come with the OS, so both server images. os: [windows-2022, windows-2025] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # For all users: the SYSTEM pass below runs Pester as SYSTEM. - name: Install Pester 5+ shell: powershell run: | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null Install-Module Pester -Scope AllUsers -Force -SkipPublisherCheck -MinimumVersion 5.5.0 # Windows PowerShell 5.1 on purpose: that is the runtime IME uses to execute # platform scripts, so guard/relax logic is verified without resetting a device. # The runner is elevated, so the integration tests use the real folder permissions. - name: Run Pester shell: powershell run: | Import-Module Pester -MinimumVersion 5.5.0 $c = New-PesterConfiguration $c.Run.Path = 'scripts/Bootstrap' $c.Run.Exit = $true $c.Output.Verbosity = 'Detailed' Invoke-Pester -Configuration $c # On a device the bootstrap runs as SYSTEM: the integration tests once more as SYSTEM. - name: Integration tests as SYSTEM shell: powershell run: ./.github/scripts/Invoke-BootstrapSystemPass.ps1 - name: Folder permissions against a real standard user shell: powershell run: ./.github/scripts/Test-BootstrapAttacker.ps1 - name: Guard scripts stay pure ASCII shell: powershell run: | $failed = $false Get-ChildItem 'scripts/Bootstrap' -Filter '*.ps1' -Recurse | ForEach-Object { $bytes = [System.IO.File]::ReadAllBytes($_.FullName) for ($i = 0; $i -lt $bytes.Length; $i++) { if ($bytes[$i] -gt 127) { Write-Host "::error file=scripts/Bootstrap/$($_.Name)::Non-ASCII byte 0x$('{0:X2}' -f $bytes[$i]) at offset $i. PS 5.1 (IME) reads BOM-less files as ANSI and corrupts multi-byte chars." $failed = $true break } } } if ($failed) { exit 1 } Write-Host 'All bootstrap scripts are pure ASCII.' - uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: '10.0.x' # The names and log lines the scripts share with the agent, checked against the agent's # own code. - name: Script contracts with the agent run: dotnet test src/Agent/AutopilotMonitor.Agent.V2.Core.Tests --configuration Release --filter "Category=BootstrapContract" --logger "console;verbosity=normal" # The publish chain renders the -Dev variant by literal substitution and refuses to # ship when an anchor moved. Running it dry here surfaces that (and any drift against # the alias) on the PR instead of on the push that would have published. - name: Publish dry run (dev-render anchors + version-bump guard + drift) shell: pwsh run: ./.github/scripts/Publish-BootstrapScripts.ps1 -DryRun