Files
AutopilotMonitor/docs/versioning.md
T
Oliver Kieselbach c4280e823c fragility audit P5.x + P7: drift guards across SignalR names, RowKeys, config sections, scoped routing
P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.

P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).

P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.

P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.

P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).

P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.

P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).

Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 23:17:48 +02:00

5.8 KiB

type, title, description, resource, tags, timestamp
type title description resource tags timestamp
Concept Version Contract — Agent, Backend, MCP, Web One version shape for all shipping components, the ETag-CAS counter blob that mints build numbers, and the published manifests that make "what is deployed" checkable from outside. /.github/scripts/Request-BuildNumber.ps1
build
ci
release
versioning
2026-07-22T00:00:00+02:00

Version Contract

Four components ship from this repository — the agent, the Functions backend, the MCP server and the web portal. They share one version shape, one way of minting build numbers, and one manifest format, so that a version string means the same thing whoever emits it. The web portal is the commit-only exception: a static export has no runtime that could report a major.minor.build, so it carries commit + buildUtc alone (no counter blob).

version   = <major>.<minor>.<build>    major.minor curated by hand, build minted per build
commit    = 7-char short SHA of this repo
buildUtc  = ISO-8601 build moment
Component major.minor from Counter blob Reported by
Agent V2 VersionPrefix in AutopilotMonitor.Agent.V2.csproj agent/buildcounter-v2.txt agent/version.json
Backend VersionPrefix in AutopilotMonitor.Functions.csproj versions/backend.txt GET /api/health
MCP version in package.json versions/mcp.txt GET /health, MCP handshake
Web portal — (commit-only) — GET /version.json on the portal host

Schema

Build numbers — reserve before build, ETag-CAS

.github/scripts/Request-BuildNumber.ps1 is the only implementation; the CI workflows and the local build.ps1 all call it.

  1. Anonymous GET on the counter blob → value N and ETag. The blob holds the last used number as plain text and is public-read; only the write needs the container SAS.
  2. PUT N+1 with If-Match: <etag>. The quoted ETag form is required — PowerShell strips the quotes on read, and an unquoted If-Match is rejected client-side.
  3. 412 Precondition Failed → a concurrent build reserved first → re-read and retry (5 attempts, linear backoff).
  4. Build with the reserved number.

Reserving before the build is what makes concurrent minting impossible. A failed build burns a number — deliberate: uniqueness matters, density does not.

Why a counter and not the published manifest: manifests are written on publish, not on build. The agent writes version.json only for stable releases, so deriving the next number from it would hand two consecutive dev builds the same number and let the second overwrite the first's artifact. A state-free scheme such as git rev-list --count HEAD fails for the same reason — two builds of one commit collide.

Seeding (once per component): PUT <last used number> with If-None-Match: *, so an existing counter can never be clobbered. Agent V2 was seeded with 1356 on 2026-07-17; versions/backend.txt with 999 and versions/mcp.txt with 299 on 2026-07-22.

Local builds

No counter is reached, the build number stays 0, and the commit is empty or unknown. A 1.5.0 with no commit hash on /api/health is therefore a developer machine, not a release — the fallback is the signal, not a defect. The agent is the exception: build.ps1 produces uploadable artifacts and reserves a real number unless -SkipUpload is passed.

Manifests

versions/backend.json and versions/mcp.json (public-read) record what is deployed:

{ "component": "backend", "version": "1.5.1000", "commit": "abc1234",
  "buildUtc": "…", "deployedUtc": "…", "runId": "…" }

The MCP manifest adds docsCommit, because for that component "which build" and "which documentation" are separate questions. versions/web.json carries component, commit, deployedUtc and runId (no version/buildUtc — the served /version.json holds the build moment). The portal's own stamp is generated into public/version.json by scripts/generate-version.js (prebuild) from GITHUB_SHA, served with Cache-Control: no-store, and deploy-web.yml polls it until the deployed commit matches the pushed SHA before publishing the manifest.

Each is written last and only after the deploy workflow has polled the component's health endpoint until it reports that exact version and commit. A green deploy step alone means the package was accepted, not that it is being served. Consequently a manifest that disagrees with the live endpoint means the running instance drifted after the fact (rollback, stale revision) rather than a half-finished pipeline.

The agent keeps its own manifest at agent/version.json — the bootstrap derives that URL from the download URL and verifies the ZIP against its sha256, so it cannot move. It carries the same commit and buildUtc fields; sha256 and bootstrapVersion remain, being the customer-facing integrity contract.

Consequences

  • Bumping major.minor is a human decision, made in the component's own file (VersionPrefix, or version in package.json). Nothing bumps it automatically.
  • A new component joins by seeding a counter blob and calling the same script; no new mechanism.
  • The counter blobs and manifests live in the versions container on autopilotmonitoreu (writes via AZURE_BLOB_VERSIONS_SAS_TOKEN). The agent's counter stays in the agent container next to the artifacts it numbers.

Citations

  • .github/scripts/Request-BuildNumber.ps1 — the CAS protocol
  • .github/workflows/deploy-backend.yml, deploy-mcp.yml — reserve, verify, publish
  • .github/workflows/deploy-web.yml, src/Web/autopilot-monitor-web/scripts/generate-version.js — web stamp, verify, publish
  • .github/workflows/build-agent.yml, scripts/Deployment/build.ps1 — agent line
  • Build Counter Blob — agent-specific consequences