From 01311898a8c4bb3c194d744d814ec9d754aefa25 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Sun, 30 Aug 2026 18:21:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(ci):=20no=20third-party=20action=20holds=20?= =?UTF-8?q?release/deploy=20credentials=20by=20mutable=20tag=20=E2=80=94?= =?UTF-8?q?=20build-agent=20creates=20releases=20with=20first-party=20`gh?= =?UTF-8?q?=20release=20create`=20(job=20holds=20contents/id-token/attesta?= =?UTF-8?q?tions:write),=20combine-rules=20pushes=20generated=20outputs=20?= =?UTF-8?q?with=20plain=20`git`=20(contents:write=20on=20main),=20deploy-w?= =?UTF-8?q?eb=20builds=20the=20static=20export=20in=20CI=20(setup-node=202?= =?UTF-8?q?2,=20npm=20ci,=20next=20build=20=E2=80=94=20same=20path=20ci.ym?= =?UTF-8?q?l=20tests)=20and=20uploads=20it=20with=20the=20exactly=20pinned?= =?UTF-8?q?=20SWA=20CLI=202.0.10=20instead=20of=20Azure/static-web-apps-de?= =?UTF-8?q?ploy=20(Oryx=20in-container=20build,=20deploy=20token=20as=20ac?= =?UTF-8?q?tion=20input),=20attests=20portal-.tar.gz=20and=20drops=20?= =?UTF-8?q?the=20unusable=20per-PR=20preview=20environments;=20azure/login?= =?UTF-8?q?=20and=20Azure/functions-action=20are=20SHA-pinned=20with=20ver?= =?UTF-8?q?sion=20comments=20for=20Dependabot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .github/workflows/build-agent.yml | 41 ++++++-- .github/workflows/combine-rules.yml | 24 +++-- .github/workflows/deploy-backend.yml | 4 +- .github/workflows/deploy-mcp.yml | 2 +- .github/workflows/deploy-web.yml | 135 ++++++++++++++----------- .github/workflows/oidc-login-test.yml | 2 +- docs/log.md | 4 + docs/web/portal-navigation-prefetch.md | 2 +- 8 files changed, 132 insertions(+), 82 deletions(-) diff --git a/.github/workflows/build-agent.yml b/.github/workflows/build-agent.yml index c9165ed6..71ab70a2 100644 --- a/.github/workflows/build-agent.yml +++ b/.github/workflows/build-agent.yml @@ -324,13 +324,19 @@ jobs: name: version-metadata path: ${{ env.MANIFEST_NAME }} + # First-party `gh` instead of a third-party action: this job holds contents:write, + # id-token:write and attestations:write, so no code we do not control runs here. + # --generate-notes appends the auto-generated notes below the body. - name: Create GitHub Release if: inputs.create_release - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ steps.version.outputs.tag }} - name: Autopilot Monitor Agent ${{ steps.version.outputs.version }} - body: | + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.version.outputs.tag }} + RELEASE_NAME: Autopilot Monitor Agent ${{ steps.version.outputs.version }} + RELEASE_PRERELEASE: ${{ inputs.prerelease }} + RELEASE_ZIP: ${{ steps.artifact.outputs.versioned_zip_release }} + RELEASE_BODY: | ## Autopilot Monitor Agent ${{ steps.version.outputs.version }} ### Installation @@ -355,9 +361,22 @@ jobs: ``` gh attestation verify AutopilotMonitor-Agent-v${{ steps.version.outputs.version }}.zip --repo okieselbach/AutopilotMonitor ``` - draft: false - prerelease: ${{ inputs.prerelease }} - files: | - ${{ steps.artifact.outputs.versioned_zip_release }} - ${{ env.MANIFEST_NAME }} - generate_release_notes: true + run: | + $ErrorActionPreference = 'Stop' + $notesFile = Join-Path $env:RUNNER_TEMP 'release-notes.md' + Set-Content -Path $notesFile -Value $env:RELEASE_BODY -Encoding utf8 -NoNewline + + $ghArgs = @( + 'release', 'create', $env:RELEASE_TAG, + $env:RELEASE_ZIP, $env:MANIFEST_NAME, + '--repo', $env:GITHUB_REPOSITORY, + '--target', $env:GITHUB_SHA, + '--title', $env:RELEASE_NAME, + '--notes-file', $notesFile, + '--generate-notes' + ) + if ($env:RELEASE_PRERELEASE -eq 'true') { $ghArgs += '--prerelease' } + + & gh @ghArgs + if ($LASTEXITCODE -ne 0) { throw "gh release create failed with exit code $LASTEXITCODE" } + Write-Host "Release $env:RELEASE_TAG created" diff --git a/.github/workflows/combine-rules.yml b/.github/workflows/combine-rules.yml index bccfbaaf..38dae3bc 100644 --- a/.github/workflows/combine-rules.yml +++ b/.github/workflows/combine-rules.yml @@ -63,11 +63,23 @@ jobs: - name: Combine rules into dist/ run: node rules/scripts/combine.js + # Plain git instead of a third-party action: this step runs with contents:write on + # main, so no code we do not control gets to push. Checkout leaves the job's + # GITHUB_TOKEN in the remote credential helper, which is all `git push` needs. + # NOTE: combine.js writes THREE outputs; committing only dist/ would silently discard + # the two generated TS modules and leave the guardrails-in-sync CI check red. - name: Commit generated outputs if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: "chore: regenerate rules/dist + generated TS from individual files" - # combine.js writes THREE outputs; committing only dist/ would silently discard - # the two generated TS modules and leave the guardrails-in-sync CI check red. - file_pattern: "rules/dist/* src/Web/autopilot-monitor-web/utils/guardrails.generated.ts src/McpServer/autopilot-monitor-mcp/src/rule-authoring.generated.ts" + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -- rules/dist/ \ + src/Web/autopilot-monitor-web/utils/guardrails.generated.ts \ + src/McpServer/autopilot-monitor-mcp/src/rule-authoring.generated.ts + if git diff --cached --quiet; then + echo "Generated outputs unchanged - nothing to commit" + exit 0 + fi + git commit -m "chore: regenerate rules/dist + generated TS from individual files" + git push origin HEAD:main diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 2951a7a5..1338d7e5 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v7 - name: Login to Azure (OIDC) - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -65,7 +65,7 @@ jobs: run: dotnet publish ${{ env.CSPROJ_PATH }} --configuration Release --output ./publish --no-build -p:SourceRevisionId=${{ github.sha }} -p:BuildNumber=${{ steps.version.outputs.build_number }} -p:BuildTimestampUtc=${{ steps.version.outputs.build_utc }} - name: Deploy to Azure Functions - uses: Azure/functions-action@v1 + uses: Azure/functions-action@c5060b3b8bb1ebbcb531abd00c822ecbaa8ea656 # v1 with: app-name: ${{ env.FUNCTION_APP }} package: ./publish diff --git a/.github/workflows/deploy-mcp.yml b/.github/workflows/deploy-mcp.yml index b239f1bd..f29a18cc 100644 --- a/.github/workflows/deploy-mcp.yml +++ b/.github/workflows/deploy-mcp.yml @@ -46,7 +46,7 @@ jobs: run: echo "sha=$(git -C mcp-docs rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Login to Azure (OIDC) - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index c2444ac9..f2f49c8f 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -1,5 +1,13 @@ name: Deploy Web (Static Web App) +# Build here, upload with the pinned SWA CLI. No third-party action sees the deploy token +# (the CLI is Microsoft-published npm code at an exact version), the build runs on the same +# path ci.yml tests (setup-node 22 + npm ci + next build) instead of Oryx guessing inside the +# action's container, and the exported site is attested like the agent ZIP and the MSI. +# +# PR preview environments were dropped with the action: their per-PR URLs are never +# registered as redirect URIs, so nobody could log in on them anyway. + on: push: branches: @@ -8,39 +16,44 @@ on: - 'src/Web/autopilot-monitor-web/**' - '.github/workflows/deploy-web.yml' workflow_dispatch: - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - paths: - - 'src/Web/autopilot-monitor-web/**' - - '.github/workflows/deploy-web.yml' + +permissions: + contents: read + id-token: write # keyless Sigstore signing (artifact attestation) + attestations: write # store the provenance attestation env: PORTAL_VERSION_URL: https://portal.autopilotmonitor.com/version.json VERSIONS_CONTAINER: https://autopilotmonitoreu.blob.core.windows.net/versions + WEB_DIR: src/Web/autopilot-monitor-web + # Exact version: `npx --package` resolves from the registry at run time, so the pin is the + # only thing that keeps the deploy step's code fixed. Bump deliberately. + SWA_CLI_VERSION: 2.0.10 jobs: build_and_deploy_job: - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest name: Build and Deploy Job - # Dependabot PRs run without repo secrets, so the deploy token is absent. - # Surface it as an env var so the deploy step can gate on it — secrets - # cannot be referenced directly in an `if:` expression. - env: - DEPLOY_TOKEN: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} steps: - uses: actions/checkout@v7 with: lfs: false fetch-depth: 0 # Full history needed for git-based sitemap lastmod dates - - name: Build And Deploy - id: builddeploy - # Skip (green) instead of failing when the token is missing (Dependabot - # PRs); real deploys on main and internal PRs still have it. - if: env.DEPLOY_TOKEN != '' - uses: Azure/static-web-apps-deploy@v1 + + - uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + cache-dependency-path: ${{ env.WEB_DIR }}/package-lock.json + + - name: Install dependencies + working-directory: ${{ env.WEB_DIR }} + run: npm ci + + # next.config output:'export' -- `next build` writes the static site to out/ + # (prebuild: sync-error-codes, generate-lastmod, generate-version). + - name: Build + working-directory: ${{ env.WEB_DIR }} env: NEXT_PUBLIC_ENTRA_CLIENT_ID: ${{ vars.NEXT_PUBLIC_ENTRA_CLIENT_ID }} # Dual app-reg window: previous app registration (unset = single-app behaviour) @@ -55,28 +68,50 @@ jobs: # Build stamp for public/version.json (scripts/generate-version.js in prebuild); # the verify step below polls the deployed file for exactly this commit. GITHUB_SHA: ${{ github.sha }} - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "src/Web/autopilot-monitor-web" # App source code path - api_location: "" # Api source code path - optional - # Static export (next.config output:'export'): Oryx runs `npm run build` - # and uploads the exported out/ directory as plain static files — no - # managed SSR runtime is provisioned anymore. (No is_static_export - # input on this action version; Oryx detects the export from - # next.config + output_location.) - output_location: "out" # Built app content directory - optional - ###### End of Repository/Build Configurations ###### + run: | + npm run build + test -f out/version.json || { echo "out/version.json missing - export did not run"; exit 1; } - # The SWA action reporting success proves the upload, not that the production host - # serves the new build — verify against the live /version.json (Cache-Control: - # no-store via staticwebapp.config.json) before publishing the version manifest. - # PR builds deploy to per-PR preview environments, so only main deploys are verified. + # One archive of the exact bytes that get uploaded, so the attestation has a single subject. + - name: Package export + id: package + run: | + archive="portal-${GITHUB_SHA::7}.tar.gz" + tar -C "$WEB_DIR/out" -czf "$archive" . + echo "archive=$archive" >> "$GITHUB_OUTPUT" + sha256sum "$archive" + + # Sigstore keyless provenance: binds the export digest to this workflow run (repo, ref, + # commit, workflow file) in a public transparency log. + # Verify: gh attestation verify portal-.tar.gz --repo okieselbach/AutopilotMonitor + - name: Attest build provenance (portal export) + uses: actions/attest-build-provenance@v4 + with: + subject-path: ${{ steps.package.outputs.archive }} + + - name: Upload build artifact + uses: actions/upload-artifact@v7 + with: + name: portal-${{ github.sha }} + path: ${{ steps.package.outputs.archive }} + + # Upload only -- out/ is already built. --swa-config-location points at the app root, + # where staticwebapp.config.json lives (next export does not copy it into out/). + - name: Deploy to Static Web App + env: + SWA_CLI_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} + run: | + npx --yes --package "@azure/static-web-apps-cli@${SWA_CLI_VERSION}" swa deploy \ + --app-location "$WEB_DIR" \ + --output-location out \ + --swa-config-location "$WEB_DIR" \ + --env production \ + --no-use-keychain + + # The CLI reporting success proves the upload, not that the production host serves the + # new build -- verify against the live /version.json (Cache-Control: no-store via + # staticwebapp.config.json) before publishing the version manifest. - name: Verify deployed portal build is live - if: env.DEPLOY_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') shell: pwsh run: | $expectedCommit = '${{ github.sha }}'.Substring(0, 7) @@ -100,10 +135,9 @@ jobs: throw "Portal did not serve commit $expectedCommit within 5 minutes -- deployment not verified, version manifest NOT published" - # Cross-component deploy record, written last and only on a verified deploy — + # Cross-component deploy record, written last and only on a verified deploy -- # same contract as versions/backend.json and versions/mcp.json. - name: Publish version manifest - if: env.DEPLOY_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') shell: pwsh run: | $manifest = [ordered]@{ @@ -119,22 +153,3 @@ jobs: Invoke-RestMethod -Uri $url -Method Put -Headers @{ 'x-ms-blob-type' = 'BlockBlob'; 'x-ms-blob-cache-control' = 'no-cache' } -Body $manifest -ContentType 'application/json' | Out-Null Write-Host "Published versions/web.json" Write-Host $manifest - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - env: - DEPLOY_TOKEN: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} - steps: - - name: Close Pull Request - id: closepullrequest - if: env.DEPLOY_TOKEN != '' - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_SWA_DEPLOY_TOKEN }} - action: "close" - - - - diff --git a/.github/workflows/oidc-login-test.yml b/.github/workflows/oidc-login-test.yml index 72114b92..7da6d381 100644 --- a/.github/workflows/oidc-login-test.yml +++ b/.github/workflows/oidc-login-test.yml @@ -15,7 +15,7 @@ jobs: name: Federated login + read check steps: - name: Login to Azure (OIDC) - uses: azure/login@v3 + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} diff --git a/docs/log.md b/docs/log.md index 6298d240..ccdaa337 100644 --- a/docs/log.md +++ b/docs/log.md @@ -1,5 +1,9 @@ # Log +## 2026-08-30 (12) + +* **Update**: `web/portal-navigation-prefetch.md` — hosting citation now points at `deploy-web.yml`: the portal is built in CI (setup-node 22, `npm ci`, `next build`) and uploaded with the pinned SWA CLI; the Azure/static-web-apps-deploy action, its Oryx in-container build and the per-PR preview environments are gone. All non-GitHub-owned actions are SHA-pinned; the release and rules auto-commit steps use first-party `gh`/`git`. + ## 2026-08-30 (11) * **New**: `agent/ime-process-watcher.md` — `ImeProcessWatcher` no longer attaches to the first `IntuneManagementExtension` name match: `ImeProcessIdentity` requires session 0 + image under the IME install root (`QueryFullProcessImageName`, cross-bitness), prefers the oldest trusted candidate, logs untrusted matches once per PID, and re-arms discovery after every reported exit (previously one-shot — a fake in a user session could forge `ime_process_exited` and mute the real signal for the session; even an IME service restart was never watched again). Registered in `index.md`. diff --git a/docs/web/portal-navigation-prefetch.md b/docs/web/portal-navigation-prefetch.md index 940aa09c..3d7af514 100644 --- a/docs/web/portal-navigation-prefetch.md +++ b/docs/web/portal-navigation-prefetch.md @@ -123,4 +123,4 @@ Application Insights resource — the prefetch is cheap only while the runtime i * `src/Web/autopilot-monitor-web/components/GlobalSidebar.tsx` — nav link rendering, rationale comment * `src/Web/autopilot-monitor-web/components/Navbar.tsx` — top-bar and notification links -* [SWA and Oryx node pinning](../architecture.md) — hosting model for the web app +* `.github/workflows/deploy-web.yml` — hosting model: static export built in CI (Node 22 via setup-node, `engines` in package.json), uploaded with the pinned SWA CLI