name: Publish customer scripts # Keeps the scripts behind download.autopilotmonitor.com/agent/ current WITHOUT waiting # for an agent release. Before this workflow existed, the script blobs rotated only on a # publish_as_stable run of build-agent.yml, so a bootstrap fix sat unpublished until the # next agent cutover -- while the WDP bootstrap MSI downloads the published copy on every # enrolling device. # # Same publish code as the release path (.github/scripts/Publish-BootstrapScripts.ps1); # this workflow only supplies a second trigger for it. on: push: branches: [main] paths: - 'scripts/Bootstrap/**' - 'scripts/CustomerSetup/**' - '.github/scripts/Publish-BootstrapScripts.ps1' workflow_dispatch: inputs: dry_run: description: 'Run every check and report drift, upload nothing' required: true type: boolean default: false channel: description: 'release: every script. dev: only the two -Dev scripts, to try a change on dev devices first' required: true type: choice options: - release - dev default: release permissions: contents: read # Shared with build-agent.yml: a script publish and an agent release both touch # version.json, and the bootstrapVersion read-modify-write must not interleave with the # release writing its agent fields. Never cancel -- a half-published set is worse than a queue. concurrency: group: agent-blob-publish cancel-in-progress: false # Authenticode for the customer scripts. Same identity and same repository variables as # build-agent.yml -- the loader on every device pins this publisher, so the two must not # drift apart. The publisher takes every host, the timestamp server included, from # .github/scripts/Get-Endpoints.ps1. env: CODE_SIGN_CLIENT_ID: ${{ vars.CODE_SIGN_CLIENT_ID }} CODE_SIGN_CERTIFICATE_NAME: ${{ vars.CODE_SIGN_CERTIFICATE_NAME }} CODE_SIGN_KEY_VAULT_URL: ${{ vars.CODE_SIGN_KEY_VAULT_URL }} CODE_SIGN_EXPECTED_SUBJECT: ${{ vars.CODE_SIGN_EXPECTED_SUBJECT }} jobs: # $/ is the self-repository form: this repository at the running commit, never a # path some earlier step could have written to the workspace. # # A push with [skip publish] in one of its commit messages runs neither job and uploads # nothing; the gates' own push trigger, whose paths cover these, still checks it. Never # [skip actions] for that: it drops every check of the push, CI and these gates included. gates: if: github.event_name != 'push' || !contains(join(github.event.commits.*.message, ' '), '[skip publish]') uses: $/.github/workflows/bootstrap-script-gates.yml publish: name: Publish to the download alias needs: gates runs-on: windows-latest # Both identities trust only the `release` environment, which admits main only. environment: release # Job-scoped: only the publish job federates to Azure, the gates job never holds an # OIDC token it does not need. permissions: contents: read id-token: write # OIDC federated login for the storage writes (secret-less) steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Check code-signing configuration shell: pwsh run: | foreach ($name in 'CODE_SIGN_CLIENT_ID', 'CODE_SIGN_CERTIFICATE_NAME', 'CODE_SIGN_KEY_VAULT_URL') { if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { throw "Repository variable $name is not set. Published customer scripts must be signed." } } # First, and only to fetch the Key Vault token: azure/login sets the default az # account and the storage login below has to be the one that stays. - name: Login to Azure (code-signing identity) uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 with: client-id: ${{ env.CODE_SIGN_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} allow-no-subscriptions: true - name: Acquire Key Vault token for signing shell: pwsh run: | $ErrorActionPreference = 'Stop' $token = az account get-access-token --scope https://vault.azure.net/.default --query accessToken --output tsv if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($token)) { throw "Could not acquire a Key Vault token for the code-signing identity." } Write-Host "::add-mask::$token" "CODE_SIGN_AKV_ACCESS_TOKEN=$token" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # Authorizes the eu blob writes and the AdminConfiguration merge. The gates job above # runs the same publisher with -DryRun and deliberately without a login -- a dry run # must never need Azure credentials. - name: Login to Azure (OIDC) uses: azure/login@a641126d1b8aa4d1fa005f4f92df94a3a4c4c906 # v3.1.0 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} # Version pinned so a new or compromised NuGet release cannot flow into signing # unreviewed; roll-forward because the tool targets an older runtime than the SDK # preinstalled on the runner. - name: Install AzureSignTool env: DOTNET_ROLL_FORWARD: LatestMajor run: dotnet tool install --no-cache --global AzureSignTool --version 7.0.1 # A push publishes the release channel; only a dispatch can choose dev. - name: Publish bootstrap + customer scripts shell: pwsh env: DOTNET_ROLL_FORWARD: LatestMajor PUBLISH_CHANNEL: ${{ inputs.channel || 'release' }} run: | $dryRun = '${{ inputs.dry_run }}' -eq 'true' ./.github/scripts/Publish-BootstrapScripts.ps1 ` -Channel $env:PUBLISH_CHANNEL ` -LegacySasToken '${{ secrets.AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN }}' ` -SigningToken $env:CODE_SIGN_AKV_ACCESS_TOKEN ` -KeyVaultUrl $env:CODE_SIGN_KEY_VAULT_URL ` -CertificateName $env:CODE_SIGN_CERTIFICATE_NAME ` -ExpectedPublisher $env:CODE_SIGN_EXPECTED_SUBJECT ` -SignedOutputDir "$env:RUNNER_TEMP/signed-scripts" ` -DryRun:$dryRun # The exact bytes that were (or, on a dry run, would have been) uploaded. A dry-run # dispatch is therefore the way to inspect a signed script locally before it reaches # a single customer. Kept on failure too: a failed alias verification is precisely # when someone wants to look at the file. - name: Upload signed scripts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: signed-scripts path: ${{ runner.temp }}/signed-scripts if-no-files-found: ignore