| name | cmb2-release |
| description | Cuts a release of the CMB2 WordPress plugin. Walks through the full ordered checklist from develop branch hygiene to the manual wp.org SVN deploy. |
| when_to_use | Use when releasing, publishing, shipping, tagging, or bumping the version of CMB2 —
phrases like "release CMB2", "cut a CMB2 release", "ship CMB2 2.x.x", "bump CMB2
version", "publish CMB2 to wordpress.org", "tag CMB2", "deploy CMB2 to wp.org".
Also when in a CMB2 checkout and asked "how do I release this?" — there's no
RELEASE.md in the repo, so this skill is the canonical documentation.
|
| disable-model-invocation | true |
| argument-hint | [version] |
| allowed-tools | ["Bash(git status*)","Bash(git diff*)","Bash(git log*)","Bash(git describe*)","Bash(git branch*)","Bash(git add*)","Bash(git commit*)","Bash(git checkout*)","Bash(git pull*)","Bash(git fetch*)","Bash(git merge*)","Bash(git tag*)","Bash(git show*)","Bash(git rev-parse*)","Bash(gh run*)","Bash(gh release*)","Bash(gh workflow*)","Bash(npm install*)","Bash(npm run*)","Bash(composer install*)","Bash(vendor/bin/*)","Bash(grep*)","Bash(sed*)","Bash(awk*)","Bash(tr*)","Bash(echo*)","Bash(curl*)","Bash(bash*)","Read","Edit","Write","TodoWrite"] |
Cutting a CMB2 Release
CMB2 has no RELEASE.md and no automated wp.org deploy. This skill is the canonical process — reconstructed from how v2.10.0 and v2.11.0 were cut. Follow it in order; skipping a step has historically caused broken {{next}} placeholders to ship, mismatched bootstrap class names, or wp.org being out of sync with GitHub.
If a version was passed as an argument ($ARGUMENTS), use it as NEW in the "Lock in the values" step. Otherwise, propose a version from the commit log shown in the snapshot below.
Repo snapshot
!bash ${CLAUDE_SKILL_DIR}/scripts/snapshot.sh
How to use this skill
Track progress with TodoWrite (one todo per top-level step). The release has hard-to-reverse actions (git push, svn ci) — at every 🛑 STOP-AND-VALIDATE checkpoint, show the user the diff/output and wait for explicit confirmation before continuing. Releases are infrequent enough that "ask twice" is cheaper than "untag and republish."
Why these steps look the way they do
A few things are non-obvious and easy to get wrong:
CMB2_Bootstrap_XXXX class name in init.php encodes the version (e.g. CMB2_Bootstrap_2110 for 2.11.0, derived by stripping dots). It's how multiple bundled copies of CMB2 in different plugins/themes negotiate which one wins. If you forget to rename it, two copies of the new version will collide instead of deduping.
_Develop suffix on the bootstrap class between releases. Develop branch always carries CMB2_Bootstrap_<NEXT>_Develop — a distinct class name from any released CMB2_Bootstrap_<N> so the two can coexist when both are bundled in different plugins/themes. The release flips _Develop off; the post-release bump (Step 9) flips it back on for the next planned version. (See the PRIORITY note below for the load-order trade-off.)
const PRIORITY decrements by 1 each release (9958 → 9957 → 9956…). Newer versions need to load before older ones (lower priority = earlier hook) so the bootstrap can pick the highest version. The decrement happens in the release commit (Step 1). The post-release develop bump (Step 9) does not touch PRIORITY — develop ends up at the same priority as the just-released version, and a plugin embedding develop CMB2 alongside another plugin embedding the released CMB2 will tie-break by plugin load order. This is a known trade-off matching 5 of the last 7 CMB2 releases; if it ever becomes a real problem, decrement PRIORITY in Step 9 too.
{{next}} placeholder is sprinkled in @since docblocks during development so contributors don't have to guess the next version. The release is the moment those get resolved. If you ship with {{next}} still in the source, IDEs and docs tools will show literal {{next}} to users.
- No wp.org automation exists. GitHub tags do not deploy. You must SVN by hand or the wp.org listing will silently stay on the old version while GitHub shows the new one — confusing for users.
Pre-flight
git checkout develop && git pull --rebase
The snapshot above already shows working tree state, CI status, and commits since the last tag — review it instead of re-running those commands.
Determine the new version (semver)
If $ARGUMENTS was provided, use that. Otherwise classify the commits-since-last-tag from the snapshot:
- MAJOR: Breaking API removals, raised PHP minimum, removed field types/parameters
- MINOR: New field types, new field parameters, new filters/actions, new public methods
- PATCH: Bug fixes, internal refactors, doc tweaks
Present the recommendation and let the user confirm or override. CMB2 trends conservative — when in doubt between MINOR and PATCH, prefer MINOR if any new public API surface was added.
Pre-release checks (all must pass)
npm install
composer install
vendor/bin/phpunit
vendor/bin/phpcs
npm run build:js:lint
npm run build
If any fail, stop. Don't release on red.
Lock in the values
These are real shell variables. Set them once and reference them as $NEW, $OLD, etc. throughout the rest of the skill.
OLD is what the version strings in the files currently say — read it straight from init.php (same source as OLDBOOT/OLDPRIO), not from git tags. The last git tag can disagree with the files: tags carry a v prefix the files don't, and on a diverged develop git describe returns the latest tag reachable from HEAD, which may lag the actual latest release. Matching the files is what makes the Step 1 seds land.
export NEW=2.X.Y
export OLD=$(grep -oE "const VERSION = '[^']+'" init.php | grep -oE "[0-9][0-9.]*")
export NEWBOOT=$(echo "$NEW" | tr -d .)
export OLDBOOT=$(grep -oE "CMB2_Bootstrap_[0-9]+(_Develop)?" init.php | head -1 | sed 's/CMB2_Bootstrap_//')
export OLDPRIO=$(grep -oE "PRIORITY = [0-9]+" init.php | grep -oE "[0-9]+")
export NEWPRIO=$((OLDPRIO - 1))
echo "NEW=$NEW OLD=$OLD NEWBOOT=$NEWBOOT OLDBOOT=$OLDBOOT OLDPRIO=$OLDPRIO NEWPRIO=$NEWPRIO"
Eyeball the echo before continuing. Empty values mean a grep didn't match — investigate before moving on. $OLD should match what init.php, package.json, and the CSS banners currently contain.
Step 1 — Version bump commit on develop
This is a single commit titled like Update changelog/readme/versions (matches prior history). Touch every file below; do not split.
init.php
sed -i.bak \
-e "s/Version: $OLD/Version: $NEW/" \
-e "s/CMB2_Bootstrap_$OLDBOOT/CMB2_Bootstrap_$NEWBOOT/g" \
-e "s/const VERSION = '$OLD';/const VERSION = '$NEW';/" \
-e "s/const PRIORITY = $OLDPRIO;/const PRIORITY = $NEWPRIO;/" \
init.php
rm init.php.bak
grep -nE "Version:|Bootstrap_|VERSION|PRIORITY" init.php
The bootstrap class name should appear ~6 times — all should now read CMB2_Bootstrap_$NEWBOOT.
package.json + package-lock.json
sed -i.bak "s/\"version\": \"$OLD\"/\"version\": \"$NEW\"/" package.json package-lock.json
rm package.json.bak package-lock.json.bak
grep "\"version\":" package.json package-lock.json
The lockfile has version in two places (top-level and under packages."") — the single sed hits both.
readme.txt
Tested up to: <CURRENT_WP> — use the WordPress version shown in the snapshot at the top of this skill.
Stable tag: $NEW
- Add a new changelog section under
== Changelog == matching the format below.
CHANGELOG.md
CMB2 has an ## Unreleased placeholder at the top. In theory contributors append bullets to it during development; in practice it's often empty (just *). Check it first — if it's empty or sparse, reconstruct the entries from the commit log since the last release tag (v$OLD — the bare file version always corresponds to the last released tag):
git log "v$OLD"..HEAD --oneline
Read the actual PRs/commits, group them into Enhancements / Bug Fixes, and write user-facing bullets — don't just paste commit subjects. Then rename the ## Unreleased header to the new version section and add a fresh empty ## Unreleased above it for the next cycle.
Format (verify against existing entries before writing):
## [$NEW - YYYY-MM-DD](https://github.com/CMB2/CMB2/releases/tag/v$NEW)
### Enhancements
* User-facing or developer-facing improvement. Props [@author](https://github.com/author) ([#1234](https://github.com/CMB2/CMB2/pull/1234)).
* [Development] Internal tooling / CI / build changes get this prefix.
### Bug Fixes
* Fixed thing. Fixes [#5678](https://github.com/CMB2/CMB2/issues/5678).
So the top of the file ends up looking like:
## Unreleased
*
## [$NEW - YYYY-MM-DD]...
Tone is contributor-facing — link PRs/issues, credit authors with Props, prefix internal tooling work with [Development]. Look at the 2.11.0 entry for the canonical example.
readme.txt (changelog section)
Mirrors CHANGELOG.md but one heading level deeper. Prepend the new entry above the previous one:
### $NEW
#### Enhancements
* (same bullets as CHANGELOG.md)
#### Bug Fixes
* (same bullets as CHANGELOG.md)
⚠️ wp.org caps the == Changelog == section at 5,000 words and silently truncates the overflow. Unlike CHANGELOG.md (which keeps the full history forever), the readme changelog must not accumulate unbounded history — past it, wp.org cuts the display and shows an author-only warning on the plugin page: "The Changelog section is too long and was truncated. A maximum of 5,000 words is supported." After adding the new entry, measure the section and trim if needed:
awk '/^== Changelog ==/{f=1;next} f&&/^== /{exit} f' readme.txt | wc -w
If it's near or over ~4,500 words (leave margin under the 5,000 cap), delete the oldest version entries and replace them with a pointer to the full history:
For the changelog of versions prior to <oldest-kept-version>, see
https://github.com/CMB2/CMB2/blob/master/CHANGELOG.md
Nothing is lost — the complete history lives in CHANGELOG.md and GitHub Releases. Keeping the most recent ~10–15 versions is plenty. Apply the same trim to a == Upgrade Notice == block if it has grown unbounded too. (This bit 2.12.0: the section had reached ~9,000 words and wp.org truncated it. The trim only needs to happen once when it crosses the cap, then occasionally as it re-accumulates.)
Because wp.org reads this section from trunk/readme.txt to render the plugin page, a trim only takes effect after the Step 5 SVN deploy — trimming readme.txt here in the version-bump commit is what gets it there.
== Upgrade Notice == (usually nothing to do)
readme.txt has no == Upgrade Notice == section — it was removed deliberately. It had been misused as a second, stale copy of the changelog (wrong ### x.y.z headings, full multi-bullet entries), so wp.org wasn't surfacing it as real upgrade notices anyway. Default: leave it absent. Most CMB2 releases don't need one.
Only add it back for a release where users genuinely need a "update now" nudge (security fix, breaking change). When you do, follow the wp.org format exactly — it is not a changelog mirror:
== Upgrade Notice ==
= $NEW =
One or two sentences, ≤300 chars, on why to upgrade. No bullet lists.
- Version keys are
= x.y.z = (single-equals), not ### x.y.z — wp.org matches the notice to the offered version by parsing those keys, and shows it in the wp-admin update prompt.
- Keep only the most recent one or two notices; this section is not a running history (that's the Changelog +
CHANGELOG.md).
README.md
README.md carries the same header block as readme.txt (Requires at least, Requires PHP, Tested up to, Stable tag) plus badges. Keep it in sync with the readme.txt edits above — don't just $OLD-bump:
**Tested up to:** <CURRENT_WP> — match readme.txt (an $OLD search will miss this; it's the WP version, not the plugin version).
**Stable tag:** $NEW
- Search for
$OLD and bump any remaining references (badges, "current version" mentions).
README.md has no inline changelog (it just links to CHANGELOG.md), so the 5,000-word trim above does not apply here.
css/*.css
Regenerate banners — do not hand-edit:
npm install
npm run build:css:banner
grep -l "Version: $OLD" css/*.css
Commit
🛑 STOP-AND-VALIDATE: Run git status and git diff --cached. Show the user the staged diff and confirm before committing. Spot-check: Bootstrap_$NEWBOOT appears in init.php ~6 times all updated, PRIORITY = $NEWPRIO is exactly one less than $OLDPRIO, readme.txt "Tested up to" matches current WP, and the readme.txt == Changelog == section is under the 5,000-word wp.org cap (see the trim note above).
git add init.php package.json package-lock.json readme.txt CHANGELOG.md README.md css/
git commit -m "Update changelog/readme/versions"
Step 2 — Rebuild i18n (separate commit)
npm run build:i18n
git add languages/
git commit -m "update i18n files"
This regenerates languages/cmb2.pot (and .mo files if msgfmt is on PATH). Kept as a separate commit historically because the pot diff is noisy.
Step 3 — Resolve {{next}} placeholders
The snapshot at the top showed which files have {{next}}. For each hit, replace {{next}} with $NEW in @since docblocks:
grep -rl "{{next}}" --include="*.php" includes/ init.php | xargs sed -i.bak "s/{{next}}/$NEW/g"
find includes/ init.php -name "*.bak" -delete
grep -rn "{{next}}" --include="*.php" .
git add -p
git commit -m "Replace next tag placeholders with $NEW"
Step 4 — Tag on develop, fast-forward master
Per CMB2 history, the release-prep commits live on develop and master is fast-forwarded to the tagged commit. There is no merge commit — every release tag in the history sits on a develop commit, with master pointing at the same SHA.
You're still on develop with Steps 1–3 committed. Tag the latest commit:
git tag -a "v$NEW" -m "v$NEW"
🛑 STOP-AND-VALIDATE: Show git log --oneline -5 and git show v$NEW --stat. Confirm the tag points to the right commit before pushing. Pushing a tag is hard to undo cleanly.
git push origin develop
git push origin "v$NEW"
git checkout master && git pull --rebase
git merge --ff-only "v$NEW"
git push origin master
git checkout develop
If --ff-only fails, master has commits develop doesn't — investigate before forcing anything.
Per the project's CLAUDE.md "Landing the Plane" rule: work is not done until git push succeeds.
Step 5 — wordpress.org SVN deploy
This step is fully manual (no GitHub Action exists) and the most error-prone part of the release. Follow references/svn-deploy.md — it has the exact rsync exclude list, the SVN tag-copy steps, the STOP-AND-VALIDATE gate before svn ci, and the post-deploy verification.
Step 6 — GitHub Release
Pull the changelog body straight from CHANGELOG.md so the GitHub release page matches what users see in readme.txt:
NOTES=$(awk -v v="## [$NEW" 'index($0, v)==1{flag=1; next} /^## /{flag=0} flag' CHANGELOG.md)
gh release create "v$NEW" --title "v$NEW" --notes "$NOTES" --target master
gh release view "v$NEW" --web
The wiki (wiki-cmb2/Notable-Changes-in-CMB2.md) is not auto-updated — update by hand if the release introduces user-facing changes worth highlighting.
Step 7 — Refresh cmb2.io docs
cmb2.io is a static VitePress site built from the CMB2/Wiki repo. Its index, changelog, and contributing pages are pulled from this repo's README.md, CHANGELOG.md, and CONTRIBUTING.md — they do not update on the live docs site on their own. Trigger the "Refresh upstream docs" workflow in CMB2/Wiki, which re-fetches those three files from CMB2/CMB2@develop, re-applies the build-safe transforms, commits, and auto-deploys cmb2.io:
gh workflow run refresh-upstream.yml -R CMB2/Wiki
This keeps cmb2.io's README/changelog/contributing in sync with the release. The workflow reads from develop, so it only reflects the release once Step 4 has pushed the version-bump commit (by the time the GitHub release in Step 6 is published, everything user-facing is final). Requires gh to be authenticated with access to the CMB2/Wiki repo; the dispatch is fire-and-forget — watch it with gh run list -R CMB2/Wiki --workflow=refresh-upstream.yml if you want to confirm the deploy.
Step 8 — Verify the plane has landed
git fetch origin
for branch in develop master; do
local=$(git rev-parse "$branch")
remote=$(git rev-parse "origin/$branch")
[ "$local" = "$remote" ] && echo "$branch: in sync" || echo "$branch: OUT OF SYNC"
done
git tag --contains HEAD
gh release view "v$NEW"
curl -sI "https://downloads.wordpress.org/plugin/cmb2.$NEW.zip" | head -1
If any of those fail, the release isn't done.
Step 9 — Post-release develop bump
Once the release is shipped, develop needs to be re-flagged for the next version. The pattern (used since 2.8.0): one commit on develop titled exactly Add develop suffix to init class that touches init.php only and renames the bootstrap class from CMB2_Bootstrap_$NEWBOOT → CMB2_Bootstrap_<NEXT>_Develop.
This keeps develop's bootstrap class name distinct from any released CMB2_Bootstrap_<N> so they coexist cleanly when both are bundled in the same WP install. (PRIORITY stays at the just-released value — see the trade-off note in "Why these steps look the way they do.")
Skip this step for patch releases. History (v2.10.1) shows patches don't get a develop bump — the next minor's bump rolls it in. If the user is cutting a patch, ask whether to skip; otherwise default to skipping.
For minor/major releases, ask the user what the next planned version is. Usually the next minor (e.g. after 2.11.0 → 2.12.0). Note that historically the bump skips ahead to the next minor even when a patch might come next (after 2.11.0, develop went straight to 2120_Develop, not 2111_Develop).
export NEXT=2.X.Y
export NEXTBOOT=$(echo "$NEXT" | tr -d .)
sed -i.bak \
"s/CMB2_Bootstrap_$NEWBOOT/CMB2_Bootstrap_${NEXTBOOT}_Develop/g" \
init.php
rm init.php.bak
git diff init.php
The diff should show ~6 occurrences of the class rename and nothing else.
🛑 STOP-AND-VALIDATE: confirm only init.php is touched, only the class name changed, and const VERSION / const PRIORITY / Version: header are unchanged.
git add init.php
git commit -m "Add develop suffix to init class"
git push origin develop
Known gaps & follow-ups
These aren't blockers but are worth raising with the user once shipped:
- No automated wp.org SVN deploy. Adding
10up/action-wordpress-plugin-deploy (or similar) as a release.yml workflow triggered on tag push would eliminate Step 5 entirely. It's been a long-standing gap; offer to file a bd issue for it.
- No release script. Steps 1–4 are mechanical and could be a
bin/release.sh. The bootstrap class rename + priority decrement are the only non-trivial parts.
{{next}} is fragile. If a contributor writes @since 2.12.0 directly in a PR before 2.12.0 is cut, the placeholder grep won't catch it. A pre-release bd preflight check could grep for @since referencing unreleased versions.
Mention these once after Step 8 verification completes.