Craft Plugin Release logo

Craft Plugin Release

Community
michtio
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 git tag, or checking what Packagist serves.

Overview

Publishermichtio
Repositorycraftcms-claude-skills
Skill namecraft-plugin-release
Stars
87
Forks
9
Bundled files
2
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 2 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by michtio on GitHub. Read the source before you install it.

Installation

Install the Craft Plugin Release AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/michtio/craftcms-claude-skills/tree/main/skills/craft-plugin-release
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/michtio/craftcms-claude-skills.git /tmp/craftcms-claude-skills
mkdir -p .claude/skills
cp -r /tmp/craftcms-claude-skills/skills/craft-plugin-release .claude/skills/craft-plugin-release
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Craft Plugin Release in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Craft Plugin Release on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Craft Plugin Release is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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:

bash
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).

bash
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 release is a separate object that references a tag — and the two drift independently. Packagist reads tags; humans read the releases page. A repo can be tagged several versions past its newest release object, so the releases page lies while Packagist is correct. Verify both:

bash
git ls-remote --tags origin | tail -5
gh release list --limit 5

Editing and publishing pitfalls, each observed in practice:

  • gh api -X PATCH /repos/O/R/releases/<id> with only a body field wipes tag_name — the release becomes untagged-<hash>, silently unbound from its tag. Update notes with gh release edit <tag> --notes-file <file> instead, then re-verify tag_name, draft, prerelease, and target_commitish on the release object — the edit path shares a call surface with the tag-wiping PATCH.
  • gh api repos/O/R/releases/latest returns 404 when every release is a prerelease — that endpoint excludes prereleases and drafts by design. Not a bug; use gh release list instead of chasing it.
  • Publishing a draft release mints its tag from the tip of target_commitish at publish time. Anything pushed to that branch between drafting and publishing lands in the release. Useful when intentional; a footgun when the branch moved. Pin the draft to a SHA, or verify the branch tip immediately before publishing.

Release notes: verify the body landed

gh release create <tag> --notes-file <file> with an empty file creates a release with a blank body and exits 0. If the notes were extracted by a shell pipeline that failed, the pipeline's error prints right next to the success URL and the whole thing reads like a win. Always verify:

bash
gh release view <tag> --json body -q '.body' | wc -c   # near-zero means blank

Extract the changelog section with a single regex rather than chained sed ranges, which fail obscurely and silently produce nothing:

bash
perl -0777 -ne 'print $1 if /^## 1\.7\.4 - [0-9-]+\n(.*?)(?=^## |\z)/ms' CHANGELOG.md > /tmp/notes.md
test -s /tmp/notes.md   # refuse to proceed on an empty extraction

Fix a bad body with gh release edit <tag> --notes-file, then re-verify tagName, isDraft, isPrerelease, and targetCommitish as above.

Plugin Store automation creates the release for you

Craft's standard create-release.yml workflow (triggered by a craftcms/new-release repository_dispatch from Craft Console — see the craftcms skill's quality.md) creates the GitHub release itself when a version is published through the Plugin Store. Two consequences:

  • Creating the release by hand first makes the automation fail with 422 already_exists. Setting allowUpdates: true on ncipollo/release-action makes either order safe.
  • The dispatch only fires for Store-listed plugins, so the same repo behaves differently before and after listing: pre-listing you must create releases yourself; post-listing doing so collides with the automation unless allowUpdates is set.

Branch promotion: never trust local refs

When deciding what to promote from a development branch to a release branch, stale local refs produce confidently wrong answers. Always:

bash
git fetch --prune origin
git rev-list --left-right --count origin/develop...origin/main

Compare origin against origin, in both directions. A one-way "commits behind" count cannot distinguish behind from diverged, and cannot detect an inverted pair (release branch ahead of the dev branch) — both of which occur in real estates and both of which make a naive promotion destructive.

Shared libraries: release order and runtime blast radius

When plugins share a library (an SDK package, a kit module), library releases interact with already-released consumers through their version constraints. "Additive" describes the API surface, not the runtime.

Release the dependency first, verify Packagist serves it (p2, as above), then bump and release the consumer. Tagging a consumer whose code needs an unreleased dependency guarantees red CI — and that is the mild version of what follows.

An additive minor of a library can silently break released consumers. The observed failure shape, worth checking for before any library release that touches schema:

  1. The library adds a column and starts writing to it.
  2. Consumers only reach that schema through their own migrations — the library has no migration track of its own (see below).
  3. Every released consumer's caret constraint (^1.2) admits the new minor immediately, so anyone running composer update gets new code against old schema.
  4. The library's write path wraps everything in try { … } catch (Throwable $e) { Craft::warning(…) } as "best-effort bookkeeping" — so the entire write fails on every request, silently, with one log line nobody reads.

The rules that fall out:

  • Before releasing a library that adds schema, ask what a released consumer on a caret constraint does when it resolves the new minor before running any migration. If the answer is "breaks," the change isn't additive, whatever the API diff says.
  • A best-effort catch (Throwable) around bookkeeping writes converts a schema mismatch into invisible data loss. When a library writes to a schema its consumers own the migrations for, that catch is the hazard, not the safety net.
  • Where a consumer must ship a migration to receive a library change, say so in the library's release notes. A constraint bump alone changing nothing at runtime is deeply unobvious.

Library-shipped Yii modules have no CLI migration track

craft migrate/up --track=module:<handle> fails with Invalid migration track. Craft's MigrateController resolves only craft, content, and plugin:<handle> tracks (plus an EVENT_REGISTER_MIGRATOR escape hatch that nothing wires up for you) — verified in craftcms/cms 5.10.12, src/console/controllers/MigrateController.php:467.

So a library-shipped module's migrations reach an install only when a consumer plugin runs them from its own dated migration:

php
// In the consumer plugin's dated migration
\acme\kit\Kit::getInstance()->getMigrator()->up();

— and the consumer must also bump its own schemaVersion, or that dated migration never runs. Bumping the library constraint alone changes nothing at runtime; each consumer needs a migration + schemaVersion bump of its own.

Release verification checklist

  1. Suite green from the plugin's own root; check-cs and phpstan pass.
  2. If the release needs a shared-library bump: the library is released and served by p2 first.
  3. Changelog dated and composer.json version bumped (if present) in the same commit.
  4. Tag created; git show <tag>:composer.json shows the right version.
  5. repo.packagist.org/p2/<name>.json lists the new version (allow a minute for the webhook).
  6. GitHub release object exists, tag_name matches, prerelease/draft flags correct — and the body is non-empty: gh release view <tag> --json body -q '.body' | wc -c.
  7. If promoting branches first: git fetch --prune, two-way origin...origin comparison, no unexplained divergence.

Reference Files

TaskRead
Multi-plugin local dev: path repositories, canonical semantics, branch aliases, duplicate package namesreferences/path-repositories.md
Purging content from published history: git filter-repo re-runs, --refs/--partial scoping, tree-hash verification, blob-level sweepsreferences/history-rewrites.md

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Craft Plugin Release AI skill do?

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.

Why use Craft Plugin Release on TypingMind?

Because you install it once and use it with any model. Craft Plugin Release is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Craft Plugin Release in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/michtio/craftcms-claude-skills/tree/main/skills/craft-plugin-release. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Craft Plugin Release?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Craft Plugin Release?

As many as you like. As long as a model supports skills, you can use Craft Plugin Release with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Craft Plugin Release AI skill free?

Yes. It is published on GitHub by michtio under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇