From 793d54700798368c87c762f6bffa89858738c7c1 Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Mon, 27 Jul 2026 18:44:25 -0600 Subject: [PATCH 1/5] Document FOG_VERSION/FOG_CHANNEL sync automation --- docs/development/version-sync-automation.md | 71 +++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/development/version-sync-automation.md diff --git a/docs/development/version-sync-automation.md b/docs/development/version-sync-automation.md new file mode 100644 index 0000000..9e41d7a --- /dev/null +++ b/docs/development/version-sync-automation.md @@ -0,0 +1,71 @@ +--- +title: Version Sync Automation +description: How FOG_VERSION/FOG_CHANNEL are kept in sync across branches +context_id: version-sync-automation +aliases: + - Version Sync Automation + - FOG_VERSION automation +tags: + - development + - release + - automation +--- +# Version Sync Automation + +`FOG_VERSION` and `FOG_CHANNEL` (defined in +`packages/web/lib/fog/system.class.php`) are computed from git state — the +current branch name, distance from the last tag, and commit counts — rather +than being bumped by hand. Three separate mechanisms keep that computed value +in sync with what's actually committed, each covering a different branch and +a different way changes land on it. + +## 1. The local pre-commit hook + +`.githooks/fogproject/pre-commit` is the source of truth for the version +formula. Every local commit on `working-1.6`, `dev-branch`, `rc-*`, and +`feature-*` branches recomputes `FOG_VERSION`/`FOG_CHANNEL` and stages the +change as part of that commit. This is what most contributors experience +day-to-day. + +## 2. The fog-workflows backstop + +The pre-commit hook is client-side — it never runs for a PR merged through +GitHub's web UI (squash, merge-commit, or rebase), so a version merged that +way can silently go stale. This is backstopped across two repos: + +- **[`FOGProject/fog-workflows`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/check-fog-version.yml)** + holds all the actual logic, as a reusable (`workflow_call`) workflow. It + independently recomputes the expected version/channel using the same + formula as the pre-commit hook, compares it against what's actually + committed, and — if they disagree — commits the fix **directly** to the + branch (no PR, since this is the same kind of mechanical correction the + hook already makes without review). +- **`FOGProject/fogproject`**'s own + [`.github/workflows/check-fog-version.yml`](https://github.com/FOGProject/fogproject/blob/dev-branch/.github/workflows/check-fog-version.yml) + is a thin, push-triggered stub that delegates to the fog-workflows + reusable workflow via `uses:`. It only exists because GitHub Actions has + no native cross-repo `push` trigger — some file has to live in `fogproject` + to react to pushes there, but it should rarely need to change once in + place. + +This backstop watches `working-1.6`, `dev-branch`, `rc-*`, and `feature-*` — +the same branches the pre-commit hook covers. **It intentionally does not +watch `stable`** (see below). Because GitHub resolves a push-triggered +workflow from the copy of the file that exists on the branch receiving the +push, the stub has to be present on every branch being watched; new +`feature-*`/`rc-*` branches cut from `dev-branch` inherit it automatically, +but any long-lived branch not descended from `dev-branch` needs it added by +hand. + +## 3. stable-releases.yml + +`stable`'s version is owned entirely by fog-workflows' +[`stable-releases.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/stable-releases.yml), +which drives the whole release flow (validation, tagging, release notes, +syncing `stable` back into `dev-branch`). The version-sync backstop above +deliberately excludes `stable` so the two mechanisms never fight over the +same branch. + +See [Fog Release](fog-release.md) for the manual side of cutting a release +(kernel/init/iPXE updates); this page covers only how the version *string* +itself stays correct. From d2a8bf8a2791189be686b755ed80796b2618f472 Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Mon, 27 Jul 2026 18:44:41 -0600 Subject: [PATCH 2/5] Fix pre-commit hook path --- docs/development/version-sync-automation.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/development/version-sync-automation.md b/docs/development/version-sync-automation.md index 9e41d7a..86db722 100644 --- a/docs/development/version-sync-automation.md +++ b/docs/development/version-sync-automation.md @@ -21,11 +21,10 @@ a different way changes land on it. ## 1. The local pre-commit hook -`.githooks/fogproject/pre-commit` is the source of truth for the version -formula. Every local commit on `working-1.6`, `dev-branch`, `rc-*`, and -`feature-*` branches recomputes `FOG_VERSION`/`FOG_CHANNEL` and stages the -change as part of that commit. This is what most contributors experience -day-to-day. +`.githooks/pre-commit` is the source of truth for the version formula. Every +local commit on `working-1.6`, `dev-branch`, `rc-*`, and `feature-*` branches +recomputes `FOG_VERSION`/`FOG_CHANNEL` and stages the change as part of that +commit. This is what most contributors experience day-to-day. ## 2. The fog-workflows backstop From 6faa8ee49b8ce2c28700804a62097299b71cf08a Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Mon, 27 Jul 2026 18:45:09 -0600 Subject: [PATCH 3/5] Cross-link version sync automation doc --- docs/development/fog-release.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/development/fog-release.md b/docs/development/fog-release.md index 289a8e9..2af1602 100644 --- a/docs/development/fog-release.md +++ b/docs/development/fog-release.md @@ -19,7 +19,9 @@ compatibility, the usual practice is to update them and let them bake on the is cut. The sections below cover updating each of those components and the testing that -validates a release. +validates a release. See [Version Sync Automation](version-sync-automation.md) +for how `FOG_VERSION`/`FOG_CHANNEL` itself is kept in sync separately from +this manual release process. ## Updating dependencies From 2a6cfd3919ae9d1d3b3be17751972ef1a43c6ce1 Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Tue, 28 Jul 2026 11:08:51 -0600 Subject: [PATCH 4/5] Rewrite version-sync-automation.md for the corrected design The previous draft documented yesterday's first attempt (a push-triggered stub in fogproject calling a reusable workflow), which had a live self-triggering commit loop - its gitcount formula counted its own prior fixup commits as drift, and a bot-authored push re-triggered the stub that called it. That mechanism has since been replaced with a single schedule-triggered workflow in fog-workflows that discovers branches itself (no stub file in fogproject to propagate anymore) and a shared .githooks/lib/fog-version.sh script used by both the local hook and CI, with a two-pass compute that anticipates a fixup commit's own +1 instead of recomputing the same wrong answer forever. Documents the corrected design, the FOG_CHANNEL convention (dev-branch/stable carry none, by design), and the incident that prompted the rewrite for future reference. --- docs/development/version-sync-automation.md | 130 ++++++++++++++------ 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/docs/development/version-sync-automation.md b/docs/development/version-sync-automation.md index 86db722..678b106 100644 --- a/docs/development/version-sync-automation.md +++ b/docs/development/version-sync-automation.md @@ -15,55 +15,113 @@ tags: `FOG_VERSION` and `FOG_CHANNEL` (defined in `packages/web/lib/fog/system.class.php`) are computed from git state — the current branch name, distance from the last tag, and commit counts — rather -than being bumped by hand. Three separate mechanisms keep that computed value -in sync with what's actually committed, each covering a different branch and -a different way changes land on it. +than being bumped by hand. See the main repo's README ("Versioning and +branches") for the version format itself +(`{CodeBaseMajor}.{Major}.{Minor}.{Patch}`) and what each branch represents. +This page covers how the version *string* stays correct on each branch +without anyone bumping it by hand. + +## The formula lives in one script + +`.githooks/lib/fog-version.sh` is the single source of truth. Given a branch +name, it recomputes `FOG_VERSION`/`FOG_CHANNEL` and rewrites +`system.class.php` in place. It deliberately does **not** `git add` or +`git commit` anything, and makes no assumption that it's running mid-commit +— that's what lets both mechanisms below share it instead of keeping two +copies of the formula that can silently drift apart (which is exactly what +happened before this was factored out — see *Incident history* below). + +The formula per branch prefix: + +| Branch prefix | Channel | Version scheme | +|---|---|---| +| `dev` (`dev-branch`) | *(none)* | `{tag base}.{commits since master}` | +| `stable` | *(none)* | Same as `dev`, but counted from `dev-branch` instead of `HEAD` | +| `working` (`working-1.6`) | Beta | `{branch suffix}.0-beta.{commits since master}` | +| `feature` (`feature-*`) | Feature | `{branch suffix}.0-feature.{commits since master}` | +| `rc` (`rc-*`) | Release Candidate | `{branch suffix}.0-RC-{n}`, where `n` increments by 1 from whatever's currently committed | + +`dev-branch` and `stable` deliberately carry **no `FOG_CHANNEL` line at all** +— a clean version string with no channel text, by design. This isn't an +oversight; don't add one. `working`/`feature`/`rc` branches do show a channel +label. + +The `rc` case is intentionally different from the others: it's a manual +per-change counter (each real change to a release candidate bumps `-N` by +one), not a commit count. That's a deliberate choice for how release +candidates are numbered, not something to make consistent with the +count-based branches. ## 1. The local pre-commit hook -`.githooks/pre-commit` is the source of truth for the version formula. Every -local commit on `working-1.6`, `dev-branch`, `rc-*`, and `feature-*` branches -recomputes `FOG_VERSION`/`FOG_CHANNEL` and stages the change as part of that -commit. This is what most contributors experience day-to-day. +`.githooks/pre-commit` calls `fog-version.sh` on every local commit to +`working-1.6`, `dev-branch`, `rc-*`, and `feature-*` branches, then stages the +result as part of that same commit. This is what most contributors experience +day-to-day, and it's why a version bump usually rides along silently inside +an otherwise-unrelated commit rather than showing up as its own change. -## 2. The fog-workflows backstop +## 2. The fog-workflows hourly sweep The pre-commit hook is client-side — it never runs for a PR merged through GitHub's web UI (squash, merge-commit, or rebase), so a version merged that -way can silently go stale. This is backstopped across two repos: - -- **[`FOGProject/fog-workflows`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/check-fog-version.yml)** - holds all the actual logic, as a reusable (`workflow_call`) workflow. It - independently recomputes the expected version/channel using the same - formula as the pre-commit hook, compares it against what's actually - committed, and — if they disagree — commits the fix **directly** to the - branch (no PR, since this is the same kind of mechanical correction the - hook already makes without review). -- **`FOGProject/fogproject`**'s own - [`.github/workflows/check-fog-version.yml`](https://github.com/FOGProject/fogproject/blob/dev-branch/.github/workflows/check-fog-version.yml) - is a thin, push-triggered stub that delegates to the fog-workflows - reusable workflow via `uses:`. It only exists because GitHub Actions has - no native cross-repo `push` trigger — some file has to live in `fogproject` - to react to pushes there, but it should rarely need to change once in - place. - -This backstop watches `working-1.6`, `dev-branch`, `rc-*`, and `feature-*` — -the same branches the pre-commit hook covers. **It intentionally does not -watch `stable`** (see below). Because GitHub resolves a push-triggered -workflow from the copy of the file that exists on the branch receiving the -push, the stub has to be present on every branch being watched; new -`feature-*`/`rc-*` branches cut from `dev-branch` inherit it automatically, -but any long-lived branch not descended from `dev-branch` needs it added by -hand. +way can silently go stale. This is backstopped by a single scheduled +workflow: +[`FOGProject/fog-workflows`'s `check-fog-version.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/check-fog-version.yml) +runs hourly (plus `workflow_dispatch` for a one-off or all-branches run). Each +run: + +1. Lists `fogproject`'s branches via the GitHub API and filters to the + watched patterns (`working-1.6`, `dev-branch`, `rc-*`, `feature-*` — the + same set the local hook covers, and **not** `stable`, see below). +2. For each match, checks out that branch and runs its own copy of + `.githooks/lib/fog-version.sh` — the same script the local hook calls, not + a separately-maintained copy of the formula. +3. If the recomputed value disagrees with what's committed, pushes a fixup + commit directly to that branch (no PR — this is the same kind of + mechanical correction the local hook already makes without review). + +There's no stub file living in `fogproject` for this — the workflow discovers +branches itself via the API, so a new `feature-*`/`rc-*` branch is covered +automatically the next time the schedule fires, with nothing to propagate or +forget to add. + +### Why a fixup commit anticipates its own `+1` + +A fixup commit is itself a real commit on the branch, so if it simply wrote +"the value that's correct right now" it would be wrong the instant it lands +— the next check would see the fixup commit itself as one more commit than +what got written, and "fix" it again. `fog-version.sh` avoids this with a +two-pass compute: first with the raw commit count; if that already matches +what's committed, nothing happens. If it doesn't, it recomputes once more +with the count incremented by one — the value that will actually be true +once this fix exists — and writes that instead. This converges in exactly +one commit no matter how large the gap is, and is dynamic (a real recount +every run), not a fixed offset applied forever. + +### Incident history (2026-07-28) + +An earlier version of this workflow didn't anticipate its own commit. It +lived as a push-triggered stub in `fogproject` calling a reusable workflow in +`fog-workflows`, and its commit-count formula counted its own prior fixup +commits as real drift. A bot-authored push re-triggered the push-triggered +stub, which recomputed a value one higher than it had just committed, and +pushed another fixup — 30 commits landed on `dev-branch` in about 20 minutes +before it was caught. Moving to a schedule trigger (instead of push) removed +the immediate retrigger, and the two-pass anticipation above removed the +underlying non-convergence, which would otherwise have just repeated once +per scheduled run instead of once per push. The ~150 leftover fixup commits +from that incident were left in `dev-branch`/`working-1.6` history rather +than rewritten away; `stable-releases.yml`'s changelog generation excludes +them by commit-message pattern so they don't spam release notes, but they're +still there in `git log` for anyone who needs to look. ## 3. stable-releases.yml `stable`'s version is owned entirely by fog-workflows' [`stable-releases.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/stable-releases.yml), which drives the whole release flow (validation, tagging, release notes, -syncing `stable` back into `dev-branch`). The version-sync backstop above -deliberately excludes `stable` so the two mechanisms never fight over the -same branch. +syncing `stable` back into `dev-branch`). The hourly sweep above deliberately +excludes `stable` so the two mechanisms never fight over the same branch. See [Fog Release](fog-release.md) for the manual side of cutting a release (kernel/init/iPXE updates); this page covers only how the version *string* From d689134eebf379470b4daa0adf4fc44350afd262 Mon Sep 17 00:00:00 2001 From: JJ Fullmer Date: Tue, 28 Jul 2026 11:12:26 -0600 Subject: [PATCH 5/5] Document the stable-releases.yml pipeline Adds a new page walking through how a dev-branch release becomes a tagged, published stable release: the PR-based gate on install-validation tests, changelog/issue/security-advisory generation, the patched_versions update on affected advisories, and the sync-back PR into dev-branch. Cross-links with version-sync-automation.md (which covers the version number itself, read but never computed by this pipeline) and fog-release.md (the manual kernel/init/iPXE side of cutting a release). --- docs/development/fog-release.md | 3 +- docs/development/stable-release-workflow.md | 85 +++++++++++++++++++++ docs/development/version-sync-automation.md | 4 +- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 docs/development/stable-release-workflow.md diff --git a/docs/development/fog-release.md b/docs/development/fog-release.md index 2af1602..cf1f158 100644 --- a/docs/development/fog-release.md +++ b/docs/development/fog-release.md @@ -21,7 +21,8 @@ is cut. The sections below cover updating each of those components and the testing that validates a release. See [Version Sync Automation](version-sync-automation.md) for how `FOG_VERSION`/`FOG_CHANNEL` itself is kept in sync separately from -this manual release process. +this manual release process, and [Stable Release Workflow](stable-release-workflow.md) +for how a `dev-branch` release actually gets tagged and published to `stable`. ## Updating dependencies diff --git a/docs/development/stable-release-workflow.md b/docs/development/stable-release-workflow.md new file mode 100644 index 0000000..e9901c8 --- /dev/null +++ b/docs/development/stable-release-workflow.md @@ -0,0 +1,85 @@ +--- +title: Stable Release Workflow +description: How dev-branch changes become a tagged stable release +context_id: stable-release-workflow +aliases: + - Stable Release Workflow + - stable-releases.yml +tags: + - development + - release + - automation +--- +# Stable Release Workflow + +[`stable-releases.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/stable-releases.yml) +in `FOGProject/fog-workflows` is the pipeline that turns whatever's currently +on `dev-branch` into a tagged, published `stable` release. It runs monthly +(`11 11 11 * *` — the 11th at 11:11 UTC) or on demand via +`workflow_dispatch`. See [Version Sync Automation](version-sync-automation.md) +for how the version *number* itself gets computed; this page covers what +happens once a release is actually cut. + +## The flow + +1. **`create-release-pull-request`** reads `FOG_VERSION` straight out of + `dev-branch`'s `system.class.php` and compares it to the tag name of the + latest published GitHub release. If they match, there's nothing new to + release — the run stops here (**`no-release-needed`** posts a "nothing to + release" note to Discord and exits). If they differ, it opens a + `dev-branch → stable` PR titled `Stable Release PR For {version} - + {date}`. +2. **`run-install-tests`** triggers `FOGProject/fogproject-install-validation`'s + `run_all_distros.yml` — the full matrix of supported distros. +3. **`check-all-tests-completed-successfully`** polls that run until it + reports `success` or `failure`, and fails the job if the distro tests + failed. +4. On success, **`merge-after-all-tests-passed`** merges the PR + (`dev-branch` into `stable`). On failure, + **`close-pr-if-tests-fail`** closes the PR instead and posts a failure + notice to Discord — nothing gets released. +5. **`tag-and-release`** (only after a successful merge) builds the release: + - Generates base notes via GitHub's `releases/generate-notes` API. + - Appends a `## Commits` section built from `git log`, filtering out + merge commits, prior "Stable Release" PR commits, and + `chore: fix stale FOG_VERSION/FOG_CHANNEL` fixup commits (see + [Version Sync Automation](version-sync-automation.md)) — those stay in + git history, they're just excluded from the rendered changelog so a + run of near-identical bot commits doesn't spam the notes. + - Appends issues closed since the previous release, and any new/updated + GitHub security advisories — for the latter, it also **PATCHes the + advisory itself**, appending this release's tag to + `patched_versions`. + - Publishes the GitHub release (`gh release create`) tagged and titled + with the version, marked `--latest`. +6. **`sync-branches`** opens and merges a `stable → dev-branch` PR, so the + release commit (and its tag-adjacent history) flows back into + `dev-branch` rather than the two branches silently diverging. +7. **`discord-success`** posts the release announcement with a link to the + GitHub release notes. + +## Why it's structured this way + +- **Reading the version instead of computing it**: this workflow only ever + *reads* `FOG_VERSION` to decide whether there's something new to release — + it never recomputes it. `stable`'s version is entirely the one already + computed on `dev-branch` by the mechanisms in + [Version Sync Automation](version-sync-automation.md); this workflow just + decides when to promote it. +- **Tests gate the merge, not the version**: nothing here is a + `pull_request`-triggered CI check. The install-validation suite runs + *after* the release PR is opened, and only a passing run causes the merge + to happen — a failing run closes the PR instead of leaving it open + indefinitely. +- **The sync-back PR** exists so `stable` and `dev-branch` never drift apart + after a release — without it, the release/tag commit created directly on + `stable` would only ever exist there. + +## Related + +- [Version Sync Automation](version-sync-automation.md) — how + `FOG_VERSION`/`FOG_CHANNEL` are kept correct on every branch, including + `dev-branch` before this workflow ever reads it. +- [Fog Release](fog-release.md) — the manual side of a release (kernel, + init, and iPXE binary updates) that typically happens before this + workflow is triggered. diff --git a/docs/development/version-sync-automation.md b/docs/development/version-sync-automation.md index 678b106..5330afb 100644 --- a/docs/development/version-sync-automation.md +++ b/docs/development/version-sync-automation.md @@ -118,7 +118,9 @@ still there in `git log` for anyone who needs to look. ## 3. stable-releases.yml `stable`'s version is owned entirely by fog-workflows' -[`stable-releases.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/stable-releases.yml), +[`stable-releases.yml`](https://github.com/FOGProject/fog-workflows/blob/main/.github/workflows/stable-releases.yml) +(see [Stable Release Workflow](stable-release-workflow.md) for how that +pipeline works end to end), which drives the whole release flow (validation, tagging, release notes, syncing `stable` back into `dev-branch`). The hourly sweep above deliberately excludes `stable` so the two mechanisms never fight over the same branch.