diff --git a/docs/development/fog-release.md b/docs/development/fog-release.md index 289a8e9..cf1f158 100644 --- a/docs/development/fog-release.md +++ b/docs/development/fog-release.md @@ -19,7 +19,10 @@ 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, 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 new file mode 100644 index 0000000..5330afb --- /dev/null +++ b/docs/development/version-sync-automation.md @@ -0,0 +1,130 @@ +--- +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. 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` 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 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 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) +(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. + +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.