From ab819989960e7e7336a47ba93a0472bfbcb10efb Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Fri, 29 May 2026 11:18:45 +0200 Subject: [PATCH] docs(release): establish stable-channel cadence + promotion process (#565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #565. Beta has been the de-facto stable channel because the actual stable lagged so far behind that new users following the README ended up worse off than users who knew to switch to beta. The fix has two parts: regular stable cuts (the PR #568 promotion is the first one) and a written process so future cuts don't depend on memory. This adds: - RELEASING.md at the repo root — full operational doc with cadence target (4–6 weeks), promotion criteria (CI green + 7-day bug soak + upgrade-walk on real-shaped data + operator smoke), the actual beta→main mechanics including the conflict-resolution checklist we used in PR #568, hotfix backport path (with PR #412 as the worked example), and the project's versioning rules. - CONTRIBUTING.md — replaces the four-line "Release Process" stub (which was wrong; it described a hand-rolled flow that release-please has handled for the last several releases) with a brief summary and a pointer to RELEASING.md. - README.md — one-sentence addition to the existing "Release Channels" section pointing curious users at RELEASING.md. No code change. CHANGELOG.md and version files are intentionally untouched — release-please will catch this on the next regular cut. --- CONTRIBUTING.md | 7 ++-- README.md | 2 +- RELEASING.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 RELEASING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66aedcec..87ee9df1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,10 +146,9 @@ picpeak/ ## 🔄 Release Process -1. Update version numbers in package.json files -2. Update CHANGELOG.md -3. Create a new release on GitHub -4. Docker images are automatically built and published +Releases are cut from the `beta` branch (rolling beta) and promoted to `main` (stable) on a 4–6 week cadence. `release-please` handles version bumps, changelog generation, and Docker image publication automatically — contributors don't update `package.json` or `CHANGELOG.md` by hand. + +See [RELEASING.md](RELEASING.md) for the full operational doc (promotion criteria, conflict-resolution checklist for the beta→main merge, hotfix backport path, versioning rules). ## 📮 Contact diff --git a/README.md b/README.md index b8c9b78e..efea2a32 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Note on Docker file permissions ## 🔄 Release Channels -PicPeak offers two release channels for different needs: +PicPeak offers two release channels for different needs. Stable promotions are cut from a known-good beta point every 4–6 weeks — see [RELEASING.md](RELEASING.md) for the maintainer's promotion criteria and cadence policy. ### Stable Channel (Recommended) - Production-ready releases diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..2f1782fe --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,92 @@ +# Release Process + +This document describes how PicPeak releases are cut. It's the maintainer's reference, not user documentation — for the user-facing channel choice (stable vs beta) see the [Release Channels section in README.md](README.md#-release-channels). + +## TL;DR + +- **`beta` branch** receives all merged work. Every push triggers a `release-please` PR that proposes the next `vX.Y.Z-beta.N` release. Merging that PR tags the beta and publishes Docker images on the `beta` tag. +- **`main` branch** holds the stable channel. Stable releases are cut from a known-good `beta` point via a `release/X.Y.Z-merge-from-beta` branch and a manual PR to `main`. Merging that PR triggers `release-please` to propose the stable release. +- Target cadence: **a stable release every 4–6 weeks**, or sooner if a beta has been quiet and ready for promotion. + +## Cadence target + +4–6 weeks between stable releases is the working target. Reasoning: + +- Long enough that each stable carries meaningful changes worth the upgrade burden. +- Short enough that beta users aren't carrying the "real" project alone for months — the stable channel should actually be usable as the recommended channel for new installs. +- Aligns with how release-please surfaces beta releases (multiple beta points usually accumulate inside a 4–6 week window, which gives natural promotion candidates). + +This is a target, not a hard rule. Cut sooner if a beta has been quiet and stable longer than usual. Cut later if a beta is in flux for security or migration reasons. + +## Promotion criteria + +A beta is eligible for promotion to stable when **all** of the following hold: + +1. **CI green on the candidate beta tip.** Specifically: `schema-drift` (`upgrade-from-bootstrap`), `fresh-install`, `Tests` (backend Jest + frontend Vitest), the four `Build and Push Docker Images` arch matrices, and `GitGuardian Security Checks`. +2. **No open `bug`-labelled issues against the candidate beta for at least 7 days.** Issues fixed-but-not-yet-closed count as fixed; verify their PR is in the candidate beta before closing them out. +3. **An upgrade walk has been done on real production-shaped data** — apply the candidate's migration chain to a snapshot of the previous stable's DB and verify no manual intervention is required. CI proves fresh-install works; the upgrade walk is what proves the upgrade path works. +4. **Operator-time smoke** on the candidate: log in, create event, upload photos, share gallery, open as a customer, log out. Catches binary-incompatibility regressions and UI-level breaks that unit tests don't see. + +If any of the four fail, the promotion waits. File any blockers as `bug`-labelled issues and let them bake on beta before re-evaluating. + +## How a stable release is cut + +The actual mechanics, in order: + +1. **Pick the beta tip.** Confirm it satisfies the four promotion criteria above. Note the exact SHA — that's what you're promoting. + +2. **Create the release branch from the beta tip.** + ```bash + git push origin :refs/heads/release/X.Y.Z-merge-from-beta + ``` + Naming convention: `release/X.Y.Z-merge-from-beta`, where `X.Y.Z` is the stable version you intend to land. release-please will write the actual `X.Y.Z` on merge — the branch name is just a human label. + +3. **Open a PR to `main`.** Title: `chore(release): promote beta → main as vX.Y.Z`. Body should summarise the major themes since the previous stable, the migration count, and any operator notes (e.g. "this release adds 22 migrations; existing installs should snapshot before upgrading"). See PR #568 as a worked example. + +4. **Resolve conflicts.** Main almost always has commits beta doesn't (security backports, release-please's stable-channel release commits, README rewrites). For each conflicting file, decide deliberately: + - **`backend/package.json` / `package-lock.json` + `frontend/package.json` / `package-lock.json`** — usually take beta's version (superset), but verify any security-pinned deps (`axios`, `nodemailer`, `i18next-http-backend`, `multer`, `tar`) on beta are `>=` the pinned versions on main. If main has a newer pinned version (e.g. an emergency CVE backport beta hasn't picked up), take main's pin. + - **`README.md`** — keep main's version if main has had a recent rewrite that beta didn't pick up; otherwise take beta's. + - **`CHANGELOG.md`** — keep main's; release-please regenerates entries on its next stable cut from the commits going forward. + - **`.release-please-manifest.json`** — keep main's; release-please owns this file. + - Any other auto-merged file — spot-check that the auto-merge produced something sensible, especially for security-sensitive files (`backend/src/middleware/`, `backend/src/utils/tokenUtils.js`). + +5. **Wait for CI on the PR.** All ten checks (the original eight plus `merge-backend` and `merge-frontend`) must be green. If anything fails, fix on the release branch (NOT on beta — beta has already moved on). + +6. **Merge.** Standard merge commit, not squash — the PR's history (the individual feature commits) carries forward into main's log. + +7. **release-please picks it up.** Within minutes, release-please will open a new `chore(main): release X.Y.Z` PR proposing the stable release. Review the auto-generated CHANGELOG.md entries for accuracy, edit if needed, and merge. That merge creates the `vX.Y.Z` git tag, publishes Docker images on the `stable` and `latest` tags, and creates the GitHub Release page. + +8. **Close the loop.** Bulk-close any `bug` issues that were fixed-but-not-closed and now appear in the released changelog. Reference the merge commit so reporters know which version contains the fix. + +## Hotfix path (backport to current stable) + +If a critical bug or security issue affects the current stable and beta has moved too far for a full promotion to be appropriate, backport just the fix: + +1. Create a `security/cve-backport-X.Y.Z` or `fix/critical-X.Y.Z` branch off `main`. +2. Cherry-pick or hand-write the minimal fix. +3. Open a PR to `main` with the smallest possible diff. +4. After merge, release-please will propose a patch-level stable release (e.g. `v3.55.1`). +5. **Forward-port the fix to beta** if it isn't already there. Otherwise the next full promotion will reintroduce the bug. + +PR #412 ("backport 18 dependency CVE patches from beta") is a worked example of this path. + +## Versioning + +PicPeak follows [Semantic Versioning](https://semver.org/) with one project-specific convention: + +- **MAJOR** bumps are reserved for breaking schema changes that require operator action on upgrade (e.g. a migration that's not safe to auto-apply, an env-var rename that can't be auto-detected). +- **MINOR** bumps for new features, additive schema changes, and any change to the public HTTP API surface. +- **PATCH** bumps for bug fixes and operator-invisible internal changes. +- **Beta suffix** (`-beta.N`) for every beta cut; the `N` counter resets on each new MINOR or MAJOR target. + +release-please derives all of this from conventional commit prefixes (`feat:`, `fix:`, `BREAKING CHANGE:`, etc.) automatically. + +## Things that don't go through this process + +- **Documentation-only changes** can land on either `main` or `beta` directly (no release cut needed); release-please will pick them up on the next regular release. +- **Test-only changes** — same. +- **CI / workflow changes** — same, but be aware they take effect on the branch they land on, so a CI fix targeting beta won't fix a broken stable-channel workflow until the next promotion. + +## When this doc is wrong + +If you find yourself working around something here, update the doc before doing the workaround. The point of a written process is that future-you doesn't have to remember the workaround.