Files
AutopilotMonitor/docs/script-publishing.md
T
Oliver Kieselbach c0be389a95 ci(scripts): version-bump guard compares behaviour, not bytes
Byte comparison made the guard fire on comment fixes, typos in comments and
reflow -- changes no consumer of the version can act on, so forcing a bump for
them is friction without a payoff.

The guard now compares a code fingerprint: the PowerShell token stream with
comments, newlines and line continuations removed. A behaviour change under an
unchanged $ScriptVersion still fails; comment and formatting changes publish
with a warning. That is also the rule the source file's own comment on
$ScriptVersion already states ("bump on meaningful changes").

Tokenising rather than regex-stripping keeps a '#' inside a string literal from
being mistaken for a comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 15:15:48 +02:00

7.5 KiB

type, title, description, resource, tags, timestamp
type title description resource tags timestamp
Concept Customer Script Publishing — download.autopilotmonitor.com/agent/ How the customer-facing PowerShell scripts reach the download alias, why publishing is decoupled from agent releases, and the guards that keep the published copy, the version badge and the portal's version oracle in agreement. /.github/scripts/Publish-BootstrapScripts.ps1
build
ci
release
bootstrap
download-alias
2026-08-19T00:00:00+02:00

Overview

Four PowerShell scripts are served to customers from https://download.autopilotmonitor.com/agent/:

Blob Source Consumed by
Install-AutopilotMonitor.ps1 scripts/Bootstrap/Install-AutopilotMonitor.ps1 Intune platform script (customer uploads a copy); downloaded live on every device by the WDP bootstrap MSI
Install-AutopilotMonitor-Dev.ps1 rendered from the same source dev fleet
Test-ShouldBootstrapAgent.ps1 scripts/Bootstrap/Test-ShouldBootstrapAgent.ps1 customer dry run via irm ... | iex
Grant-AutopilotMonitorAddOn.ps1 scripts/CustomerSetup/Grant-AutopilotMonitorAddOn.ps1 optional Graph permission grant

Uninstall-AutopilotMonitor.ps1 is deliberately not published — nothing links it, and an unauthenticated uninstall script on the public download host is not a feature.

The alias is a Front Door route in front of the agent container; see url-registry.md for the host registry and the migration to the alias.

Publishing chain

.github/scripts/Publish-BootstrapScripts.ps1 is the single owner. Two workflows call it with identical arguments:

  • publish-scripts.yml — on every push to main touching scripts/Bootstrap/**, scripts/CustomerSetup/** or the publisher itself, plus workflow_dispatch with a dry_run input.
  • build-agent.yml — inside the publish_as_stable branch, after version.json is uploaded, so a release lands a set consistent with what is already live.

Before this split, the script blobs rotated only on a stable agent release. A bootstrap fix therefore sat unpublished until the next agent cutover — while the WDP MSI downloads the published copy on every enrolling device. Script publishing is now decoupled from the agent release cadence; the release path still publishes, so it can never fall behind either.

Both workflows share the concurrency group agent-blob-publish (cancel-in-progress: false). Both write version.json, and the publisher's bootstrapVersion read-modify-write must not interleave with a release writing the agent fields.

Publisher steps

  1. Parse $ScriptVersion from the bootstrap source.
  2. Dev render — literal substitution of the two $AgentDownloadUrl / $VersionJsonName defaults. A missing anchor literal is a hard failure: a silently un-substituted dev script would point the dev fleet at the stable agent.
  3. Version-bump guard — if the published bootstrap changed behaviour under an unchanged $ScriptVersion, the run aborts. Comment and formatting changes publish with a warning. See Version oracles.
  4. Upload every blob with Cache-Control: no-cache (they all rotate in place), mirrored fail-soft to the legacy storage account.
  5. Reconcile the version oracles.
  6. Verify through the alias — re-download each blob and compare SHA-256, six attempts 20 s apart. A persistent mismatch means blobs were written but Front Door serves stale content, and the error names the az afd endpoint purge command.

Deterministic publish form

Every blob is published as CRLF, UTF-8 without BOM, normalized by the publisher rather than taken from the working copy. Git normalizes line endings on checkout — a Windows runner yields CRLF, a core.autocrlf=false clone yields LF — so publishing raw working-copy bytes would emit different blobs for identical source and make every byte comparison, guard and verification alike, depend on where the workflow ran. CRLF is also what PS 5.1 expects on the device and what has always been published.

The ASCII-only rule for scripts/Bootstrap/*.ps1 (enforced by the gates below) is unrelated but adjacent: PS 5.1 reads BOM-less files as ANSI and corrupts multi-byte characters.

Version oracles

Three places state the bootstrap script version, and all three are derived from $ScriptVersion in the source:

  • version.json.bootstrapVersion — read by the docs badge. Written by the publisher as a read-modify-write under If-Match, so a concurrent agent release keeps its own fields. build-agent.yml seeds the field when it builds the manifest so the published manifest is never briefly missing it; the publisher owns and re-asserts it afterwards.
  • AdminConfiguration.LatestBootstrapV2ScriptVersion — read by the portal to tell a customer their uploaded copy is outdated. Only the publisher writes it; build-agent.yml writes the LatestAgentV2* fields and nothing else. One writer per field.
  • The Bootstrap script version: v<x> marker the script logs, parsed out of captured platform script stdout by utils/bootstrapVersion.ts.

The version-bump guard exists because those three would otherwise keep asserting a version that no longer matches what customers download: a behaviour change under an unchanged version is invisible to every consumer, and customers decide from that version whether to re-upload their Intune copy.

"Changed" means changed behaviour, matching what the source file's own comment on $ScriptVersion already asks for ("bump on meaningful changes"). The guard compares a code fingerprint — the PowerShell token stream with comments, newlines and line continuations removed — so comment fixes, typo corrections in comments and reflow publish with a ::warning:: instead of failing. Tokenising rather than regex-stripping keeps a # inside a string literal from being mistaken for a comment.

Gates

.github/workflows/bootstrap-script-gates.yml is a workflow_call workflow holding the checks once:

  • Pester suite under Windows PowerShell 5.1 — the runtime IME uses for platform scripts.
  • ASCII-only scan over scripts/Bootstrap/*.ps1.
  • Publish-BootstrapScripts.ps1 -DryRun — exercises the dev-render anchors and the version-bump guard, and reports per blob whether the alias is current, stale or missing.

Two callers: pester.yml on pull requests (reports, does not block the merge button), and publish-scripts.yml on pushes to main, where the gates job is a needs: dependency of the publish job and therefore blocks the upload. pester.yml deliberately has no push trigger — it would duplicate the gate run with no consequence.

Operating it

  • Dry run and drift check, no credentials needed for the read paths: pwsh .github/scripts/Publish-BootstrapScripts.ps1 -DryRun
  • Republish without a code change: workflow_dispatch on Publish customer scripts with dry_run=false.
  • Front Door serving stale content after a successful upload: az afd endpoint purge --resource-group rg-autopilotmonitor-prd-gwc --profile-name autopilotmonitor-fd --endpoint-name apm-download --content-paths '/agent/*'

Citations

  • .github/scripts/Publish-BootstrapScripts.ps1
  • .github/workflows/publish-scripts.yml, .github/workflows/bootstrap-script-gates.yml, .github/workflows/pester.yml
  • .github/workflows/build-agent.yml (the publish_as_stable branch of the upload step)
  • src/Agent/AutopilotMonitor.BootstrapMsi/Invoke-BootstrapDownload.ps1
  • src/Web/autopilot-monitor-web/utils/bootstrapVersion.ts