Release Workflow
- Release Workflow
- At a glance
- Branches
- Tags
- Feature and bugfix workflow
- New-release workflow
- LTS lines (
release/X.Y.x) - The build counter and manual QA (
BUILD/-b0.0.N) - Old-version hotfix workflow (an LTS line that isn’t the latest)
- Latest-version hotfix workflow
- Branch protection
- Conventional Commits, changelog and build metadata (DEV-3005)
- Changelog pages and release.json (what the CI/CD Manager reads)
- Which ref carries which docs version
- Documentation
- Interactive installer gate
- Releasing
- Status of the DEV-3005 tooling
- Bootstrap commands
- Our own images
- Files touched by
misc/release.sh - Troubleshooting
Version 1.7.0 | Updated: October 06, 2026
Release Workflow
Status: implemented (DEV-3005, 2026-09-18). Git Flow. This document is the authoritative branching/release policy for this repository;
misc/release.shimplements thecut/tagflow described below. The branching model itself is sourced fromdocs-portal/7.1.2/ci-cd/git-flow— this page is this repository’s application of that model, including which tags trigger what and which filesmisc/release.shtouches.
At a glance
FEATURE WORK
feature/*, bugfix/* --(PR, --no-ff)--> develop
|
| ./misc/release.sh cut
v
release/X.Y.Z (stabilization)
|
QA deploys it to a test server by hand
(bootstrap.sh --branch release/X.Y.Z),
fixes land as more commits; before each
re-deploy: ./misc/release.sh build
|
[ QA signs off manually ]
|
./misc/release.sh tag --qa-approved
|
+--------------------------+--------------------------+
| |
v v
main release/X.Y.x (LTS,
tagged vX.Y.Z created from the tag,
(GitHub Release created) never deleted)
|
| merge back --no-ff
v
develop
OLD-LINE HOTFIX (a release/X.Y.x that is no longer the latest):
hotfix/* --(branch from release/X.Y.x, merge --no-ff back)--> release/X.Y.x
./misc/release.sh tag --qa-approved --> vX.Y.Z+1
fix commit(s) then CHERRY-PICKED (never merged) forward
to main and develop
The sections below are the detail this diagram doesn’t show: what each branch is for and how long it lives, exact tag semantics, the build counter, branch protection, and every command in full.
Branches
| Branch | Cut from | Purpose | Lifetime |
|---|---|---|---|
|
— |
Production-ready code only. Every production tag ( |
Permanent |
|
|
Permanent integration branch for the next release. All |
Permanent |
|
|
New functionality for the next release. |
Deleted after merge |
|
|
Non-emergency fixes for the next release. |
Deleted after merge |
|
|
Stabilization branch for one release: bug fixes and release-notes/version bumps only, no new features. After |
Kept for good — deleting it unpublishes that version’s docs |
|
created from the |
Long-lived line for one supported major.minor version. QA installs a specific line here, and future patches for that line are tagged from it. |
Kept for as long as the line is supported |
|
|
Emergency fix outside the normal release cadence. |
Deleted after merge |
Note the literal x in release/X.Y.x — it is not a version number, it is the
LTS branch name for an entire major.minor line (e.g. release/1.7.x covers every
1.7.Z patch). misc/release.sh tells the two apart with regexes:
release/\.[0-9]\.` for a stabilization branch,
`release/[0-9]\.[0-9]+\.x (literal x) for an LTS branch.
Tags
-
vX.Y.Z— the production release identity. Created only bymisc/release.sh tag --qa-approved, once, onmain(new release) or on the relevantrelease/X.Y.x(LTS patch).changelog-check.ymlrequires a matching## vX.Y.Zentry inCHANGELOG.mdfor everyvX.Y.Ztag pushed. -
Optional beta/RC tags (e.g.
vX.Y.Z-rc.1) — not currently produced by any script in this repository; if a pre-release needs to be shared beforetag --qa-approvedruns, tag it manually following that pattern so it never collides with a productionvX.Y.Ztag. -
X.Y.Z-b0.0.N(frommisc/version-string.sh) is not a release tag at all — it is the build/runtime version string a running deployment reports, whereNis theBUILDcounter for the currentVERSION. It identifies a specific build of a version, not a production release; the production release identity is always thevX.Y.Ztag, never this string.
Feature and bugfix workflow
-
Branch
feature/<DEV-xxxx>-<slug>orbugfix/<DEV-xxxx>-<slug>fromdevelop. -
Commit using Conventional Commits (
commit-lint.ymlenforces this on every PR intomain,develop, orrelease/**). -
Open a PR back into
develop. After QA, merge with--no-ff— Git Flow preserves the branch’s history as a distinct merge rather than folding it into a single fast-forward, which is what makesgit log --first-parent developreadable as a list of merged features/fixes.
New-release workflow
develop --(cut)--> release/X.Y.Z --(QA)--> main --(tag)--> vX.Y.Z --(tag)--> develop
-
From
develop, with no uncommitted changes anddevelopup to date withorigin/develop:./misc/release.sh cut [patch|minor|major]. This createsrelease/X.Y.Zfromdevelop(X.Y.Z= the currentVERSION), resetsBUILDto0on the new branch, then advancesdevelop’sVERSIONto the nextpatch/minor/major(defaultpatch) and resets itsBUILDto0so development for the following release continues immediately. -
QA deploys
release/X.Y.Zto a test server by hand (bootstrap.sh --branch release/X.Y.Z) and verifies it manually — there is no automated build or CI pipeline for this repository’s QA process; it is a person running the installer and checking the result. Fixes found this way land as further commits onrelease/X.Y.Z. Before each re-deploy for another round of manual testing, run./misc/release.sh build— see The build counter and manual QA below — so the redeployed instance reports a version distinct from the previous test pass. -
Once QA signs off: from
release/X.Y.Z, run./misc/release.sh tag --qa-approved. This: generatesCHANGELOG.md; dry-runs the merge back intodevelopand stops, before anything is pushed, if it would conflict where no rule applies (see Merging back into develop below); pushesrelease/X.Y.Z; mergesrelease/X.Y.Zintomainwith--no-ff; tagsvX.Y.Zonmain; pushesmainand the tag; creates the GitHub Release; best-effort triggers the PDF docs workflow; mergesrelease/X.Y.Zback intodevelopwith--no-ff(bringing any stabilization fixes back, whiledevelopkeeps its ownVERSION,BUILDanddocs/antora.yml); and creates (or updates, if it already exists)release/X.Y.xfrom the newvX.Y.Ztag and moves it to the next patch version — see Which ref carries which docs version below. No branch is deleted:release/X.Y.Zpublishes theX.Y.Zdocs from now on, andrelease/X.Y.xis the line.
Merging back into develop
The merge back runs after main and the tag are pushed, so tag makes sure it
cannot stop there. A dry run (git merge-tree) checks it before the first push
and again right before main is pushed; a conflict no rule covers stops the
release with nothing pushed. The rules:
| Conflict in | Resolution |
|---|---|
|
|
|
|
|
|
|
the release’s side — the fragment text that shipped. Listed in the output: a develop-side rewrite meant for the next release must be re-applied by hand |
|
regenerated from the |
Anything else — a source file both lines changed, CHANGELOG.md after an LTS
forward-port — stops tag before the push with the list of paths. Make the two
branches agree on them (cherry-pick develop’s change onto release/X.Y.Z, or
bring release/X.Y.Z into develop through a pull request) and re-run tag.
LTS lines (release/X.Y.x)
release/X.Y.x is created as a side effect of step 3 above, not as a separate
manual step. It is the long-lived line for that major.minor: QA installs it
with --branch release/X.Y.x (the full branch name — X.Y.x on its own is not
a ref and the installer will 404 on it), a customer’s stand is pinned to it, and
every future patch on that line is tagged from it. It is never deleted, and
neither is the release/X.Y.Z branch that produced it — that one publishes the
X.Y.Z docs.
The build counter and manual QA (BUILD / -b0.0.N)
BUILD is a plain integer counting rebuilds of one semantic version. It is
reset to 0 whenever VERSION changes (by cut on both branches, and by the
LTS tag path) and incremented by one command:
./misc/release.sh build # increments BUILD, commits, pushes the branch
./misc/release.sh build --no-push # local only
It runs on release/X.Y.Z or release/X.Y.x — the branches QA installs from —
refuses a dirty tree, and never touches VERSION: a build counter is not a
substitute for a patch bump (1.6.9 → 1.6.10, never 1.6.9-b0.0.1).
This repository has no CI/build pipeline of any kind for QA — QA is a
person running bootstrap.sh --branch release/X.Y.Z (or a manual re-install)
on a test server and checking the result by hand. Nothing can increment the
counter automatically, so it is a manual step: run it yourself whenever a fix
lands on the release branch and you’re about to redeploy for another round of
testing. Skipping it means two different manual test deploys of the same
VERSION both report the identical 1.7.0-b0.0.0 at /version and in the
bootstrap banner, which is the exact ambiguity the counter exists to remove.
Old-version hotfix workflow (an LTS line that isn’t the latest)
-
Branch
hotfix/<DEV-xxxx>-<slug>from the affectedrelease/X.Y.x. -
Fix, and merge
--no-ffback intorelease/X.Y.x. -
From
release/X.Y.x, run./misc/release.sh tag --qa-approved [--version X.Y.Z]. On an LTS branch this bumpsVERSIONto the next patch on that line (or to--versionif given — it must be on the sameX.Yline), resetsBUILD, updatesdocs/antora.yml’sversion:if the file exists, generates and pushesCHANGELOG.md, tagsvX.Y.Z, pushesrelease/X.Y.Zat the tag (the branch the portal publishes the patch’s docs from), pushes the tag, and creates the GitHub Release. It refuses whenrelease/X.Y.Zalready exists elsewhere — a release of the same version cut fromdevelopis in flight.--versionis only meaningful on this LTS path; on the new-release path the version always comes fromVERSIONon the branch. -
The fix is not propagated automatically. The LTS
tagpath only tags and releases; it warns afterward that the fix commit(s) still need to reachmainanddevelop, normally by cherry-pick (a straight merge of an old LTS branch upward would drag stale code onto newer lines).branch-sync-gate.ymlenforces this: on a push torelease/**, it checks that the branch’s new commits are also reachable frommainanddevelop, and opens forward-port PRs for whatever is missing.Expect this gate to be red on every real LTS patch, by construction: the LTS
tagpath pushesrelease/X.Y.xfirst and the forward-propagation tomain/developis a manual step that happens afterwards, so at the moment the gate runs the fix genuinely is missing upstream. That is the gate doing its job, not a false alarm. Anyone making it a required status check needs to know that: the resolution is to merge the forward-port PRs it opens (or land the cherry-picks yourself), after which a re-run goes green — it is not a reason to relax the check.
Latest-version hotfix workflow
If the fix is needed on the currently-released, still-latest version (no newer
release/X.Y.Z cut yet in the way), branch hotfix/<DEV-xxxx>-<slug> from
main instead of from an LTS line, fix, and merge --no-ff into main.
Finalizing this path is manual — there is no release.sh subcommand for it.
./misc/release.sh tag dispatches only on release/X.Y.Z and release/X.Y.x;
run from main it refuses outright with tag must run from a release/X.Y.Z or release/X.Y.x branch (currently: main). Do not reach for it here. The sequence
below is the human equivalent of what the LTS tag path does internally. Run
it from main, with the hooks installed (./misc/install-hooks.sh) so the
pre-commit hook refreshes docs/antora.yml and the generated .adoc pages and
runs misc/check-versions.sh for you:
# 1. Pick the next patch number NOT already tagged AND not in flight as a
#
release/X.Y.Z cut from develop — both must come back empty:
#
git tag -l 'v1.7.*'
#
git ls-remote origin 'refs/heads/release/1.7.*'
NEW_VERSION=1.7.1
# 2. Bump the version identities the LTS path bumps. REPO_BRANCH stays/becomes
#
`main` on this path: you are on main, and main is what the hub publishes.
echo "$NEW_VERSION" > VERSION
echo "0" > BUILD
sed -i.bak 's|^REPO_BRANCH=".*"|REPO_BRANCH="main"|' misc/hub/install.sh
rm -f misc/hub/install.sh.bak
(. misc/lib/image-pins.sh && set_own_image_pins prometheus-grafana-stack "$NEW_VERSION")
git add VERSION BUILD misc/hub/install.sh prometheus-grafana-stack/.env.example prometheus-grafana-stack/docker-compose.yml
git commit -m "chore(version): bump to $NEW_VERSION for hotfix"
# 3. Generate and commit the CHANGELOG entry; keep its text for the Release.
RELEASE_NOTES="$(misc/release-notes/generate.sh "$NEW_VERSION")"
# 4. Push, wait until both own images are built under $NEW_VERSION (main's
#
push starts them; each must end "completed success"), then tag and
#
publish. release/X.Y.Z goes up BEFORE the tag: the docs portal publishes
#
a version only from that branch, and changelog-check.yml fails the tag
#
push when it is missing.
git push origin main
gh run list --workflow alertmanager-config-ui.yml --commit "$(git rev-parse HEAD)" --limit 1
gh run list --workflow snmp-catcher.yml --commit "$(git rev-parse HEAD)" --limit 1
git tag -a "v$NEW_VERSION" -m "$NEW_VERSION"
git push origin "v$NEW_VERSION^{commit}:refs/heads/release/$NEW_VERSION"
git push origin "v$NEW_VERSION"
gh release create "v$NEW_VERSION" --title "v$NEW_VERSION" \
--notes "$RELEASE_NOTES" --target main
Five things about that sequence are easy to get wrong:
-
The patch number is not simply
VERSION+ 1. The matchingrelease/X.Y.xLTS line may already have tagged patches thatmainnever saw (they are not reachable frommain, soVERSIONonmaindoes not know about them). Checkgit tag -l 'vX.Y.*'and take the next free number, orgit pushrejects the tag as already existing. -
misc/hub/install.sh’sREPO_BRANCHmust name a branch that exists, ormisc/check-versions.shfails the pre-commit hook and step 2’s commit is refused. Onmainthat value ismain; a bareX.Y.Zis only tolerated while a legacy branch of that name still exists, and Git Flow does not create one. Skip thesedif it already saysmain. -
The generated docs —
docs/antora.ymland the Antora pages underdocs/modules/ROOT/pages/— are refreshed by the pre-commit hook itself onceVERSIONis staged; do not edit them by hand. Without the hooks installed, run./misc/md2adoc.shyourself and include the result in the same commit. -
Push
mainbefore the tag, becausegh release create --target mainresolvesmainserver-side: run against an unpushedmainit attaches the release to whatever commit GitHub still has, which is the release before this one. (changelog-check.ymlis not the reason — it triggers on the tag push and checks the tag out directly, so it is indifferent tomain’s state.) -
Pushing straight to
mainneeds the access to do it. This procedure is the one documented exception to the no-direct-pushes rule below, and nothing automates it. On amainwith branch protection actually enforced you need push permission or a protection bypass; without either, stop here and take the version-bump and CHANGELOG commits through a PR, then tag the merge commit.
Then propagate, which nothing here automates:
-
Merge the fix into
developas well, so it isn’t lost on the nextcut. -
If a
release/X.Y.xline already exists for this version and is still supported, bring the fix onto it too, or every install pinned to that line ships without it. Cherry-pick the fix commit — that line normally carries its own patch tags and its ownchore(version)bumps, so it has diverged frommainand a merge would dragmain’s version bump onto it. This is the same rule the old-version hotfix section states in the other direction: fixes move between lines by cherry-pick, not by merging one release line into another.
Branch protection
Configured on GitHub, 2026-09-19 — this used to describe an intended policy that GitHub was not actually enforcing (
main/develophad no branch protection at all; verified via the API, which returned “Branch not protected” for both). It is now real: classic branch protection onmainanddevelop, plus arelease-branchesruleset (patternrelease/**, since classic protection only attaches to one existing branch name and futurerelease/X.Y.Z/release/X.Y.xbranches need the rule the moment they’re created).
-
main— no direct pushes; only fast-forward/--no-ffmerges performed bymisc/release.sh tag, plus PRs forhotfix/. *What “performed bymisc/release.sh tag” actually requires: that path mergesrelease/X.Y.Zintomainlocally and then runsgit push origin main— a direct push, from whoever’s account is running the release. The release operator’s account needs repo Admin permission —enforce_admins: falseexempts admins from the “require a pull request” rule, which is the only way a directgit pushcan land on a protected branch. Anyone without that permission runningrelease.sh tagfails at that push with thevX.Y.Ztag already created locally; recovery is a re-run once they have it (the path is resumable — it skips the tag and the GitHub Release if they already exist). One further documented exception: the manual finalization in Latest-version hotfix workflow above pushes the version-bump and CHANGELOG commits tomaindirectly, and needs the same Admin permission. Required status check:commit-lint(Conventional Commits on the PR’s commits).changelog-checkruns on the resultingvX.Y.Ztag push, not on the PR.docs-pdf.yml’s bot-driven version-bump-and-push step cannot bypass this (it runs asgithub-actions[bot]/GITHUB_TOKEN, not an Admin) — see the note below; this is deliberate, not a gap to fix. -
develop— no direct pushes except the version-bump commitsmisc/release.sh cut/tagpush as part of their own automation (same Admin requirement asmain); all other changes land viafeature//bugfix/PRs withcommit-lintrequired. -
release/* (bothrelease/X.Y.Zandrelease/X.Y.x, via therelease-branchesruleset) — direct pushes restricted to release automation and hotfix merges (same Admin-role bypass);commit-lintandbranch-sync-gaterequired on PRs into arelease/**branch.
docs-pdf.yml’s dormant version-bump path. This workflow predates
DEV-3005 and still has a workflow_dispatch input, bump_type, that — if
someone manually triggers the workflow and sets it to patch/minor/major
— bumps VERSION and pushes the commit straight to main as
github-actions[bot]. On a push trigger (every ordinary push to main) and
on release.sh tag’s own gh workflow run docs-pdf.yml -f force_all=true
call, bump_type is never set and defaults to none, so this path does not
run in practice — Git Flow’s release.sh is the only thing that bumps
VERSION today. Branch protection now hard-blocks it if anyone ever does
trigger it by hand (the bot isn’t an Admin and isn’t on any bypass list),
which is the correct outcome: a second, uncoordinated way to move VERSION
on main would silently conflict with release.sh’s own versioning. The
bump_type input itself is legacy and could be removed from the workflow,
but that is a separate cleanup, not part of this section.
Conventional Commits, changelog and build metadata (DEV-3005)
-
Conventional Commits are enforced by
.github/workflows/commit-lint.ymlon every PR intomain,develop, orrelease/**, usingmisc/lib/conventional-commits.sh. Merge/fixup/squash commits are exempted. -
CHANGELOG.mdis generated bymisc/release-notes/generate.sh, invoked bymisc/release.sh tagon both the new-release and LTS paths..github/workflows/changelog-check.ymlfails avX.Y.Ztag push ifCHANGELOG.mdhas no matching## vX.Y.Zentry — the signal that the tag was pushed by hand instead of throughmisc/release.sh tag. -
Changelog fragments — a
.changelogs/<slug>.ymlfile withtitle:anddescription:keys, for a notice a commit subject alone would misrepresent (a security advisory, an operator action).generate.shfolds every pending fragment into the entry and moves it to.changelogs/archive/, staging the move on both sides so it is consumed exactly once. It requiresyq, and refuses to run if fragments are present without it rather than dropping them silently. -
The one-time tag transition. Production tags are
vX.Y.Z, but every tag in this repository’s history is a legacy bareX.Y.Z(1.6.9,1.6.8, …). For the first Git-Flow release there is therefore nov*tag to bound the commit range, andgenerate.shfalls back to the newest legacyX.Y.Ztag reachable fromHEAD, announcing that it did so. Every release after the first is bounded by a realvX.Y.Z. If neither shape exists — a genuinely fresh repository —generate.shrefuses instead of emitting the entire history, and names the two escape hatches:--since <ref>to bound it explicitly, or--allow-full-historyto accept the full range on purpose. -
VERSION/BUILDfeedmisc/version-string.sh, which prints the build-qualified runtime stringX.Y.Z-b0.0.N(N= theBUILDcounter for thatVERSION). This string is what a running deployment reports for itself; it is never the production release identity, which is always thevX.Y.Ztag created bytag --qa-approved.
Changelog pages and release.json (what the CI/CD Manager reads)
misc/release-notes/generate.sh writes these from the same parse as the
CHANGELOG.md entry (misc/lib/changelog-adoc.sh), and stages them in the same
docs(release) commit, so the vX.Y.Z tag carries them:
| File | Content |
|---|---|
|
The release body. Lines 1-3 are fixed: |
|
The release page, linking |
|
Every release, newest first, with its date. |
|
The changelog menu, newest first. It is the second file under |
|
What the release produced (schema 1): version, build |
The index and the menu are regenerated from the changelog/X.Y.Z/ directories
on every run, sorted by version number (1.10.0 above 1.9.0). The menu is a
separate file because misc/md2adoc.sh rewrites nav.adoc on every docs commit
and would delete anything added to it. Do not edit any of these files by hand.
release.json lists the GitHub Release, the three hub PDFs, the
snmp-catcher and alertmanager-config-ui images, and the source archive of
release/X.Y.x. The image tags are the defaults in
prometheus-grafana-stack/docker-compose.yml at the release commit, which is
what an installation pulls. The archive name is the one
misc/publish-repo-archives.sh publishes (release_X.Y.x.tar.gz), and it moves
with later patches on that line. previousTag is the lower bound of the
CHANGELOG.md range.
The CI/CD Manager does not cut grafana releases. It reads the vX.Y.Z tag,
links external.adoc on the docs portal
(<portal>/grafana-monitoring/X.Y.Z/changelog/X.Y.Z/external), and records
release.json in Version Control. A tag without release.json cannot be
recorded there.
Re-running is safe. When CHANGELOG.md already has the entry, generate.sh
reprints it and writes nothing. A committed external.adoc is never
overwritten. If the page writer fails, CHANGELOG.md and the pending
.changelogs/ fragments are left as they were, so a plain re-run does the
whole release again.
Which ref carries which docs version
The docs portal (docs-portal antora-playbook.yml) publishes grafana from the
branches release/ minus release/.x, plus the legacy bare 1.6.x refs. It
does not publish v* tags. Two refs that claim the same component version
stop the whole portal build, so the rule is: version X.Y.Z is published
from the branch release/X.Y.Z, and from nothing else. The branch name is the
version — tag refuses a VERSION that does not match its branch — so a second
published ref with the same version cannot exist.
-
During QA,
release/X.Y.ZpublishesX.Y.Zas a pre-release (prerelease: -dev). The release commit removesprerelease:, so aftertagthe same branch publishesX.Y.Zas released. It is never deleted. -
An LTS patch
vX.Y.Zpushes its ownrelease/X.Y.Zat the tag. -
release/X.Y.x(LTS lines) anddevelopcarry the NEXT version withprerelease: -devand are not published:-
develophas the next version fromcut; the merge back keeps it. -
release/X.Y.xgoes toX.Y.(Z+1),BUILD0, before it is pushed; after an LTS patchvX.Y.(Z+1)it goes toX.Y.(Z+2).
-
-
cutrefuses a version that already has itsrelease/X.Y.Zor its tag; movedevelopto the next free version first. -
Every step skips itself when it is already done. A re-run after the last bump changes nothing and does not release the next patch.
Fixing released docs (a postfix)
A documentation mistake found after vX.Y.Z is fixed by a docs-only commit on
release/X.Y.Z (through a pull request — the release/** ruleset applies). The
portal republishes it; no tag is needed. Cherry-pick the same commit to
develop, and to release/X.Y.x while that line continues, so the next release
does not reintroduce the mistake. tag on a released branch with such a commit
refuses — a code fix is an LTS patch, not a re-tag.
Documentation
-
Every released version publishes its docs from its
release/X.Y.Zbranch (Antora,docs-friendly-tech.pages.dev/grafana-monitoring/<version>/); see Which ref carries which docs version above.release/X.Y.xlines anddevelopare not published. -
Feature branches that add or change docs get a docs build (preview) so the new content is reviewable before merge.
-
VERSIONanddocs/antora.ymlare kept in sync per branch (pre-commit version check); on the LTStagpath,misc/release.shupdatesdocs/antora.yml’sversion:itself.
Interactive installer gate
The install command in our own docs is curl … | sudo bash -s, and the
installer draws a menu over it. That path has broken in ways no unit test saw:
a black screen on MobaXterm under sudo, and a run that hung forever waiting
for a keypress on a /dev/tty nobody was watching.
Before tagging a release, work through
interactive-release-checklist.md. It has
four parts and all four are required:
-
The automated Phase 1 suite —
bash misc/testing/interactive/run-phase1.sh, mirrored in CI by theinteractivejob in.github/workflows/sandbox-tests.yml. Compare against the absolute counts recorded in the checklist, not only againstFAILED STAGES: 0: every gate asserts “at or above a floor”, which a suite that quietly shrank still clears. -
Mutation coverage —
bash misc/testing/scripts/check-red-verify.shplus a scopedred-verify.shsweep. A suite where every test passes proves nothing until the tests have been watched to fail against the defects they guard; that is the whole reason this phase exists. -
A manual client matrix — OpenSSH, Windows Terminal, MobaXterm and PuTTY / a web console. The automated suites run on terminals we control and cannot speak for the clients customers actually use.
-
Acknowledgement of the known open defect: Ctrl-C at the config menu does not restore the terminal. Phase 1 reproduces it and neither state fails CI, so a release must not be signed off assuming it is fixed.
Three prerequisites are easy to miss and silent when absent, so section 0 of the
checklist covers them: the pre-commit gate does not exist until
bash misc/install-hooks.sh --yes has been run (once per clone — in a worktree
the hook lives in the shared git directory and covers every worktree); tmux
must be installed on every runner carrying the ft-ci label, because the
interactive job’s rendering suite refuses to skip under CI; and a Docker
daemon must be running on the machine you sign off from, unless promtool is
installed locally. Without either, validate-all’s Test 41 fails with
failed to connect to the docker API, and step 2’s red-verify.sh sweeps abort
with exit 65 and the baseline run is not a usable reference — a red baseline,
not a broken gate.
The agent-facing version of the same rule — which test to update when you touch
TTY detection, /dev/tty, screen/cursor handling or the sourced UI — is in
AGENTS.md at the repository root.
Releasing
misc/release.sh (DEV-3005 Task 4) implements this document’s develop /
release/X.Y.Z / release/X.Y.x model with two subcommands:
./misc/release.sh cut [patch|minor|major] # from develop: cut release/X.Y.Z
./misc/release.sh build [--no-push] # from release/X.Y.Z or release/X.Y.x: BUILD += 1
./misc/release.sh tag --qa-approved [--version X.Y.Z] # from release/X.Y.Z or release/X.Y.x
cut creates the stabilization branch and advances develop to the next
version (bump type defaults to patch). tag finalizes a release: run on
release/X.Y.Z for a new production release, or on release/X.Y.x for the
next LTS patch. It refuses to run the merge/tag/push sequence without
--qa-approved — QA sign-off itself is a manual deploy-and-verify pass on the
stabilization/hotfix branch (see The build counter and manual QA above);
--qa-approved is only the gate that checks a human has actually done that
before production tagging runs.
misc/release.sh deletes no branch. release/X.Y.x is what QA installs
with --branch, what a customer’s stand is pinned to, and what a future patch
tags again; release/X.Y.Z is where the docs portal publishes X.Y.Z from.
The former --delete-temp-branch option is refused with that explanation.
Requirements
-
No uncommitted changes.
-
GitHub CLI (
gh) installed and authenticated (checked bytagbefore any mutation runs).
Status of the DEV-3005 tooling
misc/release.sh, commit-lint.yml, branch-sync-gate.yml and
changelog-check.yml implement the model above as described in this document —
there is no separate “to do” list left for them. Two gaps remain, called out
where they apply above rather than repeated here: the LTS tag path does not
push the propagated fix to main/develop itself (only warns and lets
branch-sync-gate.yml catch a missed one), and a per-feature-branch docs
preview build (mentioned under Documentation) is not yet implemented.
Bootstrap commands
-
… | bash -s — <ip>— latest release frommain -
… | bash -s — --branch <ref> <ip>— a specific branch. The default comes frommisc/hub/install.sh’sREPO_BRANCH(see Files touched bymisc/release.sh`below for what each path writes there). It must be a branch, not a tag: bootstrap checks it out as `origin/<ref>. Today that meansmain, arelease/X.Y.xLTS line, or a surviving legacyX.Y.Zbranch. -
… | bash -s — --version-list— show all versions
Our own images
alertmanager-config-ui and snmp-catcher are built by this repository’s CI
(.github/workflows/<name>.yml), not mirrored. What an installed host runs is
the tag in prometheus-grafana-stack/.env.example — every stack run brings
the host’s .env back to that set (sync_image_versions). Until DEV-3178
nothing tied that tag to a release: it said 1.6.9 on develop and on
release/1.7.0, so no Config UI or Trap Catcher change after 1.6.9 reached an
installed host.
-
Release lines pin the release.
cutpinsrelease/X.Y.ZtoX.Y.Z, the LTStagpins each patch, andmisc/check-versions.sh(pre-commit) refuses arelease/X.Y.Zwhose pins or compose defaults differ fromVERSION. Arelease/X.Y.xline keeps its last release’s pins while the next patch is developed (anyX.Y.*, the same in both places). On any other branch the two places only have to agree with each other; develop’s value is whatever the last release merged back. -
CI publishes the version tag from release lines and
mainonly. Every build is tagged<branch>and<branch>-<sha>; onrelease/**andmainit is also tagged withVERSION. develop never gets a version tag: itsVERSIONis the next version and can equal an LTS patch number. The workflows build on a change toVERSIONor.env.example, so the commit that moves a pin builds the image it names. -
A release does not ship pins to missing images. A tag the registry does not have fails compose’s pull and aborts
up -dfor every service, and an image build can fail on its own (Trivy).tagtherefore stops until both builds for the release succeeded — when there is none, or the last one failed, it starts one for the branch head and waits for it. To retry by hand:gh run list --workflow alertmanager-config-ui.yml --branch <branch>, thengh workflow run alertmanager-config-ui.yml --ref <branch>(same forsnmp-catcher.yml). -
Feature, bugfix and develop deploys run their own build. On any branch other than
mainandrelease/*, the installer (ensure_mirror_pins→follow_branch_image_pins) points each own image at the branch’s tag when the registry has it, and otherwise keeps the pin. QA of a branch therefore tests that branch’s Config UI and Trap Catcher without editing.env. An air-gapped host, which cannot reach the registry, keeps the pin.
Files touched by misc/release.sh
cut [patch|minor|major]:
-
On the new
release/X.Y.Zbranch:BUILDreset to0,misc/hub/install.sh’`s `REPO_BRANCHset tomain, and our own images (ALERTMANAGER_CONFIG_UI_VERSION,SNMP_CATCHER_VERSIONinprometheus-grafana-stack/.env.exampleand their compose defaults) pinned toX.Y.Z— see Our own images. After the pushcutstarts both image builds on the new branch itself (gh workflow run, socutneeds an authenticatedghtoo): the push that creates a branch does not pass the workflows’ path filter and would build nothing. -
On
develop:VERSIONadvanced to the nextpatch/minor/major,BUILDreset to0,REPO_BRANCHset tomain. -
Both commits are made before either branch is pushed, so a failure in either one leaves nothing published and the retry is two plain pushes.
build [--no-push]:
-
BUILDincremented by one and committed on the current release branch (VERSIONis never touched).
tag --qa-approved on release/X.Y.Z (new-release path):
-
CHANGELOG.md— a## vX.Y.Zentry, generated bymisc/release-notes/generate.sh, committed onrelease/X.Y.Z. The commit range is bounded by the newestvX.Y.Ztag onmain(passed as--since), not by a tag reachable from the release branch: Git Flow puts the tag onmain’s merge commit, which never reachesdevelop. -
the
vX.Y.Zgit tag, onmain. -
a GitHub Release for
vX.Y.Z. -
docs/antora.yml— theprerelease:line is removed in a commit before the changelog commit, so the tag carries a released version. -
the changelog pages,
changelog-nav.adocandrelease.json(see Changelog pages and release.json above), in the changelog commit. -
develop— gets the release branch merged back, but keeps its ownVERSION,BUILDanddocs/antora.yml. -
release/X.Y.x— created (or updated) from thevX.Y.Ztag, then moved toVERSIONX.Y.(Z+1),BUILD0,docs/antora.ymlX.Y.(Z+1)withprerelease: -dev, andREPO_BRANCHrelease/X.Y.x. Its own-image pins stay atX.Y.Zwhile the next patch is developed: installs from the LTS line run released images, not development builds (check-versionsaccepts anyX.Y.*there, and the LTStagmoves them to the patch). -
before anything reaches
main, the own-image pins are checked: a branch cut before the pins existed gets achore(version): pin own images to X.Y.Zcommit (pushed, built), andtagstops unless both image builds for the branch succeeded — starting them itself when there are none.
tag --qa-approved on release/X.Y.x (LTS-patch path):
-
VERSION— the next patch on that line (or--versionif given). The line normally has this value already;BUILDis reset to0only whenVERSIONchanges. -
docs/antora.yml—version: 'X.Y.Z'and noprerelease:line, if the file exists. -
CHANGELOG.md— a## vX.Y.Zentry, generated the same way, with the changelog pages andrelease.json. -
the own-image pins — set to the new
X.Y.Zin the samechore(version)commit (whichbranch-sync-gatedoes not forward-port). After the push,tagstarts both image builds for that commit and waits for them (up to 30 minutes); it stops if either fails or never starts. -
the
vX.Y.Zgit tag, onrelease/X.Y.x. -
a GitHub Release for
vX.Y.Z. -
then
release/X.Y.xagain:VERSIONX.Y.(Z+1),BUILD0,docs/antora.ymlX.Y.(Z+1)withprerelease: -dev, pushed.
misc/hub/install.sh’s REPO_BRANCH — the default install ref a customer
gets from the public one-liner — is rewritten by both paths, via
release.sh’s sync_hub_repo_branch helper:
| Where | Value written | Why |
|---|---|---|
|
|
The stabilization branch has no LTS line of its own yet — |
|
|
|
|
|
An installer shipped on an LTS line defaults to its own line, and that branch exists. |
The value must always name a branch that actually exists, because it is used
as a live git ref in three places: the hub installer’s
…/contents/misc/bootstrap.sh?ref=<value> GitHub API call, and misc/bootstrap.sh’s
git fetch origin "<value>" and git checkout -f -B "<value>" "origin/<value>".
A tag is not acceptable — the last of those needs a remote-tracking branch.
misc/check-versions.sh asserts both the identity and that the ref resolves, and
the pre-commit hook runs it. Before this was enforced, sync_hub_repo_branch
wrote a bare X.Y.Z, which Git Flow never creates as a branch; it only worked at
all because a legacy branch of that name survived from the pre-Git-Flow model.
Neither cut nor tag rewrites README.md’s documented install commands —
those are edited by hand when the install-facing story actually changes, same
as any other prose. What no longer needs manual upkeep is checking them.
Install refs after the Git Flow transition
misc/testing/scripts/check-doc-commands.sh used to require every documented
install command to pass --branch $VERSION literally — a rule that matched
the old version-branch model, where VERSION genuinely was the branch every
commit lived on, and broke the moment develop existed: develop’s VERSION
is a moving target with no installable branch of its own.
The check now verifies every documented --branch <ref> against origin
directly (git ls-remote, not the local checkout — CI’s clone is shallow and
single-ref), so a documented ref is correct as long as it actually exists,
whatever it names: main, a specific release/X.Y.x line, or a vX.Y.Z tag.
A command with no --branch at all is correct too — the installer’s own
default (misc/hub/install.sh, misc/bootstrap.sh) is main.
README.md documents that default (no --branch → main) as the primary
path, and --branch release/<major>.<minor>.x as the pattern for pinning to a
specific supported line, instead of a version number that stops being a real
ref the moment cut advances develop past it. No further reconciliation
between the two is expected as lines are cut, tagged, and eventually retired.