# Releasing Lamb Maintainer checklist for cutting a release. Lamb ships from the `release` branch (see `BRANCHES`); `main` is the active development branch. Versions are plain SemVer tags (`0.9.0`); pre-releases use a suffix (`0.9.0-rc1`). There is no version string in the code — **the Git tag is the source of truth**. > **Run everything below from Lamb's devbox shell** (`devbox shell` in the repo) > or via `devbox run -- …`. The pre-push hook runs the test suite, which needs > PHP — a bare shell (or the *global* devbox) won't have it. ## 1. Pre-flight (on `main`) - [ ] All intended PRs are merged into `main`; nothing release-worthy is still open. - [ ] Working tree is clean and `main` is up to date: `git checkout main && git pull`. - [ ] Tests pass: `vendor/bin/codecept run` (Unit + Acceptance). Acceptance needs `.env` (`SITE_URL`, `LAMB_TEST_PASSWORD`); generate it with `LAMB_WRITE_TEST_PASSWORD=1 php make-password.php ` so the cleartext `LAMB_TEST_PASSWORD` is written (it is omitted by default). Acceptance starts its own server, so don't have another server on the test port. - [ ] Static checks pass: `viv run lint` && `viv run analyse`. - [ ] Docs are accurate for any user-facing change (`docs/`, `README.md`). - [ ] `changelog.d/` has a fragment for every user-visible change (see `changelog.d/README.md`). Catch anything missed from the milestone's closed issues: `scripts/changelog.sh '' ` prints a generated section to compare against; hand-written bullets beat generated titles, so add a fragment for what's missing rather than replacing the block. - [ ] Outstanding **major** dependency updates reviewed. Patch/minor bumps auto-merge, so majors are the only ones that accumulate — and a stale major is how you end up on a branch that no longer gets security fixes. List them without needing an install: `viv outdated` and `pnpm outdated`. Either take the bump now (its own PR, not this release) or consciously leave it; check the branch you're on is still supported upstream. ## 2. Choose the version - [ ] Decide the new version from the change set (SemVer): patch = fixes only, minor = new features, major = breaking changes. - [ ] **Pre-1.0, the minor slot is the major slot.** SemVer says only that "anything MAY change at any time" under `0.y.z`, so it gives no boundary of its own; the convention every package manager encodes (`^0.14.0` resolves `>=0.14.0 <0.15.0`) makes `0.y` the compatibility boundary, and that is the one Lamb uses. So a breaking change bumps `0.14.0` to `0.15.0`, not to `1.0.0` — read "major" above as `0.y` until 1.0. - [ ] **Anything deprecated in the last published version comes out in this one.** A deprecation buys exactly one published major (pre-1.0: one `0.y`) of grace, so the removal is due in the next one — not "a future release". Check for anything owed before you settle the version, since the removals are breaking and shape it: `grep -rn 'deprecat' src bin docs`. - [ ] Confirm it's unused: `git tag | sort -V | tail`. - [ ] If cutting a pre-release first, use an `-rcN` suffix and mark it pre-release in step 6. ## 3. Generate end-user release notes Notes are for **people running a Lamb blog**, not contributors. Start from the fragments in `changelog.d/`, then curate. - [ ] Promote the changelog: run `scripts/changelog-assemble.sh []` from the repository root (add `--dry-run` to preview the section first). It inserts `## [] - ` below `## [Unreleased]`, updates the link lines at the bottom and deletes the fragments it used. A fragment can't hold prose, so add any intro sentence a section needs (such as the release that removes the deprecated keys) by hand afterwards. This lands in the release PR (step 4). - [ ] Cross-check against the commit list for anything the changelog missed: ```sh # Everything on main since the last release tag (use the previous final tag): git log --format='- %s' ..main ``` - [ ] **Keep** changes an end user would notice: new/changed features, bug fixes affecting the blog or admin, new config keys, install/upgrade requirements (e.g. a newly required PHP extension), deployment changes (Docker/FrankenPHP/Nginx/DDev/Devbox). - [ ] **Drop** internal-only changes: dev-environment tooling (e.g. Workshop), CI, tests, refactors, code-comments/`CLAUDE.md`/`DECISIONS.md`, and dependency bumps with no user-visible effect. - [ ] Rewrite each kept line in plain language (what changed for the user, not the PR title). Group under **Added / Changed / Fixed**. - [ ] **Breaking changes go first, always.** If the release removes or changes anything an existing install depends on — a removed CLI entry point or config key, a renamed theme part, a changed default, a dropped platform requirement — open the notes with a **⚠️ Breaking changes** section, above Added/Changed/Fixed. One line per change, each saying what broke and what to use instead. A reader deciding whether to upgrade must see this without scrolling; never leave it to be discovered further down. - [ ] Call out anything else requiring action on upgrade in an **Upgrade notes** section after the grouped changes (e.g. "install the `pdo_mysql` PHP extension", config changes). - [ ] Save the notes to a temp file (e.g. `/tmp/notes.md`) for step 5. ## 4. Promote `main` to `release` via PR Direct pushes to `release` are rejected by the branch ruleset (`GH013` — changes must go through a pull request), so promotion happens as a PR merged with a **merge commit** (not squash/rebase, to keep histories connected): ```sh gh pr create --base release --head main \ --title "Release " \ --body "Promote main to release for ." gh pr merge --merge --subject "Release " ``` - [ ] Confirm the **release-verify** check is green on the PR before merging. It runs the Acceptance suite against the Docker/FrankenPHP release image and an nginx + php-fpm install — the well-travelled production paths — in addition to `ci`'s built-in-server run. - [ ] If the PR reports `BEHIND`, `release` has commits not on `main` (e.g. old release merges). Sync first: branch from `main`, `git merge origin/release` (a merge commit, no content changes expected), PR that into `main`, then re-check the release PR. - [ ] Resolve any conflicts (uncommon — `main` is the source of truth, though `release` may also carry the occasional release-only commit). - [ ] Re-run `vendor/bin/codecept run` on `release` to confirm green. ## 5. Tag and create the GitHub release `gh release create` creates the tag itself at `--target release` (the merged branch tip) and publishes the release in one step — no separate `git tag` needed. ```sh gh release create \ --target release \ --title "" \ --notes-file /tmp/notes.md # add --prerelease for an -rcN tag # add --latest to mark a final release as the latest ``` **No local `gh`?** The `Cut release` workflow (`.github/workflows/cut-release.yml`) does the same `gh release create --target release` server-side from a `workflow_dispatch`, so it can be driven without a local checkout or token (e.g. from an automation session that can trigger workflows but not create releases): ```sh gh workflow run cut-release.yml \ -f version= \ -f prerelease=true \ -f notes="$(cat /tmp/notes.md)" # omit prerelease (or set =false) for a final; add -f latest=true to mark it latest ``` It tags the current `release` tip, so promote main → release (step 4) first. - [ ] Merge the release PR (step 4) first, so `--target release` tags the intended commit. If the tag landed on the wrong commit, move it — the release object and its notes follow the tag: `git tag -f -m "" origin/release && git push -f origin `. The `release: published` event will **not** re-fire for a moved tag; use the `workflow_dispatch` re-run from step 6 instead. - [ ] Final releases: pass `--latest`. Pre-releases: pass `--prerelease` and do **not** mark latest. - [ ] Verify: `gh release view `, and `git fetch --tags` to pull the tag `gh` created. ## 6. Post-release - [ ] Publishing the release triggers the `Release artifacts` workflow. Verify it attached `lamb-.tar.gz` to the release (`gh release view `). Re-run via `gh workflow run release-artifacts.yml -f tag=` if needed. - [ ] Open an announcement discussion for the release in the **Announcements** category, linking the release tag with a short note. Match the existing per-release discussions (e.g. `Lamb 0.14.0`): ```sh REPO_ID=$(gh api graphql -f query='{repository(owner:"svandragt",name:"lamb"){id}}' --jq '.data.repository.id') gh api graphql -f query='mutation($repo:ID!,$cat:ID!,$title:String!,$body:String!){createDiscussion(input:{repositoryId:$repo,categoryId:$cat,title:$title,body:$body}){discussion{url}}}' \ -f repo="$REPO_ID" -f cat="DIC_kwDOJIhg0c4CWhS7" \ -f title="Lamb " \ -f body="https://github.com/svandragt/lamb/releases/tag/ " --jq '.data.createDiscussion.discussion.url' ``` - [ ] Announce / update any demo site if applicable. - [ ] Note that the Docker/Devbox/DDev users pull from `release`; confirm a clean checkout of `release` installs and runs. - [ ] If a `next` branch exists (work parked for the next version — see `BRANCHES`), open a PR to merge it into `main` now that the release is out: ```sh git fetch origin git show-ref --verify --quiet refs/remotes/origin/next && \ gh pr create --base main --head next \ --title "Merge next into main" \ --body "Bring parked next-version work into main after the release." ``` ## Related - `BRANCHES` — branch roles (`main`, `release`, `next`, pinned). - `CONTRIBUTING` — contribution workflow. - `docs/upgrading.md` — what end users run to upgrade.