diff --git a/.github/scripts/Publish-BootstrapScripts.ps1 b/.github/scripts/Publish-BootstrapScripts.ps1 index eb421f94..aa2a0e4a 100644 --- a/.github/scripts/Publish-BootstrapScripts.ps1 +++ b/.github/scripts/Publish-BootstrapScripts.ps1 @@ -125,6 +125,23 @@ function Get-BootstrapScriptVersion { throw "Could not parse ScriptVersion from $Origin" } +# What the script actually DOES, with comments and layout removed: the PowerShell token +# stream minus comments, newlines and line continuations. Lets the version-bump guard tell +# a behaviour change (must be versioned -- consumers act on the version) apart from a +# comment fix or reflow (nobody needs to know). Tokenising rather than regex-stripping +# keeps '#' inside strings from being mistaken for a comment. +function Get-CodeFingerprint { + param([string]$Text, [string]$Origin) + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseInput($Text, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors -and $errors.Count -gt 0) { + throw "Parse error in ${Origin}: $($errors[0])" + } + $ignored = @('Comment', 'NewLine', 'LineContinuation', 'EndOfInput') + return (($tokens | Where-Object { $ignored -notcontains $_.Kind.ToString() } | ForEach-Object { $_.Text }) -join "`n") +} + # ------------------------------------------------------------------ 1. source $sourceContent = Get-Content $BootstrapSource -Raw $sourceBytes = Get-PublishBytes $sourceContent @@ -150,14 +167,21 @@ if ($null -eq $publishedBytes) { } elseif ((Get-Sha256 $publishedBytes) -eq (Get-Sha256 $sourceBytes)) { Write-Host 'Published bootstrap is already byte-identical to the source' } else { - $publishedVersion = Get-BootstrapScriptVersion -Content ([System.Text.Encoding]::UTF8.GetString($publishedBytes)) -Origin "$AliasUrl/$BootstrapBlob" - if ($publishedVersion -eq $scriptVersion) { - throw ("Version-bump guard: $BootstrapBlob changed but ScriptVersion is still $scriptVersion. " + + $publishedText = [System.Text.Encoding]::UTF8.GetString($publishedBytes) + $publishedVersion = Get-BootstrapScriptVersion -Content $publishedText -Origin "$AliasUrl/$BootstrapBlob" + + if ($publishedVersion -ne $scriptVersion) { + Write-Host "Version bump: $publishedVersion -> $scriptVersion" + } elseif ((Get-CodeFingerprint -Text $publishedText -Origin "$AliasUrl/$BootstrapBlob") -eq + (Get-CodeFingerprint -Text $sourceContent -Origin $BootstrapSource)) { + # Comments, typos in comments, reflow -- nothing a consumer of the version can act on. + Write-Host "::warning::$BootstrapBlob changed in comments or formatting only; publishing under the unchanged version $scriptVersion." + } else { + throw ("Version-bump guard: $BootstrapBlob changed behaviour but ScriptVersion is still $scriptVersion. " + "Bump it in $BootstrapSource -- the docs badge, version.json.bootstrapVersion and the " + - 'portal outdated-script hint all read that value and would keep reporting a version ' + - 'that no longer matches what customers download.') + 'portal outdated-script hint all read that value, and customers decide from it whether ' + + 'to re-upload their Intune copy. Comment-only and formatting changes do not need a bump.') } - Write-Host "Version bump: $publishedVersion -> $scriptVersion" } # ------------------------------------------------------------------ 4. upload diff --git a/docs/log.md b/docs/log.md index b832c904..e798fc5f 100644 --- a/docs/log.md +++ b/docs/log.md @@ -2,7 +2,7 @@ ## 2026-08-19 -* **New**: `script-publishing.md` — the customer script publishing chain. `.github/scripts/Publish-BootstrapScripts.ps1` becomes the single owner (dev render, version-bump guard, uploads, version oracles, alias verification), called both by the new `publish-scripts.yml` (push to `main` on `scripts/Bootstrap/**` and `scripts/CustomerSetup/**`) and by the `publish_as_stable` branch of `build-agent.yml`. Script publishing no longer waits for an agent release. Blobs are published in a deterministic CRLF/UTF-8-no-BOM form so byte comparisons do not depend on the checkout; the Pester + ASCII gates move into the reusable `bootstrap-script-gates.yml` and block the upload; `AdminConfiguration.LatestBootstrapV2ScriptVersion` now has exactly one writer. +* **New**: `script-publishing.md` — the customer script publishing chain. `.github/scripts/Publish-BootstrapScripts.ps1` becomes the single owner (dev render, version-bump guard, uploads, version oracles, alias verification), called both by the new `publish-scripts.yml` (push to `main` on `scripts/Bootstrap/**` and `scripts/CustomerSetup/**`) and by the `publish_as_stable` branch of `build-agent.yml`. Script publishing no longer waits for an agent release. Blobs are published in a deterministic CRLF/UTF-8-no-BOM form so byte comparisons do not depend on the checkout; the Pester + ASCII gates move into the reusable `bootstrap-script-gates.yml` and block the upload; `AdminConfiguration.LatestBootstrapV2ScriptVersion` now has exactly one writer. The version-bump guard compares a code fingerprint (PowerShell token stream without comments, newlines and line continuations), so only behaviour changes require a `$ScriptVersion` bump — comment and formatting changes publish with a warning. ## 2026-08-18 diff --git a/docs/script-publishing.md b/docs/script-publishing.md index 007c8c24..b30c87a5 100644 --- a/docs/script-publishing.md +++ b/docs/script-publishing.md @@ -55,8 +55,9 @@ interleave with a release writing the agent fields. 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 differs from the source but carries the - same `$ScriptVersion`, the run aborts. See [Version oracles](#version-oracles). +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](#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.** @@ -92,8 +93,16 @@ Three places state the bootstrap script version, and all three are derived from 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: changed content under an unchanged version is -invisible to every consumer. +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