From 3ca6378bd7c45f23004b2d9896159057a323ef73 Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Mon, 29 Jun 2026 22:15:31 +0200 Subject: [PATCH] chore: workflows + RELEASING.md for the post-rename branch model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the org move + branch rename (#669): beta → main (active development) main → stable (curated release channel) This PR rewires the workflows that referenced the old branch names so release-please and the Docker build target the right channels. ## Workflow changes ### `.github/workflows/docker-build.yml` - **Push triggers**: `[main, beta]` → `[main, stable]` (both `push.branches` and `pull_request.branches`). `beta` no longer exists; `stable` is the curated channel that should also produce builds. - **`is_prerelease` detection**: pre-release context was decided by `refs/heads/beta`; now decided by `refs/heads/main` (active dev → prerelease, `-beta.N` version suffix unchanged). - **`:latest` + `:stable` tagging**: were gated on `{{is_default_branch}}` (which used to be `main` = stable channel). Default branch is now `main` = active dev, so the implicit gate would have aliased `:latest` to dev. Both tags now explicitly gate on `refs/heads/stable` OR a non-prerelease release tag. - **`:beta` tag**: REMOVED. Active-dev pulls are `:main` (auto-generated by `type=ref,event=branch`). The pre-rename `:beta` tag remains frozen at its last build under Option B / #669 — operators are expected to update to `:main` or pin to a versioned tag. ### `.github/workflows/release-please.yml` - `branches: [main]` → `branches: [stable]`. This is the **stable** release-please workflow (uses `release-please-config.json`); after the rename, the stable channel lives on the `stable` branch. ### `.github/workflows/release-please-beta.yml` - `branches: [beta]` → `branches: [main]`. - `target-branch: beta` → `target-branch: main`. - This is the **pre-release** release-please workflow (uses `release-please-config-beta.json`, `prerelease: true`); after the rename, pre-releases are cut from the new `main` (active dev). The version-suffix scheme stays `-beta.N` so existing operator pins keep working. ## RELEASING.md Rewrote the TL;DR, "How a stable release is cut", and hotfix path to reference the new branch names. Added a one-line "branch model background" note pointing at #669 so future maintainers know why `main` means active dev (the opposite of what some projects use). Filename conventions: `release/X.Y.Z-merge-from-main` (was `…-from-beta`); promotion PR title `promote main → stable as vX.Y.Z` (was `promote beta → main`). ## Why combined with PR A's content as a single PR Originally planned as two PRs (B = workflow triggers, C = release-please reconfigure). Splitting wasn't worth it: the configs are branch-agnostic (`release-please-config.json` and `release-please-config-beta.json` don't mention branch names internally), and not bundling them meant a window where the stable release-please workflow would fire on pushes to the new `main` (active dev) — exactly the wrong place. Single PR closes that gap. ## Versioning scheme — kept No version-scheme decision needed. The `-beta.N` suffix on pre-release versions is preserved (existing operator pins like `v3.71.3-beta.0` keep working). If a `v4.0.0-pre.N`-style reset is desired later, that's a separate PR with explicit operator-comms attached. --- .github/workflows/docker-build.yml | 37 +++++++++---- .github/workflows/release-please-beta.yml | 4 +- .github/workflows/release-please.yml | 2 +- RELEASING.md | 66 ++++++++++++----------- 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6aa4d1d5..bcbcf1d3 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,7 +1,8 @@ name: Build and Push Docker Images # This workflow is triggered by: -# - Push to main/beta branches (builds 'latest'/'stable' or 'beta' tagged images) +# - Push to main/stable branches (main → ':main' rolling tag for active-dev +# builds; stable → ':stable' + ':latest' for the curated channel) # - Version tags from Release Please (e.g., v1.2.0 -> builds versioned images) # - GitHub Releases (created by Release Please) # - Pull requests (build verification only, no push by default) @@ -20,10 +21,10 @@ name: Build and Push Docker Images on: push: - branches: [ main, beta ] + branches: [ main, stable ] tags: [ 'v*.*.*', 'v*.*.*-beta.*' ] # Triggered by Release Please tags (stable and beta) pull_request: - branches: [ main, beta ] + branches: [ main, stable ] release: types: [ published ] # Triggered when Release Please creates a release workflow_dispatch: @@ -245,7 +246,9 @@ jobs: - name: Determine build context id: context run: | - if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/main ]]; then + # Active-dev branch (`main`, renamed from `beta` per #669) produces + # prereleases; the `-beta.N` version-suffix scheme is unchanged. echo "channel=beta" >> $GITHUB_OUTPUT echo "is_prerelease=true" >> $GITHUB_OUTPUT else @@ -270,9 +273,14 @@ jobs: type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=sha,format=short - type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} - type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} + # `:latest` + `:stable` follow the stable channel (the `stable` branch + + # stable release tags). The default branch is now `main` (active dev), + # so `is_default_branch` no longer maps to "stable" — be explicit. + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/stable' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + # `:beta` is RETIRED post-rename (Option B / #669). Active-dev pulls + # are `:main` (auto via type=ref,event=branch). The pre-rename `:beta` + # tag remains frozen at its last build — operators should update. - name: Create and push multi-arch manifest working-directory: /tmp/digests @@ -455,7 +463,9 @@ jobs: - name: Determine build context id: context run: | - if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/main ]]; then + # Active-dev branch (`main`, renamed from `beta` per #669) produces + # prereleases; the `-beta.N` version-suffix scheme is unchanged. echo "channel=beta" >> $GITHUB_OUTPUT echo "is_prerelease=true" >> $GITHUB_OUTPUT else @@ -480,9 +490,14 @@ jobs: type=semver,pattern={{major}}.{{minor}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=semver,pattern={{major}},enable=${{ steps.context.outputs.is_prerelease == 'false' }} type=sha,format=short - type=raw,value=latest,enable={{is_default_branch}} - type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} - type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} + # `:latest` + `:stable` follow the stable channel (the `stable` branch + + # stable release tags). The default branch is now `main` (active dev), + # so `is_default_branch` no longer maps to "stable" — be explicit. + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/stable' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} + # `:beta` is RETIRED post-rename (Option B / #669). Active-dev pulls + # are `:main` (auto via type=ref,event=branch). The pre-rename `:beta` + # tag remains frozen at its last build — operators should update. - name: Create and push multi-arch manifest working-directory: /tmp/digests diff --git a/.github/workflows/release-please-beta.yml b/.github/workflows/release-please-beta.yml index a6356d91..701f9e5a 100644 --- a/.github/workflows/release-please-beta.yml +++ b/.github/workflows/release-please-beta.yml @@ -2,7 +2,7 @@ name: Release Please (Beta) on: push: - branches: [beta] + branches: [main] permissions: contents: write @@ -23,7 +23,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} config-file: release-please-config-beta.json manifest-file: .release-please-manifest-beta.json - target-branch: beta + target-branch: main - name: Output Release Info if: ${{ steps.release.outputs.release_created }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index f7a148f3..f44b6cf8 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,7 +2,7 @@ name: Release Please on: push: - branches: [main] + branches: [stable] permissions: contents: write diff --git a/RELEASING.md b/RELEASING.md index 2f1782fe..1fe8b755 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,74 +1,76 @@ # 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). +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 pre-release) 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. +- **`main` branch** receives all merged work (active development). Every push triggers a `release-please` PR that proposes the next `vX.Y.Z-beta.N` pre-release. Merging that PR tags the pre-release and publishes Docker images under the `:main` rolling tag + the version-specific tag. +- **`stable` branch** holds the curated stable channel. Stable releases are cut from a known-good `main` point via a `release/X.Y.Z-merge-from-main` branch and a manual PR to `stable`. Merging that PR triggers `release-please` to propose the stable release. +- Target cadence: **a stable release every 4–6 weeks**, or sooner if `main` has been quiet and ready for promotion. + +> **Branch model background** — `main` (active dev) was previously called `beta`, and `stable` (curated channel) was previously called `main`. The rename happened with #669 to match the convention every other open-source project uses. The mechanics below all reference the post-rename names. ## 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). +- Short enough that pre-release 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 pre-releases (multiple pre-release 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. +This is a target, not a hard rule. Cut sooner if `main` has been quiet and stable longer than usual. Cut later if `main` is in flux for security or migration reasons. ## Promotion criteria -A beta is eligible for promotion to stable when **all** of the following hold: +A `main` tip 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. +1. **CI green on the candidate `main` 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 for at least 7 days.** Issues fixed-but-not-yet-closed count as fixed; verify their PR is in the candidate `main` tip 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. +If any of the four fail, the promotion waits. File any blockers as `bug`-labelled issues and let them bake on `main` 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. +1. **Pick the `main` 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.** +2. **Create the release branch from the `main` tip.** ```bash - git push origin :refs/heads/release/X.Y.Z-merge-from-beta + git push origin :refs/heads/release/X.Y.Z-merge-from-main ``` - 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. + Naming convention: `release/X.Y.Z-merge-from-main`, 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. +3. **Open a PR to `stable`.** Title: `chore(release): promote main → stable 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 (predates the rename; the mechanics are unchanged). -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. +4. **Resolve conflicts.** `stable` almost always has commits `main` 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 `main`'s version (superset), but verify any security-pinned deps (`axios`, `nodemailer`, `i18next-http-backend`, `multer`, `tar`) on `main` are `>=` the pinned versions on `stable`. If `stable` has a newer pinned version (e.g. an emergency CVE backport `main` hasn't picked up), take `stable`'s pin. + - **`README.md`** — keep `stable`'s version if it has had a recent rewrite that `main` didn't pick up; otherwise take `main`'s. + - **`CHANGELOG.md`** — keep `stable`'s; release-please regenerates entries on its next stable cut from the commits going forward. + - **`.release-please-manifest.json`** — keep `stable`'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). +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 `main` — `main` 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. +6. **Merge.** Standard merge commit, not squash — the PR's history (the individual feature commits) carries forward into `stable`'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. +7. **release-please picks it up.** Within minutes, release-please will open a new `chore(stable): 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: +If a critical bug or security issue affects the current stable and `main` 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`. +1. Create a `security/cve-backport-X.Y.Z` or `fix/critical-X.Y.Z` branch off `stable`. 2. Cherry-pick or hand-write the minimal fix. -3. Open a PR to `main` with the smallest possible diff. +3. Open a PR to `stable` 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. +5. **Forward-port the fix to `main`** 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. +PR #412 ("backport 18 dependency CVE patches from beta") is a worked example of this path (predates the rename; the mechanics are unchanged). ## Versioning @@ -77,15 +79,15 @@ PicPeak follows [Semantic Versioning](https://semver.org/) with one project-spec - **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. +- **Pre-release suffix** (`-beta.N`) for every `main`-channel cut; the `N` counter resets on each new MINOR or MAJOR target. The suffix kept the historical `-beta` literal even after the branch rename — operators were already pinning to `v3.x.y-beta.N` and changing the literal would have broken those pins. 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. +- **Documentation-only changes** can land on either `stable` or `main` 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. +- **CI / workflow changes** — same, but be aware they take effect on the branch they land on, so a CI fix targeting `main` won't fix a broken stable-channel workflow until the next promotion. ## When this doc is wrong