--- name: release-procedure description: >- Cut, tag, and publish an alef release end-to-end. Use this skill any time the user asks for a release, a version bump, a hotfix tag, or a CHANGELOG roll-up in this repo. Covers the full pipeline: changelog, version sync via Taskfile, Cargo.toml verification, poly lint pass, atomic commit (no AI signatures, no --no-verify when avoidable), git tag, and `gh release create` (not just a tag). license: MIT --- # Alef Release Procedure Cut a new alef version with a verifiable, reproducible procedure. Skip no step. Every release step has a concrete verification — never assume; always check. ## When to apply - User asks to cut/release/publish a new version - User asks to bump alef version - After a stack of fix/feat commits that need a version cycle - After a breaking change is committed (must bump MINOR pre-1.0, MAJOR otherwise) - When consumer repos need a new pin ## Hard rules 1. **Always update `CHANGELOG.md`** — every release has a dated heading and one bullet per user-visible change. Move entries from `[Unreleased]` into the new version section. Group under `### Added`, `### Changed (BREAKING)`, `### Fixed`, `### Removed`. Never tag a version with an empty section. 2. **Always run `poly fmt --fix .` then `poly lint .`** to fix lint/format issues before publishing. Re-stage anything the formatter rewrites. Only commit with `--no-verify` if a hook is genuinely broken in a way unrelated to the change — and then file an issue. 3. **No AI signatures** in commit messages, tag messages, or release notes. No `Co-Authored-By: Claude`, no `Generated by ...`. Never. 4. **Atomic commits** — `chore(release): X.Y.Z` carries only the version bump and changelog roll. Code fixes live in their own commits, merged before the release commit. 5. **Add tests for any fix that changed behavior.** A release that includes a fix without a regression test is a release that will regress. 6. **Use the Taskfile** for version setting — never hand-edit `Cargo.toml`, `alef.toml`, `src/core/template_versions.rs`, or `schemas/alef.schema.json`. `task set-version` rewrites all four in lockstep. 7. **Publish with `gh release create`** — a bare `git tag` is not a release. The `Publish` workflow triggers on `release: types: [published]`, so the GitHub release is literally what runs `cargo publish`; a tag push alone runs nothing at all. This is not theoretical: v0.55.2 and v0.55.3 were tagged and pushed with no release created, and neither ever reached crates.io. Alef ships as a single crate, so one release means exactly one `cargo publish` — no multi-crate sequencing, no index propagation race. ## Procedure ### 0. Pre-flight ```bash git status # working tree clean (or only release-prep diffs) git fetch origin # inspect freshness without rewriting local commits ``` If the release branch has diverged from its upstream, stop and ask how to reconcile it. Do not rebase or merge after committing unless the user explicitly asks for it. ### 1. Update CHANGELOG.md - Open `CHANGELOG.md`. - Move every bullet under `## [Unreleased]` into a new section `## [X.Y.Z] - YYYY-MM-DD` (today's date, ISO format). - Re-create an empty `## [Unreleased]` heading at the top. - If the release contains anything tagged `!` (breaking) in commit messages, surface it under `### Changed (BREAKING)` with explicit migration guidance. - Verify no entries are lost: `git diff CHANGELOG.md` should show only adds in the new section + the moved bullets. - When folding scratchpad or agent-drafted bullets into a section, strip every heading line from the source first — write plain bullets only, never nested `#`/`##` lines. A stray heading from a pasted source reparents everything below it under the wrong version. Before and after any CHANGELOG edit, `grep -c '^## \['` must be unchanged and `grep -c '^# '` must be exactly `1` (the file's single top-level title). - `poly fmt --fix CHANGELOG.md` has previously demoted every heading below the first (rumdl's autofix for a second level-1 heading treats it as a title and reparents everything that follows). `poly.toml`'s `[fmt.markdown.rumdl] disable` already includes `"MD025"` as the guard — do not remove it. Re-run the `grep -c '^## \['` / `grep -c '^# '` check after any `poly fmt` pass over this file regardless. ### 2. Set the version via Taskfile ```bash task set-version -- X.Y.Z # bumps Cargo.toml, alef.toml, ALEF_REV; regenerates # schemas/alef.schema.json; runs cargo update ``` The `set-version` task is the **only** sanctioned way to bump versions in this repo. It rewrites `Cargo.toml` (`package.version`), `alef.toml` (`alef_version`), and `src/core/template_versions.rs::ALEF_REV`, regenerates `schemas/alef.schema.json` (`cargo run -- schema --schema-version`), then runs `cargo update` — all in one shot. Never hand-edit any of these — they must stay in lockstep. The regenerated schema shows up in the release diff (its `$id` and `version` both carry the new version); that is expected output, not drift. After the task finishes, **verify**: ```bash grep -E '^version' Cargo.toml # package version grep -E '^alef_version' alef.toml # alef.toml mirror grep ALEF_REV src/core/template_versions.rs # template version pin grep '"version"' schemas/alef.schema.json # regenerated schema ``` All four must match `X.Y.Z` (the `ALEF_REV` line and the schema `$id` both include a leading `v`). ### 3. Lint pass ```bash poly fmt --fix . poly lint . ``` Re-stage any files the formatter rewrote. If a lint fails for a real reason, fix that reason — never bypass with `--no-verify` to push past a lint failure. ### 4. Tests for changed behavior For every `fix:` or `feat:` rolled into this release, confirm there is a test that would have caught the bug or covers the new surface. Add the test now if missing — release commit goes on top. ### 5. Commit ```bash git add -A git commit -m "chore(release): X.Y.Z" ``` The commit subject is exactly `chore(release): X.Y.Z`. No body unless the release is large enough to warrant a summary; never add AI attribution. If gitfluff or another commit-msg hook rewrites the subject in an unhelpful way, prefer fixing the hook config over `--no-verify`. When the user has explicitly authorized `--no-verify` for this run, document which hooks were skipped in the release notes. ### 6. Tag and publish Push `main` **before** tagging. Tagging first and pushing second means a rebase or a rejected push after the tag exists leaves the tag pointing at a commit `origin/main` never contains — `--force-with-lease` does not work on tags, so recovering means deleting and recreating the tag. Push main, confirm it landed, then tag against the now-confirmed commit: ```bash git push origin main git tag -a vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Z ``` Then create the GitHub release — this is the part most likely to be skipped and the most important: ```bash gh release create vX.Y.Z \ --title "vX.Y.Z" \ --notes-from-tag \ --verify-tag ``` If the changelog entry is rich enough to use as release notes, replace `--notes-from-tag` with `--notes-file <(awk '/^## \[X.Y.Z\]/,/^## \[/' CHANGELOG.md | head -n -1)` or build a small notes file from the new CHANGELOG section. For pre-releases (RC, beta), add `--prerelease`. ### 7. Verify The release object existing is not proof the crate shipped — verify the *registry*, not just the GitHub release: ```bash gh release view vX.Y.Z # release exists with notes git ls-remote --tags origin vX.Y.Z # tag pushed gh run list --workflow=publish.yaml --json databaseId,status,conclusion -L 5 gh run view --json jobs \ --jq '.jobs[] | select(.name | test("crates")) | {name, conclusion}' curl -sI -H 'User-Agent: alef-release (contact: )' \ https://index.crates.io/al/ef/alef | head -1 # 200 if the version is on the index ``` Notes: - A *failed* publish job may still have published (e.g. it failed on a later step after `cargo publish` already succeeded) — check the crates.io index before assuming a red run means nothing shipped, and before re-running. - A *skipped* job in the run is not the same as a passing one — a publish run that reports overall success while a job inside it was skipped can still mean the crate never moved. Read individual job conclusions, not just the run's headline status. - If a run must be retried, `gh run rerun --failed ` re-runs only the failed jobs; confirm which ones actually need it first. - If none of the above resolve, redo the failed step — do not move on. ### 8. Downstream pins For any consumer repo that pins this version, open a follow-up PR that bumps the pin. Don't bundle that into the release commit. ### 9. Local install and cleanup (optional) To pick up the release locally, `cargo install --path . --force` from the repo root (see the `local-alef-install` rule — never `cargo install alef` from crates.io for testing a pre-release change). Then `task clean` (`cargo clean` + `rm -rf .alef/`) to reclaim space once the release artifacts are no longer needed. ## Anti-patterns - Tagging without a `gh release create` — the crate is never published at all. `Publish` fires on `release: published`, not on the tag. This silently lost v0.55.2 and v0.55.3; both are tagged on origin and absent from crates.io. - Empty `## [Unreleased]` rolled forward to a new version section. - Hand-editing `version = "..."` in `Cargo.toml`, `alef_version` in `alef.toml`, or `ALEF_REV` in `src/core/template_versions.rs` instead of using `task set-version`. - Fix commits with no test added. - `--no-verify` to skip a real lint failure. - AI attribution in commit/tag/release text. - Squashing release prep with code fixes — keep `chore(release): X.Y.Z` atomic. - Treating a green publish run as proof of a shipped crate without checking for a skipped job inside it. - Re-running a failed publish job without first checking whether it already published — a second `cargo publish` for the same version fails loudly, but the confusion it causes is avoidable. - Tagging before pushing `main` (see step 6) — recovering from a stranded tag means delete-and-recreate, not `--force-with-lease`. - Chasing a `Publish` run stuck in `queued` as a code bug — this is commonly account-wide runner capacity, not this repo. Report it once with the run link; if it is genuinely wedged rather than merely slow, `gh run cancel --force-cancel ` before retrying (`cancel` alone can leave a queued run deadlocked). ## Quick reference | Step | Command | What it verifies | | ---------- | ------------------------------------------------------------------ | -------------------------- | | Pre-flight | `git status && git fetch origin` | Clean tree + remote state | | Changelog | manual edit of `CHANGELOG.md` | Every change is documented | | Version | `task set-version -- X.Y.Z` then `grep -E '^version' Cargo.toml` | Crate version updated | | Lint | `poly fmt --fix . && poly lint .` | Lint clean | | Commit | `git commit -m "chore(release): X.Y.Z"` | Atomic release commit | | Push main | `git push origin main` | Tag will land on a commit `origin/main` actually has | | Tag | `git tag -a vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z` | Tag exists remotely | | Publish | `gh release create vX.Y.Z --notes-from-tag --verify-tag` | GitHub release exists | | Verify | `gh run view --json jobs --jq '...test("crates")...'` + crates.io index `curl` | Crate actually shipped, not just the release object |