Release Workflow

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.sh implements the cut/tag flow described below. The branching model itself is sourced from docs-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 files misc/release.sh touches.

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

main

—

Production-ready code only. Every production tag (vX.Y.Z) points at a commit on main.

Permanent

develop

main (once)

Permanent integration branch for the next release. All feature/ and bugfix/ work merges here first.

Permanent

feature/*

develop

New functionality for the next release.

Deleted after merge

bugfix/*

develop

Non-emergency fixes for the next release.

Deleted after merge

release/X.Y.Z

develop

Stabilization branch for one release: bug fixes and release-notes/version bumps only, no new features. After tag it is the branch the docs portal publishes X.Y.Z from; a docs-only fix after the release (a postfix) is committed here.

Kept for good — deleting it unpublishes that version’s docs

release/X.Y.x

created from the vX.Y.Z tag when a release is finalized

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

hotfix/*

main (latest line) or an LTS release/X.Y.x (older line)

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 by misc/release.sh tag --qa-approved, once, on main (new release) or on the relevant release/X.Y.x (LTS patch). changelog-check.yml requires a matching ## vX.Y.Z entry in CHANGELOG.md for every vX.Y.Z tag 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 before tag --qa-approved runs, tag it manually following that pattern so it never collides with a production vX.Y.Z tag.

  • X.Y.Z-b0.0.N (from misc/version-string.sh) is not a release tag at all — it is the build/runtime version string a running deployment reports, where N is the BUILD counter for the current VERSION. It identifies a specific build of a version, not a production release; the production release identity is always the vX.Y.Z tag, never this string.

Feature and bugfix workflow

  1. Branch feature/<DEV-xxxx>-<slug> or bugfix/<DEV-xxxx>-<slug> from develop.

  2. Commit using Conventional Commits (commit-lint.yml enforces this on every PR into main, develop, or release/**).

  3. 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 makes git log --first-parent develop readable as a list of merged features/fixes.

New-release workflow

develop --(cut)--> release/X.Y.Z --(QA)--> main --(tag)--> vX.Y.Z --(tag)--> develop
  1. From develop, with no uncommitted changes and develop up to date with origin/develop: ./misc/release.sh cut [patch|minor|major]. This creates release/X.Y.Z from develop (X.Y.Z = the current VERSION), resets BUILD to 0 on the new branch, then advances develop’s VERSION to the next patch/minor/major (default patch) and resets its BUILD to 0 so development for the following release continues immediately.

  2. QA deploys release/X.Y.Z to 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 on release/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.

  3. Once QA signs off: from release/X.Y.Z, run ./misc/release.sh tag --qa-approved. This: generates CHANGELOG.md; dry-runs the merge back into develop and stops, before anything is pushed, if it would conflict where no rule applies (see Merging back into develop below); pushes release/X.Y.Z; merges release/X.Y.Z into main with --no-ff; tags vX.Y.Z on main; pushes main and the tag; creates the GitHub Release; best-effort triggers the PDF docs workflow; merges release/X.Y.Z back into develop with --no-ff (bringing any stabilization fixes back, while develop keeps its own VERSION, BUILD and docs/antora.yml); and creates (or updates, if it already exists) release/X.Y.x from the new vX.Y.Z tag and moves it to the next patch version — see Which ref carries which docs version below. No branch is deleted: release/X.Y.Z publishes the X.Y.Z docs from now on, and release/X.Y.x is 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

VERSION, BUILD, docs/antora.yml

develop’s side — it carries the next version

docs/modules/ROOT/pages/*.adoc, docs/modules/ROOT/nav.adoc

develop’s side, then misc/md2adoc.sh regenerates them from the merged sources

misc/hub/install.sh

develop’s side only when the release branch changed nothing in it but REPO_BRANCH; a real fix there is never dropped

.changelogs/.yml, .changelogs/archive/.yml

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

pages/changelog/index.adoc, changelog-nav.adoc

regenerated from the pages/changelog/<version>/ directories

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)

  1. Branch hotfix/<DEV-xxxx>-<slug> from the affected release/X.Y.x.

  2. Fix, and merge --no-ff back into release/X.Y.x.

  3. From release/X.Y.x, run ./misc/release.sh tag --qa-approved [--version X.Y.Z]. On an LTS branch this bumps VERSION to the next patch on that line (or to --version if given — it must be on the same X.Y line), resets BUILD, updates docs/antora.yml’s version: if the file exists, generates and pushes CHANGELOG.md, tags vX.Y.Z, pushes release/X.Y.Z at the tag (the branch the portal publishes the patch’s docs from), pushes the tag, and creates the GitHub Release. It refuses when release/X.Y.Z already exists elsewhere — a release of the same version cut from develop is in flight. --version is only meaningful on this LTS path; on the new-release path the version always comes from VERSION on the branch.

  4. The fix is not propagated automatically. The LTS tag path only tags and releases; it warns afterward that the fix commit(s) still need to reach main and develop, normally by cherry-pick (a straight merge of an old LTS branch upward would drag stale code onto newer lines). branch-sync-gate.yml enforces this: on a push to release/**, it checks that the branch’s new commits are also reachable from main and develop, and opens forward-port PRs for whatever is missing.

    Expect this gate to be red on every real LTS patch, by construction: the LTS tag path pushes release/X.Y.x first and the forward-propagation to main/develop is 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 matching release/X.Y.x LTS line may already have tagged patches that main never saw (they are not reachable from main, so VERSION on main does not know about them). Check git tag -l 'vX.Y.*' and take the next free number, or git push rejects the tag as already existing.

  • misc/hub/install.sh’s REPO_BRANCH must name a branch that exists, or misc/check-versions.sh fails the pre-commit hook and step 2’s commit is refused. On main that value is main; a bare X.Y.Z is only tolerated while a legacy branch of that name still exists, and Git Flow does not create one. Skip the sed if it already says main.

  • The generated docs — docs/antora.yml and the Antora pages under docs/modules/ROOT/pages/ — are refreshed by the pre-commit hook itself once VERSION is staged; do not edit them by hand. Without the hooks installed, run ./misc/md2adoc.sh yourself and include the result in the same commit.

  • Push main before the tag, because gh release create --target main resolves main server-side: run against an unpushed main it attaches the release to whatever commit GitHub still has, which is the release before this one. (changelog-check.yml is not the reason — it triggers on the tag push and checks the tag out directly, so it is indifferent to main’s state.)

  • Pushing straight to main needs the access to do it. This procedure is the one documented exception to the no-direct-pushes rule below, and nothing automates it. On a main with 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 develop as well, so it isn’t lost on the next cut.

  • If a release/X.Y.x line 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 own chore(version) bumps, so it has diverged from main and a merge would drag main’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/develop had no branch protection at all; verified via the API, which returned “Branch not protected” for both). It is now real: classic branch protection on main and develop, plus a release-branches ruleset (pattern release/**, since classic protection only attaches to one existing branch name and future release/X.Y.Z/release/X.Y.x branches need the rule the moment they’re created).

  • main — no direct pushes; only fast-forward/--no-ff merges performed by misc/release.sh tag, plus PRs for hotfix/. *What “performed by misc/release.sh tag” actually requires: that path merges release/X.Y.Z into main locally and then runs git 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: false exempts admins from the “require a pull request” rule, which is the only way a direct git push can land on a protected branch. Anyone without that permission running release.sh tag fails at that push with the vX.Y.Z tag 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 to main directly, and needs the same Admin permission. Required status check: commit-lint (Conventional Commits on the PR’s commits). changelog-check runs on the resulting vX.Y.Z tag push, not on the PR. docs-pdf.yml’s bot-driven version-bump-and-push step cannot bypass this (it runs as github-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 commits misc/release.sh cut/tag push as part of their own automation (same Admin requirement as main); all other changes land via feature//bugfix/ PRs with commit-lint required.

  • release/* (both release/X.Y.Z and release/X.Y.x, via the release-branches ruleset) — direct pushes restricted to release automation and hotfix merges (same Admin-role bypass); commit-lint and branch-sync-gate required on PRs into a release/** 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.yml on every PR into main, develop, or release/**, using misc/lib/conventional-commits.sh. Merge/fixup/squash commits are exempted.

  • CHANGELOG.md is generated by misc/release-notes/generate.sh, invoked by misc/release.sh tag on both the new-release and LTS paths. .github/workflows/changelog-check.yml fails a vX.Y.Z tag push if CHANGELOG.md has no matching ## vX.Y.Z entry — the signal that the tag was pushed by hand instead of through misc/release.sh tag.

  • Changelog fragments — a .changelogs/<slug>.yml file with title: and description: keys, for a notice a commit subject alone would misrepresent (a security advisory, an operator action). generate.sh folds 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 requires yq, 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 bare X.Y.Z (1.6.9, 1.6.8, …). For the first Git-Flow release there is therefore no v* tag to bound the commit range, and generate.sh falls back to the newest legacy X.Y.Z tag reachable from HEAD, announcing that it did so. Every release after the first is bounded by a real vX.Y.Z. If neither shape exists — a genuinely fresh repository — generate.sh refuses instead of emitting the entire history, and names the two escape hatches: --since <ref> to bound it explicitly, or --allow-full-history to accept the full range on purpose.

  • VERSION/BUILD feed misc/version-string.sh, which prints the build-qualified runtime string X.Y.Z-b0.0.N (N = the BUILD counter for that VERSION). This string is what a running deployment reports for itself; it is never the production release identity, which is always the vX.Y.Z tag created by tag --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

docs/modules/ROOT/pages/changelog/X.Y.Z/external.adoc

The release body. Lines 1-3 are fixed: = Grafana Monitoring Stack X.Y.Z - External Changelog, :release-version: X.Y.Z, :icons: font. Breaking changes come first, as a [WARNING] and a == Breaking changes section.

docs/modules/ROOT/pages/changelog/X.Y.Z/index.adoc

The release page, linking external.adoc.

docs/modules/ROOT/pages/changelog/index.adoc

Every release, newest first, with its date.

docs/modules/ROOT/changelog-nav.adoc

The changelog menu, newest first. It is the second file under nav: in docs/antora.yml.

docs/modules/ROOT/attachments/changelog/X.Y.Z/release.json

What the release produced (schema 1): version, build 0.0.<BUILD>, date, previousTag, and the artifacts.

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.Z publishes X.Y.Z as a pre-release (prerelease: -dev). The release commit removes prerelease:, so after tag the same branch publishes X.Y.Z as released. It is never deleted.

  • An LTS patch vX.Y.Z pushes its own release/X.Y.Z at the tag.

  • release/X.Y.x (LTS lines) and develop carry the NEXT version with prerelease: -dev and are not published:

    • develop has the next version from cut; the merge back keeps it.

    • release/X.Y.x goes to X.Y.(Z+1), BUILD 0, before it is pushed; after an LTS patch vX.Y.(Z+1) it goes to X.Y.(Z+2).

  • cut refuses a version that already has its release/X.Y.Z or its tag; move develop to 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.Z branch (Antora, docs-friendly-tech.pages.dev/grafana-monitoring/<version>/); see Which ref carries which docs version above. release/X.Y.x lines and develop are not published.

  • Feature branches that add or change docs get a docs build (preview) so the new content is reviewable before merge.

  • VERSION and docs/antora.yml are kept in sync per branch (pre-commit version check); on the LTS tag path, misc/release.sh updates docs/antora.yml’s version: 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:

  1. The automated Phase 1 suite — bash misc/testing/interactive/run-phase1.sh, mirrored in CI by the interactive job in .github/workflows/sandbox-tests.yml. Compare against the absolute counts recorded in the checklist, not only against FAILED STAGES: 0: every gate asserts “at or above a floor”, which a suite that quietly shrank still clears.

  2. Mutation coverage — bash misc/testing/scripts/check-red-verify.sh plus a scoped red-verify.sh sweep. 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.

  3. 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.

  4. 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 by tag before 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 from main

  • …​ | bash -s — --branch <ref> <ip> — a specific branch. The default comes from misc/hub/install.sh’s REPO_BRANCH (see Files touched by misc/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 means main, a release/X.Y.x LTS line, or a surviving legacy X.Y.Z branch.

  • …​ | bash -s — --version-list — show all versions

--version-list output

Available versions:

  Released:
    1.6.8 (latest)
    1.6.7
    1.6.6
    ...

  In development:
    (current release branch)

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. cut pins release/X.Y.Z to X.Y.Z, the LTS tag pins each patch, and misc/check-versions.sh (pre-commit) refuses a release/X.Y.Z whose pins or compose defaults differ from VERSION. A release/X.Y.x line keeps its last release’s pins while the next patch is developed (any X.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 main only. Every build is tagged <branch> and <branch>-<sha>; on release/** and main it is also tagged with VERSION. develop never gets a version tag: its VERSION is the next version and can equal an LTS patch number. The workflows build on a change to VERSION or .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 -d for every service, and an image build can fail on its own (Trivy). tag therefore 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>, then gh workflow run alertmanager-config-ui.yml --ref <branch> (same for snmp-catcher.yml).

  • Feature, bugfix and develop deploys run their own build. On any branch other than main and release/*, 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.Z branch: BUILD reset to 0, misc/hub/install.sh’`s `REPO_BRANCH set to main, and our own images (ALERTMANAGER_CONFIG_UI_VERSION, SNMP_CATCHER_VERSION in prometheus-grafana-stack/.env.example and their compose defaults) pinned to X.Y.Z — see Our own images. After the push cut starts both image builds on the new branch itself (gh workflow run, so cut needs an authenticated gh too): the push that creates a branch does not pass the workflows’ path filter and would build nothing.

  • On develop: VERSION advanced to the next patch/minor/major, BUILD reset to 0, REPO_BRANCH set to main.

  • 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]:

  • BUILD incremented by one and committed on the current release branch (VERSION is never touched).

tag --qa-approved on release/X.Y.Z (new-release path):

  • CHANGELOG.md — a ## vX.Y.Z entry, generated by misc/release-notes/generate.sh, committed on release/X.Y.Z. The commit range is bounded by the newest vX.Y.Z tag on main (passed as --since), not by a tag reachable from the release branch: Git Flow puts the tag on main’s merge commit, which never reaches develop.

  • the vX.Y.Z git tag, on main.

  • a GitHub Release for vX.Y.Z.

  • docs/antora.yml — the prerelease: line is removed in a commit before the changelog commit, so the tag carries a released version.

  • the changelog pages, changelog-nav.adoc and release.json (see Changelog pages and release.json above), in the changelog commit.

  • develop — gets the release branch merged back, but keeps its own VERSION, BUILD and docs/antora.yml.

  • release/X.Y.x — created (or updated) from the vX.Y.Z tag, then moved to VERSION X.Y.(Z+1), BUILD 0, docs/antora.yml X.Y.(Z+1) with prerelease: -dev, and REPO_BRANCH release/X.Y.x. Its own-image pins stay at X.Y.Z while the next patch is developed: installs from the LTS line run released images, not development builds (check-versions accepts any X.Y.* there, and the LTS tag moves them to the patch).

  • before anything reaches main, the own-image pins are checked: a branch cut before the pins existed gets a chore(version): pin own images to X.Y.Z commit (pushed, built), and tag stops 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 --version if given). The line normally has this value already; BUILD is reset to 0 only when VERSION changes.

  • docs/antora.yml — version: 'X.Y.Z' and no prerelease: line, if the file exists.

  • CHANGELOG.md — a ## vX.Y.Z entry, generated the same way, with the changelog pages and release.json.

  • the own-image pins — set to the new X.Y.Z in the same chore(version) commit (which branch-sync-gate does not forward-port). After the push, tag starts 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.Z git tag, on release/X.Y.x.

  • a GitHub Release for vX.Y.Z.

  • then release/X.Y.x again: VERSION X.Y.(Z+1), BUILD 0, docs/antora.yml X.Y.(Z+1) with prerelease: -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

cut, on the new release/X.Y.Z

main

The stabilization branch has no LTS line of its own yet — release/X.Y.x does not exist until tag creates it.

cut, on develop

main

develop tracks a version that has not been released and has no branch named after it.

tag, on release/X.Y.x (both paths)

release/X.Y.x

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.

Troubleshooting

“Uncommitted changes detected”

Commit or stash first:

git add -A && git commit -m "fix: something"
# or
git stash

“GitHub CLI not authenticated”

gh auth login

branch release/1.7.0 · commit 582e727b3700ec06c4c6e544e853886fb8d272b9 · page generated October 06, 2026