Skip to content
Development
Skill

/craft-plugin-release

Releasing Craft CMS plugins — tagging, Packagist propagation, GitHub releases, branch promotion, shared-library ordering, history rewrites. ALWAYS load when cutting, preparing, verifying, or debugging a plugin release: bumping a version, dating a changelog, creating or moving a

From plugin
craftcms-claude-skills
8914 skills6 agents

Claude Code

$ npx -y skills add michtio/craftcms-claude-skills --skill craft-plugin-release --agent claude-code

SKILL.md

craft-plugin-release.SKILL.md
name: craft-plugin-release
description: "Releasing Craft CMS plugins — tagging, Packagist propagation, GitHub releases, branch promotion, shared-library ordering, history rewrites. ALWAYS load when cutting, preparing, verifying, or debugging a plugin release: bumping a version, dating a changelog, creating or moving a git tag, or checking what Packagist serves."
when_to_use: "Covers: composer.json version key (bump-or-omit trade-off, same-commit rule, verifying the tag's own blob), Packagist verification via repo.packagist.org/p2, the 'Skipped tag' silent failure, the tag recreation risk model, GitHub release objects drifting from tags, gh api PATCH wiping tag_name, blank bodies from empty --notes-file, the create-release.yml Store dispatch (422 already_exists, allowUpdates), shared-library releases dependency-first (additive minors breaking released consumers, module: track invalid), two-way origin comparison, filter-repo purges (already_ran re-runs, --refs/--partial, tree-hash verify, blob-level sweeps), and path repositories (canonical, no exclude, branch-alias, duplicate names). Triggers on: cut/prepare a release, tag a version, Packagist not serving the new version, 'Skipped tag', repo.packagist.org, gh release create/edit, untagged- release, prerelease latest 404, target_commitish, draft release, promote develop to main, release the library first, Invalid migration track, purge a secret from history, filter-repo, path repository, branch-alias. NOT for writing changelog entries, CI workflow YAML (craftcms quality.md), or plugin store listings."

Releasing Craft Plugins

A plugin release involves three independent systems that all claim to describe the same version: **git tags**, **Packagist**, and **GitHub release objects**. They drift from each other silently, and every observable signal can say "success" while consumers get nothing. This skill is the checklist and risk model for keeping them in agreement.

**Core rule: verify what each system actually serves, not what you pushed to it.** A `202 Accepted` from the Packagist webhook, a green tag push, and a rendered releases page each prove nothing about the other two.

Companion Skills

  • **`craft-php-guidelines`** — `references/tooling.md` for commit conventions and composer hygiene of the plugin manifest itself.
  • **`craftcms`** — `references/quality.md` for the CI workflows a release depends on (`code-analysis`, `create-release.yml`).
  • **`craft-pest`** — the suite must be green from the plugin's own root before tagging.

The release commit

If the plugin's `composer.json` carries a `version` key, **bumping it is a mandatory release step, in the same commit that dates the changelog.** A tag whose `composer.json` says a different version is silently useless: Packagist reads the manifest from the tag's own tree, sees the mismatch, and skips the tag —

Reading composer.json of acme/craft-thing (1.7.4)
Skipped tag 1.7.4, tag (1.7.4.0) does not match version (1.7.3.0) in composer.json

— while the GitHub webhook still returns `202 Accepted`. Nothing in the push, the tag, or the webhook response surfaces this. Consumers simply never see the version.

After tagging, verify against the **tag's own blob**, not the branch head:

git show 1.7.4:composer.json | grep '"version"'

The branch can be correct while the tag isn't (the bump landed one commit after the tag), and vice versa.

The `version` key: bump it or omit it

Composer's schema documentation recommends **omitting** `version` for VCS-distributed packages — the tag is the version, and an explicit key is a drift risk that `composer validate` warns about. Both choices are legitimate; know the trade-off:

  • **Omit it**: the entire class of skipped-tag failures disappears. But a version-less package loaded through a `path` repository resolves as `dev-<branch>`, and `version_compare("dev-develop-v5", "5.9.0", ">=")` is **false** — any peer-plugin version check silently fails, and the CP shows a branch name where a version should be. `extra.branch-alias` mitigates this (see `references/path-repositories.md`).
  • **Keep it**: local path-repository development gets a real, comparable version. The cost is that every release must bump it, enforced by the same-commit rule and the tag-blob check above. A release script that edits changelog date + `version` together and refuses to tag on mismatch removes the human step.

Present this as a per-plugin decision, not a doctrine — but whichever way a repo goes, it must go all the way: a `version` key that exists and doesn't get bumped is strictly worse than either consistent choice.

Verifying Packagist

**Check `https://repo.packagist.org/p2/<vendor>/<name>.json`** — the metadata endpoint Composer itself resolves from. Do **not** use `https://packagist.org/packages/<vendor>/<name>.json`; it is served from a staler pipeline and can show a version p2 doesn't have (or lack one it does).

curl -s https://repo.packagist.org/p2/acme/craft-thing.json | jq -r '.packages["acme/craft-thing"][].version'

Compare that list against `git tag --list` — every releasable tag should appear. A tag missing here with no error anywhere is the skipped-tag failure above; fetch the package's update log on packagist.org (or re-trigger the webhook and watch the response body) to see the skip reason.

The tag risk model

  • **Recreating a tag Packagist has never served is safe.** Nothing downstream has cached that version, so delete-and-repush (with the fixed manifest) is the standard remedy for a skipped tag.
  • **Moving a tag Packagist HAS served breaks that version permanently.** Composer's metadata and dist archives are cached by reference; a moved served tag means checksum mismatches and split-brain installs. Ship a new patch version instead.

That distinction is the whole model. Before touching any existing tag, check p2 for whether the version was ever served.

GitHub release objects are not tags

A GitHub *re

Read more
@michtio
Ships withcraftcms-claude-skills

Production-ready Claude Code skills, agents, and project templates for Craft CMS 5 development. Built and maintained by michtio.

8914 skills6 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
89
Star history of michtio/craftcms-claude-skills on GitHub, a chart by star-history.com

Other skills on craftcms-claude-skills.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills12d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc13d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react2d252k
@facebook@facebookInstall Skill

Questions about craft-plugin-release.

Which coding agents can use craft-plugin-release?
craft-plugin-release installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.