Version Management¶
Extracted from
.github/copilot-instructions.md§13 on 2026-05-22 to keep the always-loaded charter lean. Read this when bumping the release version or touching the header version stamp pipeline.
The control plane carries a small release + build stamp in the SPA header
(v0.2.17 · 4060551). The release version is bumped by a single script that
reads Conventional Commits since the last tag — no manual edits to
package.json or pyproject.toml. The build number is not committed; it is
computed at build time from commits since the latest release tag.
1. Version Policy¶
| Segment | Trigger | Decided by |
|---|---|---|
| A | breaking product generation change you decide to ship | manual (--major flag) |
| B | release train bump when any feat: / fix: commit is ready to ship |
auto, or manual with --release / --minor |
| C/build | commit count since the latest vA.B.0 release tag |
build-time only |
If a BREAKING CHANGE footer or feat!: / fix!: marker is detected in
the range, the script refuses to auto-bump and exits with code 2 — pass
--major to acknowledge. This prevents accidental A bumps from being hidden
behind a routine commit message.
chore:, docs:, test:, refactor:, style:, build:, ci: commits
do not trigger a bump. If nothing in the range warrants a bump, the
script exits 0 with nothing to bump. Force with --release / --minor /
--major only when you have a reason that doesn't map to a commit type.
web/package.json is the source of truth and stores release versions as
A.B.0. pyproject.toml is kept in sync by the same script so the backend
image carries the same release identifier. The displayed C value comes from
APP_BUILD_NUMBER, not from either file.
2. Header stamp pipeline¶
┌─────────────────────────────────────────────────────────────┐
│ quick-deploy.sh frontend / postprovision.sh │
│ ├─ APP_VERSION = node -p require('web/package.json') │
│ ├─ APP_BUILD_NUMBER = git rev-list --count <tag>..HEAD │
│ ├─ GIT_COMMIT = git rev-parse --short HEAD │
│ └─ BUILD_TIME = date -u +%Y-%m-%dT%H:%M:%SZ │
│ │ │
│ ▼ --build-arg │
│ az acr build (web/Dockerfile) │
│ ├─ ARG APP_VERSION / APP_BUILD_NUMBER / GIT_COMMIT / ... │
│ └─ ENV APP_VERSION=... APP_BUILD_NUMBER=... │
│ │ │
│ ▼ npm run build │
│ web/vite.config.ts define: │
│ ├─ __APP_VERSION__ = process.env.APP_VERSION ?? pkg │
│ ├─ __APP_BUILD_NUMBER__ = env ?? count from latest tag │
│ ├─ __APP_COMMIT__ = process.env.GIT_COMMIT ?? git │
│ └─ __APP_BUILD_TIME__ = env ?? now() │
│ │ │
│ ▼ baked into dist/assets/index-*.js │
│ web/src/components/Layout.tsx → `vA.B.<build> · <sha>` │
└─────────────────────────────────────────────────────────────┘
Why host-side resolution is required. ACR build context excludes
.git (and even with it, the build container's WORKDIR is /web, not
the repo root). The vite define falls back to git rev-parse for
local npm run dev and npm run build, but production builds must pass
the values through --build-arg. Both quick-deploy.sh frontend and
postprovision.sh resolve them on the host. The Dockerfile already declares
those ARGs so a missing build-arg shows up as empty string, not as a build
failure.
Build number rule. APP_BUILD_NUMBER is the commit count from the latest
merged vA.B.0 tag to HEAD. On the exact release tag this is 0, so a
release stamped 0.2.0 displays as v0.2.0. The first post-release commit
displays as v0.2.1, the next as v0.2.2, and so on, while the committed
release version remains 0.2.0 until the next release bump.
Where the values surface in the UI. The caption is rendered next to
"Control Plane" in web/src/components/Layout.tsx
using the injected globals declared in
web/src/vite-env.d.ts. The native
title= tooltip carries the release, displayed build version, build number,
commit, and build timestamp.
3. Bumping the version¶
Agent operating procedure:
- For any task that adds code, updates behaviour, or fixes a bug, evaluate the release impact before final handoff and state one recommendation:
major,minor/release, orno release bump. - Do not run the non-dry-run bump command automatically after ordinary implementation. Ask the maintainer to approve the exact bump path first, then run the script yourself once approved.
- When asked for the current version, read web/package.json and pyproject.toml, verify they match, and include the latest release tag / build-number meaning when useful.
- When asked to bump the version, run
scripts/dev/bump-version.sh --dry-run, summarize what it would do, recommend the exact follow-up command, and wait for approval before running it. - If asked for a
patchbump, explain thatCis computed at build time and--patchis intentionally rejected. For a shipped fix under this policy, use the nextminor/releasebump unless the change is breaking and needs--major.
# Dry-run first to see what would happen.
scripts/dev/bump-version.sh --dry-run
# Auto (feat/fix → next release train).
scripts/dev/bump-version.sh
# Manual override.
scripts/dev/bump-version.sh --major
scripts/dev/bump-version.sh --release
scripts/dev/bump-version.sh --minor
# Push the release commit + tag.
git push origin "$(git rev-parse --abbrev-ref HEAD)" --follow-tags
The script:
- Reads the current version from
web/package.json(source of truth). - Scans commits in
<last-v-tag>..HEAD(or full history if no tag exists). - Decides the bump kind from Conventional Commits:
feat:andfix:both move to the nextBrelease;BREAKING CHANGErequires--major. - Rewrites
web/package.json(vianode) and the firstversion = "…"line in pyproject.toml. - Creates
chore(release): vA.B.0commit + annotatedvA.B.0tag. - Does not push — that stays an explicit step so the maintainer can review the diff first.
4. Release workflow¶
Routine — feature shipped via PR, ready to cut a release:
- Merge the PR(s) to
mainusing Conventional Commits subjects. scripts/dev/bump-version.sh --dry-run→ confirm the bump kind.scripts/dev/bump-version.sh→ creates commit + tag locally.- Inspect:
git show HEAD(release commit),git tag -v vA.B.0(tag). git push origin main --follow-tags→ cloud picks up the new tag for GitHub releases automation.- Deploy the frontend with the new build stamp visible in the header:
- Open the cloud URL, hover the header version caption, confirm the
displayed build number and commit short SHA match the deployed
HEAD.
Hotfix — branch from a tag, ship the next release train:
git checkout -b hotfix/v0.2.0 v0.1.0
# … fix: … commit …
scripts/dev/bump-version.sh --release
git push origin hotfix/v0.2.0 --follow-tags
5. Validation checklist¶
Before pushing a release tag:
-
scripts/dev/bump-version.sh --dry-runshows the expected bump kind. -
web/package.jsonandpyproject.tomlversions match (git diff HEAD~1). -
cd web && npm run buildsucceeds andgrep -oE '"<short-sha>"' dist/assets/index-*.jsreturns the new SHA. -
grep -oE '"[0-9]+"' dist/assets/index-*.jsor the header tooltip confirms the expected build number. - Header tooltip shows the expected release version, displayed build version, build number, commit, and timestamp after page reload.
- No other unrelated diffs in the release commit (the script refuses if
package.json/pyproject.tomlalready have unstaged edits).
6. Common mistakes¶
- Editing
package.jsonversion by hand. The script will still run, but the commit history won't have thechore(release): vA.B.0marker that ties the tag to a known author intent. Always go through the script. - Pushing without
--follow-tags. The release commit lands but the tag stays local — production never sees the new version. Usegit push origin <branch> --follow-tags. - Deploying frontend without re-running
quick-deploy.sh. A pureazd provisiondoesn't bake the new version+SHA — only the--build-argpath throughquick-deploy.sh(or a refreshed postprovision build) carries the stamp. The pre-flight Step 0 inquick-deploy.shresolves the host-side values automatically. - Bumping inside a feature branch. Keep the bump on the same branch
that will land on
main(or on the hotfix branch). Bumping then rebasing rewrites the tag's target commit and confuses anyone who already pulled it. - Using
--patch.Cis now the build number, so--patchis rejected. Use--release/--minorfor the nextBrelease train, or--majorwhen deliberately moving to the nextAgeneration. - Forgetting
pyproject.toml. The script handles this for you; just don't editpyproject.tomlversionmanually — it must always equalweb/package.jsonversion.