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>
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 |
|
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 tomaintouchingscripts/Bootstrap/**,scripts/CustomerSetup/**or the publisher itself, plusworkflow_dispatchwith adry_runinput.build-agent.yml— inside thepublish_as_stablebranch, afterversion.jsonis 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
- Parse
$ScriptVersionfrom the bootstrap source. - Dev render — literal substitution of the two
$AgentDownloadUrl/$VersionJsonNamedefaults. A missing anchor literal is a hard failure: a silently un-substituted dev script would point the dev fleet at the stable agent. - 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. - Upload every blob with
Cache-Control: no-cache(they all rotate in place), mirrored fail-soft to the legacy storage account. - Reconcile the version oracles.
- 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 purgecommand.
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 underIf-Match, so a concurrent agent release keeps its own fields.build-agent.ymlseeds 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.ymlwrites theLatestAgentV2*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 byutils/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_dispatchon Publish customer scripts withdry_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(thepublish_as_stablebranch of the upload step)src/Agent/AutopilotMonitor.BootstrapMsi/Invoke-BootstrapDownload.ps1src/Web/autopilot-monitor-web/utils/bootstrapVersion.ts