All three components now report <major>.<minor>.<build> plus a 7-char commit and a build timestamp. The backend had been pinned at a hand-written 1.0.0 since inception and starts at 1.5.1000; the MCP server's package.json version was bumped by hand and starts at 1.5.300, and it now reports its own commit on /health instead of only the documentation bundle's. The build number keeps coming from a counter blob reserved before the build with ETag-CAS, but there is now one implementation instead of two copies: .github/scripts/Request-BuildNumber.ps1, called by all three workflows and by the local build.ps1. It lives under .github/ because scripts/Deployment/ is gitignored (those scripts carry SAS tokens) and would therefore be absent from a CI checkout; scripts/Build/ and scripts/Release/ are shadowed by the [Bb]uild/ and [Rr]elease/ artifact ignores. The counter cannot be replaced by the published manifest, which is the obvious simplification to reach for: version.json is written only on stable publish, so two consecutive dev builds would derive the same number and the second would overwrite the first's artifact. git rev-list --count fails the same way, two builds of one commit collide. Both deploy workflows now poll their health endpoint until it reports exactly the version and commit just deployed, and publish versions/<component>.json only then. A green deploy step alone means the package was accepted, not that it is being served -- so a manifest that disagrees with the live endpoint now means the running instance drifted (rollback, stale revision) rather than a pipeline that stopped halfway. The agent manifest gains the same commit and buildUtc fields additively; the bootstrap reads it by field name, so older installers ignore them, and sha256/bootstrapVersion stay untouched. Requires the versions container plus AZURE_BLOB_VERSIONS_SAS_TOKEN and the two seeded counters (backend 999, mcp 299) before the first deploy run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.0 KiB
type, title, description, resource, tags, timestamp
| type | title | description | resource | tags | timestamp | ||||
|---|---|---|---|---|---|---|---|---|---|
| Concept | Version Contract — Agent, Backend, MCP | One version shape for all three 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 |
|
2026-07-22T00:00:00+02:00 |
Version Contract
Three components ship from this repository — the agent, the Functions backend and the MCP server. 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.
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 |
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.
- Anonymous
GETon the counter blob → valueNandETag. The blob holds the last used number as plain text and is public-read; only the write needs the container SAS. PUT N+1withIf-Match: <etag>. The quoted ETag form is required — PowerShell strips the quotes on read, and an unquotedIf-Matchis rejected client-side.412 Precondition Failed→ a concurrent build reserved first → re-read and retry (5 attempts, linear backoff).- 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.
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.minoris a human decision, made in the component's own file (VersionPrefix, orversioninpackage.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
versionscontainer onautopilotmonitoreu(writes viaAZURE_BLOB_VERSIONS_SAS_TOKEN). The agent's counter stays in theagentcontainer 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/build-agent.yml,scripts/Deployment/build.ps1— agent line- Build Counter Blob — agent-specific consequences