| name | plastic-releasing |
| description | Use when merging a feature branch to main and tagging a release, bumping the version, or when the user says "release", "tag", or "ship it" |
| user-invocable | true |
Releasing
Merge, bump, tag, push. Annotated tags with changelogs. Semantic versioning.
Project configuration drives the workflow - no hardcoded assumptions.
Checklist
Workflow
0. Read Project Config
Before anything else, determine which project we are releasing and load its config.
- Read
~/.plastic/projects.yml - find the project whose path matches the current working directory.
- Extract the project slug (the key under
projects:).
- Read
~/.plastic/projects/{slug}/project.yml - this contains the release: section.
Expected release: keys in project.yml:
release:
verify: "bin/rails test"
version_file: package.json
version_files:
- package.json
- .claude-plugin/plugin.json
- .claude-plugin/marketplace.json
tag_format: "v{{version}}"
on_green:
- github_release
- npm_publish
on_complete: commit_and_push
on_red: stop
Fallback: If no project.yml exists or it has no release: section, fall back to asking the user for each step - verify command, version files, tag format, and post-push actions.
1. Verify Tests Pass
Run the verification command from release.verify in project.yml:
<verify-command-from-config>
- If
release.verify is present: run it. All checks must pass before proceeding.
- If
release.verify is absent or empty: skip verification. Log that no verify command is configured.
- If
release.on_red is stop: abort the release on failure.
- If
release.on_red is fix_and_retry: ask the user to fix and re-run.
2. Determine Version Bump
| Change type | Bump | Example |
|---|
| Breaking changes | Major | 0.x.0 → 1.0.0 |
| New features | Minor | 0.3.0 → 0.4.0 |
| Bug fixes only | Patch | 0.4.0 → 0.4.1 |
Pre-1.0: minor bumps for features, patch for fixes. No major until stable.
3. Merge Feature Branch
git checkout main
git merge <branch-name> --no-ff -m "feat: merge intent [ID] - [description]"
Always --no-ff to preserve branch history in the merge commit.
Worktree-isolated intents (intent 73c3). A worktree-delivered intent's code lives on
plastic/{id}--{slug}, merged together with cleanup in step 9, not on a hand-made feature
branch. Do not delete the worktree before its branch is merged, or the work is lost. For
the full rationale and the already-merged-by-hand no-op case, read
references/promotion-and-tagging.md.
4. Bump Version
Stable-cut guard. Before touching any version file for a stable (no pre-release suffix,
latest) cut, run the guard in scripts/lib/release_guard.rb:
require "./scripts/lib/release_guard"
result = ReleaseGuard.check(
package_json: "package.json",
plugin_json: ".claude-plugin/plugin.json",
marketplace_json: ".claude-plugin/marketplace.json",
stable: true
)
raise "release guard failed: #{result.mismatches} #{result.prerelease_suffix}" unless result.ok?
If it reports a mismatch or a pre-release-suffix violation, stop and resolve it before bumping
any file. For a beta or alpha cut, pass stable: false; only version-file agreement is checked,
a pre-release suffix is expected. Read references/release-lines.md for the stable-line
guarantees this guard protects.
Determine which files to update from project.yml:
- If
release.version_files is set: update ALL listed files (they must stay in sync).
- Else if
release.version_file is set: update that single file.
- Else: ask the user which files contain the version.
Update the version string in each file, then commit:
Cut the CHANGELOG entry. Before committing, edit CHANGELOG.md at the repo root so
the changelog change rides this same version-bump commit and reaches the tag. Write one
line in the existing shape:
`<version>` - shipped <date>; collected <intent-id> (<one-line summary>)
Prepend it as the first bullet under ## Released (newest-first). If this version was
sitting under ## Unreleased, move it out of that section and into ## Released. Keep
the line intent-centric narrative (which intents the cut collected and why), NOT commit
detail: step 5's tag-message changelog and step 7's gh release create --generate-notes
already own the commit-level detail, so do not duplicate it here.
git add <version-files> CHANGELOG.md
git commit -m "chore: bump version to X.Y.Z - [one-line summary]"
5. Create Annotated Tag
Read release.tag_format from project.yml to determine the tag name:
- If set (e.g.
"v{{version}}"): replace {{version}} with the new version string.
- If not set: default to
vX.Y.Z.
Generate the changelog from commits since the last tag:
git log $(git describe --tags --abbrev=0)..HEAD --oneline --no-merges | grep -E "^[a-f0-9]+ (feat|fix|refactor):"
Create the tag with a multi-line message:
git tag -a <tag-name> -m "<tag-name> - [release name]
- [changelog bullet points from feat/fix/refactor commits]"
6. Push
git push origin main --tags
7. Post-Push Actions
Read release.on_green from project.yml. This is a list of actions to run after a successful push. Execute each in order:
github_release
Create a GitHub release from the tag:
gh release create <tag-name> --title "<tag-name> - [release name]" --latest --generate-notes --notes-start-tag <previous-tag>
--latest is REQUIRED. Pre-release (alpha/beta) tags are NOT auto-promoted to the "Latest"
badge by GitHub, so without it the Releases page keeps showing an older version as Latest while
the newest tag sits below it (a real sync drift we hit on the alpha line). Pass --latest on
every release so the newest one always carries the badge. Do NOT pass --prerelease unless you
specifically want the release hidden from Latest.
For the first release (no previous tag), write notes manually with --notes "..." instead.
npm_publish
Publish the package to npm with the appropriate dist-tag:
npm publish --access public --tag alpha
npm publish --access public --tag beta
npm publish --access public
The dist-tag is derived from the version string in package.json:
- Contains
-alpha → --tag alpha
- Contains
-beta → --tag beta
- No pre-release suffix → no
--tag flag (publishes to latest)
Other values
If on_green contains an action not listed above, log it:
[releasing] Action "<action>" is configured but not yet implemented. Skipping.
If on_green is empty or absent: skip post-push actions entirely.
Verify sync (always, after the post-push actions)
A release is not done until all three surfaces show the SAME newest version. Confirm:
npm view <package> dist-tags
gh release list --limit 1
git ls-remote --tags origin | grep <tag-name>
If the GitHub "Latest" badge is on an older tag (the common drift), fix it without re-releasing:
gh release edit <tag-name> --latest
8. Clean Up the Intent's Worktrees (merge-then-remove)
This step now runs BEFORE step 9's end-intent call (intent 188, D7): scripts/end-intent
gained its own step 5 that disarms (releases the worktree, clears delivery.lock) as part
of every close. Its plain-remove shape does not merge, so if end-intent ran first on a
release, its step 5 would remove the worktree WITHOUT merging the code branch first,
stranding the integrated work (Worktree.finish returns early once the worktree block it
needs is gone, per worktree.rb's own "no-op if nothing was provisioned" contract).
Running this merge-then-remove step first means the worktree is already gone by the time
step 9 runs, so end-intent's own disarm becomes a harmless no-op for the worktree
(nothing left to remove), while for the FIRST time on this path it also clears the delivery
lock correctly (G5): before intent 188 this path left the lock stranded, exactly the class
of bug closed by the End-tail enforcement work.
This is the release branch of plastic-intent-ending's Step 5 disarm (merge: true), not a
separate concern: a release is the merge-then-remove path for the intent's worktree (intent
73c3), so the intent's code branch is merged back into the default branch BEFORE the worktree
is removed. Drive it through Worktree.finish with merge: true, which merges the code
branch, then removes the worktree, prunes the repo, and clears the worktree block from the
bridge:
ruby -r ~/.plastic/scripts/lib/worktree -r ~/.plastic/scripts/lib/bridge -e \
'b = Bridge.discover_bridge(session: ENV["CLAUDE_CODE_SESSION_ID"], cwd: Dir.pwd); Worktree.finish(b, merge: true) if b'
(Uses discover_bridge, not a bare session-keyed Bridge.read, because a session can own more
than one live bridge now (intent 131) and discover_bridge resolves the right one for this cwd.)
Honor the worktree-cleanup rule: never leave an orphaned worktree, and run git worktree prune in the affected repo if you hit a stale reference. For why this is the one place the
merge-vs-remove policy lands on merge, and the fail-open/idempotent guarantees of finish,
read references/promotion-and-tagging.md.
9. Complete Active Intent
A release IS a delivery. The active intent that drove this work must be completed as part of the release process. This is NOT optional. The mechanical close (outcome/INDEX/savepoint/commit, AND disarm since intent 188) is plastic-intent-ending's job, not this skill's: run its backing script rather than restating that prose here.
- Read
~/.plastic/INDEX.md (or the project's INDEX.md) - find active intent(s) related to this release.
- For each active intent being delivered:
a. Write a real
outcome.md (never leave the scaffold placeholder), disposition: delivered, referencing the release tag.
b. Update ## Insights with final observations.
c. Run the mechanical close (scripts/end-intent's steps 1-5): this stamps the intent file's ## Outcome summary, moves the INDEX.md line to ## Completed (dated today, with a rich entry description via --index-note), appends the savepoint Done bookend, commits the store, and disarms (releases the worktree - already gone from step 8 above - and clears delivery.lock), all in one call:
ruby ~/.plastic/scripts/end-intent --store <store_path> --id <ID> --disposition delivered \
--session "$CLAUDE_CODE_SESSION_ID" \
--outcome-summary "delivered in <tag-name>: <one-line summary>" \
--index-note "<tag-name>, <mode/tier>; <what shipped>; <suite result>"
A non-zero exit needs attention: 4 means a live foreign session holds the lock (back
off), 5 means the code worktree is still dirty (should not happen here, since step 8
already removed it; investigate before overriding with --discard-worktree-changes),
3 means disarm ran but the lock is still present (run /plastic-doctor check the lock status).
d. Update clusters to show _(completed)_ (the store-curating skill's job on its next pass).
If no active intent exists for this release, that itself is a problem - work happened outside the intent system. Log it and move on, but flag it.
Release lines and channels
Two lanes get code to a release, on top of the workflow above.
- Default lane. Branch, merge to main, cut stable, publish to npm
latest. This is the
workflow in the steps above, unchanged. Use it for additive, suite-verifiable,
low-blast-radius work.
- Beta-verified lane. Branch, merge to
beta, publish to the npm beta channel, verify in
real use, then merge to main and cut stable. Use it for work that changes operational
substrate, or carries data, migration, lock, or state-format risk, or that a hermetic suite
cannot fully validate on its own.
Stable-line guarantees. An external latest user can rely on:
main is always green and releasable; no pending revert awaiting re-land sits on main.
- A stable release carries no pre-release suffix, publishes to
latest, and the newest release
always carries the GitHub "Latest" badge.
- The three version files always agree, checked by
scripts/lib/release_guard.rb (see Bump
Version above).
- A stable cut collects only intents that cleared their lane's verification bar.
- Channel semantics are fixed:
latest is stable, beta is the verification line, alpha is
experimental.
Read references/release-lines.md for the full lane-routing detail, the version-line map, and
the intent-41 re-land playbook.
Conventions
- Annotated tags only -
git tag -a, never lightweight tags
- Tag format - driven by
release.tag_format in project.yml (default: vX.Y.Z)
- Tag message - first line:
<tag> - [short name], then blank line, then bullet changelog
- Commit prefixes -
feat:, fix:, refactor:, chore:, docs: (conventional commits)
- Hyphens, never em-dashes - in tag names, release titles, and commit messages, use a hyphen (
-). Never an em-dash.
- Latest badge - always
gh release create --latest; the newest release must carry GitHub's "Latest" badge.
- Version files - driven by project.yml; list and bump EVERY file carrying the version (they drift otherwise)
- Verify sync - after pushing, confirm npm dist-tag, GitHub "Latest", and the git tag all show the new version
- Branch cleanup - delete merged feature branches:
git branch -d <branch>
References
- Read
references/release-lines.md for the two release lanes, the stable-line guarantees,
the version-line map, and the intent-41 re-land playbook before starting any release
- When promoting a pre-release across channels (
--promote beta/--promote stable) or
tagging a historical release retroactively, read references/promotion-and-tagging.md
for the exact commands and rules first
- Read
references/deprecations.md for the full deprecation process, severity levels, deprecations.yml schema, and dismissal rules when adding or managing deprecations