Release promotion
Use when the integration branch looks ready to ship and someone wants to promote it to the release branch and tag a release, when a just-tagged release needs its deploy verified live and healthy ("did the deploy land", "is prod healthy after the release"), when a bad release needs rolling back ("roll back the release", "revert prod"), or when a production incident needs a hotfix landed correctly. For the hotfix path specifically, this skill's hotfix notes carry the both-branches detail.From its SKILL.md
npx -y skills add yoelgal/better-dev --skill release-promotionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.5 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it
Release promotion
One job: move the integration branch onto the release branch only when it is genuinely releasable, tag that release, and - separately - run a hotfix so the fix reaches production and never gets lost on the next promote.
Promotion is the one irreversible step in the branching model: once main moves and a tag is
pushed, users are getting that code. So this skill fails closed. Every check has to come back
green from git and CI directly; a check whose answer is unknown is not a passing check, and any
doubt stops the promote rather than shipping through it.
Before anything: read the overrides
The branch names below are the model's defaults, not a mandate. A project may integrate on
develop, release from master, or prefix hotfixes hf/. Read what the repo actually does and let
it win:
.better-dev/bin/bd-mem read overrides 2>/dev/null
.better-dev/bin/bd-mem recall "release integration branch tag hotfix" 2>/dev/null
integration="staging" # honor an override (e.g. develop)
release="main" # honor an override (e.g. master)
A repo can run trunk-based - integration and release are the same branch (a recorded
branch-model: trunk; with nothing recorded, the staged two-branch shape below is the default).
The promote then degenerates on purpose: the ancestor gate and the fast-forward are no-ops on a
single branch, and the soak window collapses to the merge itself, because /pr-and-verify's merge
into the trunk is the release. What still binds is everything after the merge: the version tag
at the released head and the deploy-verify pass in post-deploy.md, receipt and all. On a trunk
repo this skill reduces to tag-plus-verify - a smaller job, never a skipped one.
Verify the premise at the git level
A branch named in prose - or in CLAUDE.md, or in this file - isn't a branch until git shows it.
Confirm both ends are real before touching either (the same discipline /onboard uses):
git fetch origin --tags 2>/dev/null || true
git rev-parse --verify --quiet "origin/$integration" || git rev-parse --verify --quiet "$integration"
git rev-parse --verify --quiet "origin/$release" || git rev-parse --verify --quiet "$release"
If either is missing, stop and report it - a promote with no real source or target is an onboarding gap, not something to improvise.
The release gate - all of it, or no promote
Check the head commit of the integration branch, not a stale local copy. Every gate has to hold:
-
CI is green on the integration head. Read it from wherever the project's CI actually reports - a commit status (
gh api repos/{owner}/{repo}/commits/$sha/status), the checks on the last PR into integration, or the dashboard the repo uses. A red run stops the promote. So does an unknown run: no status is not a green status. -
No open blockers. No open issue or PR marked as a release blocker for this cut. If the project has no blocker convention, say so and let the operator confirm rather than assuming zero.
-
It has soaked. Read the soak window from overrides (
bd-mem recall "soak window"); with none set, default to 24h of wall-clock since the last merge, or one full green CI cycle where the repo runs no time-based window. "Stable" means a named verify-receipt in the ledger recorded against this integration head - the sha you're about to promote - not a general sense that things look fine (bd-mem ledger readthe work-item that last shipped into integration, and confirm the receipt names this sha). Check the head commit's age against the window, then confirm the receipt. No receipt on the current head is aNEEDS_INPUT, not a pass; a receipt on an older sha with merges since means it hasn't soaked at this head. -
Release contains everything already released.
mainmust be an ancestor ofstaging:git merge-base --is-ancestor "origin/$release" "origin/$integration" \ && echo "release is contained" || echo "DIVERGED"A
DIVERGEDresult meansmainholds a commitstagingdoesn't - almost always a hotfix that was never merged back. Reconcile that first (see the hotfix notes); promoting over it would drop the fix. -
Migrations in the promote range have a run plan. Diff the range against the recorded migrations glob (
git diff "$prev_tag"..origin/$integration --name-only, grepped against thesafety-denylistrule; recorded by/guardrails-install- run it if absent: a repo with a migrations directory and no recorded glob settlesNEEDS_INPUTnaming the recorder rather than passing this gate on an empty grep). A clean diff satisfies this gate silently. On a hit, run the migration gate inmigrations.mdbefore promoting - it confirms which mechanism applies the migration, fixes the apply order relative to the deploy, and takes a snapshot receipt before anything destructive. New code over an un-migrated production schema fails only after the tag, so this gate is the last place to catch it. -
Newly required env vars exist in production. When the promote range newly reads an env var, recall
"deploy-env"(recorded by/guardrails-install- run it if absent) and confirm each new var is present in production before the tag. A missing var settlesNEEDS_INPUTnaming the var - a config-only release with a green build still ships the miss, so this gate never waits for something to go red first.
If a gate is red or its answer can't be established, this is a BLOCKED (a hard failure) or
NEEDS_INPUT (a convention the operator has to name) - report which gate and stop. Don't relax a
gate to get past it.
Promote, then tag
With every gate green, move the release branch onto the integration head. Because the ancestor check passed, this is a clean fast-forward; refusing anything that isn't a fast-forward keeps a stray local commit from riding along:
git switch "$release" && git merge --ff-only "origin/$integration"
Then tag the release at that commit. Take the version from the project's scheme (a bumped
package.json, a VERSION file, the last tag's successor) - read it, don't invent one. Before
cutting it, confirm every version-bearing surface the repo ships (a package or plugin manifest, a
VERSION file) names the version being tagged - tag succession alone is not the scheme while such
a surface exists, and each tag cut over a stale manifest widens the lag every consumer of that
manifest reads. A lagging surface gets bumped as a commit that rides the promote range through the
normal gates; found after the tag, it is fixed forward on integration, since a published tag never
moves. If no scheme is discoverable, that's a NEEDS_INPUT: ask for the version rather than
guessing. Guard the tag before creating it: an existing $version tag means this promote already
ran, or the version was never bumped - either way, stop and reconcile rather than moving or
overwriting a published tag:
if git rev-parse -q --verify "refs/tags/$version" >/dev/null; then
echo "tag $version already exists - promote already ran, or the version wasn't bumped; stop and reconcile"
else
git tag -a "$version" -m "release $version" && git push origin "$release" "$version"
fi
Record the promote so a later session can see what shipped. The deploy: and health: values
come from the deploy-verify pass in the next section - write the receipt once that pass settles
its verdict:
printf 'released: %s\nfrom: %s@%s\nto: %s\ndeploy: %s\nhealth: %s\n' \
"$version" "$integration" "$(git rev-parse --short "origin/$integration")" "$release" \
"$deploy_verdict" "$health_summary" \
| .better-dev/bin/bd-mem ledger put "release-$version" release.md -
# deploy: VERIFIED | DEGRADED | UNVERIFIED | REVERTED | NO_SURFACE (typed marker, one of five)
# health: per-page "path: <load-ms>ms, <n> console errors", or "-" (the next release's baseline)
Push normally here - never --force, and never --no-verify to slip past a failing hook. A
protected release branch that rejects your push is reporting that a gate failed, not inviting you to
force through it; bypassing a hook is the same mistake wearing a different flag. That holds past this
one push: before any force-push, history rewrite, branch delete, tag move, or rm -rf across the
release surface, state exactly what you're about to run and why and get confirmation for that
specific action - a yes to one destructive step doesn't carry to the next. And if you realize you've
already lost data or pushed the wrong sha, say so at once rather than quietly repairing it.
After the tag: verify the deploy
A pushed tag starts the release; users have it only when the deploy lands and the deployed thing
runs. Recall the recorded deploy surface (.better-dev/bin/bd-mem recall "deploy"). Three recorded
answers, three paths:
deploy-surface: none- nothing runs anywhere (a library, a CLI). Recorddeploy: NO_SURFACEin the release receipt; the release is done at the tag. Where the library ships by linked install (better-dev itself), the tag is not the end - propagation is the deploy: the primary checkout pulls, fresh sessions pick up the new text, and a release that added or removed skills names theinstall.shre-run each consuming machine still owes. Record a propagation line in the release receipt.- Deploy keys recorded - run the deploy-verify pass in
post-deploy.md: wait out the deploy, drive the deployed surface, watch it hold. Its verdict lands in the receipt before the release settles. - No deploy keys at all - a gap, not a license to guess: settle
NEEDS_INPUTnaming/guardrails-installas the recorder. A deploy command or a production URL is never invented here. For a product that has never shipped, the recorder will route to/deploy-capabilityto create the surface first - the stop names both so the operator is not sent in a circle.
A release whose deploy was not observed is deploy: UNVERIFIED in the receipt and settles
NEEDS_INPUT naming what has to run - the tag going up does not round it to done. The deploy:
and health: fields are typed receipt markers, never loop states; post-deploy.md carries the
mapping to the terminal states the release actually settles.
Distill the loop's memory
Promotion is a natural threshold - a release cycle's worth of lessons has piled up, so this is where
the loop consolidates them instead of letting rules.md only ever grow. It runs at this checkpoint,
not on a clock. Do it as a review pass, not an edit: read the lessons against the current rules and
propose a diff for the operator to confirm.
.better-dev/bin/bd-mem read learnings # the append-only lesson stream
.better-dev/bin/bd-mem read rules # the promoted rules
Read one against the other and propose, per lesson or rule, one of four moves - the reconcile verbs a memory-consolidation pass uses:
- ADD - a lesson that recurred across two or more work-items, or that a verified fix confirmed,
has earned a rule:
bd-mem remember "<rule>". The negative-lesson filter still binds - promote the durable cause-and-fix, never a transient "X is broken" (a one-off timeout, a flake, a machine-specific path). - UPDATE - a rule a later lesson refined; propose the sharper wording.
- DELETE - a rule no recall has matched across the last several work-items is stale; surface it
for the operator to retire. The sweep anchors here and carries a between-release trigger: once
the store passes its threshold,
ledger initon a new work-item nudges the operator to run this full distill pass - the lessons prune and this rule disuse sweep, never compaction alone - whileprune --applystays a release-checkpoint move. So a repo that rarely promotes accumulates neither stale lessons nor never-matched rules silently waiting for one. - NOOP - most lessons; leave them where they are.
One class of lesson leaves the learnings-and-rules plane entirely: a recurring lesson whose cause
is the shipped skill text itself - a default that misled every run it touched, a step executors
keep rationalizing around - is a library-defect candidate, not a house rule. Surface it to the
operator to carry upstream (/self-extension's recent-sessions clustering mines exactly this
shape); a local override would bury a defect every adopter still hits.
Two things keep this honest. Present the moves as a reviewable diff and light-confirm before applying
any of them - propose, never auto-edit someone's memory. And never rewrite learnings.jsonl in
place: it's append-only, so consolidation happens by promoting into rules.md and retiring stale
rules there, not by editing the lesson stream. The one sanctioned shrink of learnings.jsonl is
.better-dev/bin/bd-mem prune at exactly this checkpoint: preview first, and prune --apply only
on the operator's confirmation - nowhere else, and never unattended. Nothing recurred or went
stale? That's a clean NOOP - the pass isn't obliged to change anything.
If a release goes bad
What users already pulled is out, but the release branch itself is recoverable - so recover it forward, never by force-resetting a pushed branch.
One check comes before any revert executes: diff the revert range against the recorded migrations
glob (git diff --name-only "<prev-tag>..<release>", grepped against the safety-denylist rule).
A hit means the bad range carried a migration that already ran on production - and git revert
walks back the migration file, never the applied schema, so reverted code would run against a
schema it never saw, while the re-verify, running on the revert's own code, catches nothing. That
is a NEEDS_INPUT naming the applied-schema hazard, with three ways out for the operator to
choose: run the down migration (migrations.md carries the discipline), roll forward with a fix
instead of reverting, or restore the snapshot the migration gate receipted. Record the choice in
the release receipt (rollback-schema: down-migrated | rolled-forward | restored <ref>). A clean
diff reverts without ceremony.
Revert the commits that shipped: a fast-forward promote carried a range, so revert that range
(git revert --no-commit <prev-tag>..<release>, or git revert <bad-sha> for a single culprit);
a merge-commit promote or a hotfix merge is git revert -m 1 <merge-sha>. Re-run verification on
the revert, tag it as a new patch release, and push - a new tag forward, never a moved or deleted
one. Then back-merge the revert into integration so the two histories stay reconciled, the same
both-branches discipline a hotfix uses; skip that and the next promote silently re-ships the bad
commit. If the release sits behind a feature flag, killing the flag is the faster rollback -
record the flag's path in the release receipt so the next operator finds it without spelunking.
Hotfixes
A production hotfix branches off the release branch, not integration - and once it ships it has to
land on both branches, or the next promote silently reverts it. That both-sides discipline, the
back-merge, and the proof that the fix reached each branch live in hotfix.md; read it when an
incident needs a fix in production now. Create the branch itself through /worktree-branching (it
already bases hotfix/<slug> on main), diagnose the incident with /diagnose first - its
production path stabilizes before root-causing and writes the fix-contract the loop's entry gates
check - then drive the fix with /autonomous-loop, and run /review before it merges. This skill
owns only the promotion and back-merge shape, not the diagnosing or the fixing.
Where this sits
/review runs on the PR into integration; this skill runs after integration has soaked, to carry
it the last hop to release. When branch protection forces the promote through a PR, that PR is a
promotion PR of already-reviewed content - /review derives its verdict from the constituent recorded
verdicts rather than re-reviewing what already carries one. It never merges feature work and never touches a feature branch - its
only writes are the fast-forward of the release branch, the tag, and (for a hotfix) the back-merge
into integration.