Skip to main content

release

Ships a TablePro release end to end: bumps Configs/Version.xcconfig, finalizes CHANGELOG.md, commits, tags, pushes, releases plugins, and then, when the release is big enough to be worth announcing, writes the blog post, the newsletter and the X posts that point at it. Use whenever the user says "release", "bump version", "ship version", "tag a release", "cut a release", gives a version number ("/release 0.67.0", "/release plugin-oracle 1.0.0"), or asks for the announcement that goes with one: "newsletter for 0.67", "blog post for the release", "announce 0.67", "write the X thread".

Informations de source

Dépôt
TableProApp/TablePro
Dernière activité de la source
11 octobre 2026 à 03:01
Langue détectée de SKILL.md
anglais
Étoiles
6 232
Forks
416

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
6 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
release
description
Ships a TablePro release end to end: bumps Configs/Version.xcconfig, finalizes CHANGELOG.md, commits, tags, pushes, releases plugins, and then, when the release is big enough to be worth announcing, writes the blog post, the newsletter and the X posts that point at it. Use whenever the user says "release", "bump version", "ship version", "tag a release", "cut a release", gives a version number ("/release 0.67.0", "/release plugin-oracle 1.0.0"), or asks for the announcement that goes with one: "newsletter for 0.67", "blog post for the release", "announce 0.67", "write the X thread".
# Release Run the stages in order, and do not start one until the one before it is real: 1 decide the shape, 2 release (always), 3 plugins (when they changed), 4 to 6 blog post, newsletter and X posts (big releases only). A normal release stops after stage 3: the changelog and the docs changelog are its announcement. For a big release the blog post is the announcement, and the newsletter and X posts are written from the finished post, never from the changelog, so the three cannot disagree. House rules this skill relies on and does not repeat: `.claude/rules/changelog.md` (entry shape, sections, credits, lead block), `.claude/rules/plugin-system.md` (PluginKit ABI, plugin tags) and the commit format in `AGENTS.md` ("Every change", item 7). ## Stage 1: Decide the shape The release is **big** when at least one of these holds: - `### Added` has something a reader would change their behavior over: a new pane or mode, a new way to connect, a new database, a rebuilt surface they will not recognize. - Something people rely on was removed or changed under them. - A long-standing class of bugs users have asked about is gone. Otherwise it is **normal**: fixes, small changes, plugin work. The entry count per section is a sanity check, not the rule. Say which shape you picked and why before any work. Ask when it is borderline: an unwanted newsletter costs an hour, a skipped one costs a launch. ## Stage 2: Release ### Pre-flight Check each one. If any fails, stop and say what is wrong. 1. The version is semver (`X.Y.Z`, a suffix like `-beta.1` is fine), newer than `MARKETING_VERSION` in `Configs/Version.xcconfig`, and its tag is free (`git tag -l "v<version>"`). Ask if it is missing. 2. The tree is clean (`git status --porcelain`), or the user says the changes belong in the release. `[Unreleased]` has entries; if not, say the release has no notes. 3. `swiftlint lint --strict` is clean. Fix findings first, in their own commit. 4. If `Plugins/TableProPluginKit/` changed since the last tag, settle the ABI now (stage 3, PluginKit), because a breaking bump publishes plugins before the app tag. 5. Warn, without blocking, when you are not on `main`, and report the last finished full-suite verdict on `main` with its commit. `main` is often red on merge skew; the tag's own run is the gate. ```bash gh run list --workflow=macos-tests.yml --branch main --limit 30 --json conclusion,headSha,createdAt \ -q '[.[] | select(.conclusion == "success" or .conclusion == "failure")][0] | "\(.conclusion) \(.headSha[0:9]) \(.createdAt)"' ``` ### Credit the entries Do this before rewording anything: the script reads each entry's commit with `git blame`, and a line edited in the working tree blames to nothing. ```bash python3 scripts/ci/changelog_credits.py --dry-run | tail -5 python3 scripts/ci/changelog_credits.py ``` An outside contributor's entry ends `(#2905 by @digows)`, a maintainer's ends `(#2905)`. The output ends with every contributor and every entry left bare. A bare entry came from a direct push: credit it from `git log` or leave it, and never guess a handle. Blame credits the last commit to touch a line, so check the contributors against `gh pr list --state merged --search "merged:>=<last release date>" --json author` and restore any handle a later rewording took. The script is idempotent. ### Tighten the entries Entries arrive one PR at a time and drift long. Measure them without their credit, then look for the repair-story tells: ```bash awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | grep '^- ' \ | sed -E 's/ \(#[0-9, #]+( by @[A-Za-z0-9-]+)?\)$//' \ | awk '{ t+=length; n++; if (length>120) o++ } END { if (!n) { print "no entries"; exit } print n" entries, avg "int(t/n)" chars, "o+0" over 120" }' awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | grep -nE '^- .*( now | no longer |, so )' ``` Bare `length` is the line's length. Never write a dollar sign followed by a digit in this file: Claude Code replaces it with a skill argument, so `/release 0.77.0` ran `length(0.77.0)`. `instead of` alone is fine when it describes the bug. Rewrite what runs long or matches to `.claude/rules/changelog.md`, merging entries for one change and keeping every credit as it is. Diff the `#` references and handles before and after to prove none were dropped. ### Bump the version In `Configs/Version.xcconfig`, set `MARKETING_VERSION` to the new version and add 1 to `CURRENT_PROJECT_VERSION`: Sparkle compares the build number, and the release job fails if it did not rise. Touch no other version: plugins and PluginKit pin `1.0` in `project.yml`, and the iOS app has `Configs/Version-iOS.xcconfig`. ### Finalize CHANGELOG.md 1. Add `## [<version>] - <YYYY-MM-DD>` below `## [Unreleased]`, leaving Unreleased empty. In the footer, point `[Unreleased]` at `v<version>...HEAD` and add `[<version>]: https://github.com/TableProApp/TablePro/compare/v<old>...v<version>`. 2. Each `###` heading appears once, in the order Added, Changed, Deprecated, Removed, Fixed, Security. Merge a repeated heading into the first and move an out-of-order block, then check nothing was lost: ```bash awk '/^## \[<version>\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | grep '^### ' awk '/^## \[<version>\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md | grep -c '^- ' grep -n '^## \[' CHANGELOG.md | head -5 ``` 3. Write the lead block: two or three lines under the version heading, before the first `###`, naming the `### Added` items a reader would change their behavior over, in the reader's words. The update window and the Sparkle feed show only this block. A release of pure fixes can skip it. 4. Regenerate the in-app notes so **Help > What's New** matches the feed: `scripts/generate-whats-new.sh <version>`. ### Docs changelog Add an `<Update>` block at the top of `docs/changelog.mdx`, right after the frontmatter. The docs are English only, so there is no translated copy. ```mdx <Update label="v<version>" description="<Month Day, Year>"> ### New Features - **Feature Name**: what it does for the reader ### Bug Fixes - Description </Update> ``` The headings are New Features, Improvements, Removed, Security and Bug Fixes, in that order, each only when the version has one. Each line is one CHANGELOG entry written for a reader, and a New Features line may add one sentence on what the reader does with it. An outside contributor's line ends `, by [@user](https://github.com/user)`; a maintainer's carries no credit. ### Critical update flag `.github/release-flags.json` holds `criticalUpdate`. Leave it `false`: updates install in the background and apply on quit, and the flag is what lets Sparkle interrupt the user. Set it `true` only when the release fixes one of these, and say which: - Data loss or corruption of a database, saved connections or persisted tabs. - A security issue with a `### Security` entry that users of the old build are exposed to. - A crash or hang on a common path, or a failure to launch, connect or update. - A regression from the previous release with no workaround. Nothing else, a handful a year at most. Set it in the release commit and back to `false` in the next one: the release job fails when it is `true` and the file is unchanged since the previous tag. A retag of the same release never gets it. A bad build is withdrawn (below) and superseded by a corrective release with the flag. ### Commit, tag, push ```bash git add Configs/Version.xcconfig CHANGELOG.md docs/changelog.mdx .github/release-flags.json \ TablePro/Resources/WhatsNew.md scripts/check-banned-words.sh --staged git commit -m "release: v<version>" git tag -a v<version> -m "v<version>" git push origin main git push origin v<version> ``` - Keep the subject exactly `release: v<version>`: `scripts/ci/detect-changed-paths.sh` matches it to skip the duplicate test run on `main`, since the tag's run is the one that gates. - Always `-a -m`: the owner's git has `tag.gpgSign=true`, and a plain `git tag <name>` fails with "no tag message?". - Keep unrelated work out of the release commit. A lint fix made on the way goes first, as its own Conventional Commit of at most 72 characters. The tag runs `.github/workflows/build.yml`: arm64 and x86_64 builds, DMG and ZIP, Sparkle signatures, the GitHub Release with notes from `CHANGELOG.md`, then `appcast.xml`. The release is created as a draft holding every asset and published in one step, only when `lint`, `test`, `build` and `registry-readiness` pass. The job fails when the tag disagrees with `MARKETING_VERSION`. A published release is immutable: its assets and its tag are locked. When a run fails: - Before **Publish the release**, only a draft exists. Fix forward on `main` and move the tag: `git tag -f -a v<version> -m "v<version>"`, then `git push origin v<version> --force`. The next run reuses the draft. - After it, at the `appcast.xml` push for instance, use **Re-run failed jobs**. The release job finds the same files already published and carries on. **Re-run all jobs** rebuilds, and the release job refuses the new files. - A published build that is wrong ships as a new version. ### Withdraw a release Only when a build loses data, fails to launch or breaks connecting, and the fix is more than a few minutes away. Removing the version from `appcast.xml` is the only way to un-ship a Sparkle release: ```bash python3 scripts/ci/pull-release.py <version> --dry-run python3 scripts/ci/pull-release.py <version> ``` It commits and pushes to `main`. Leave the GitHub Release so direct DMG links resolve. Users who already installed the build are reached by the corrective release with `criticalUpdate` set. ## Stage 3: Plugins After the app tag is pushed, list the registry plugins with commits since their last tag: ```bash python3 .claude/skills/release/scripts/changed-plugins.py ``` Slugs, tag names and targets come from `.github/plugin-registry.json`, so nothing is mapped by hand. `own` counts commits in the plugin's source paths (its folder, plus a package or `Native/` bridge it builds from), `kit` counts PluginKit commits. Skip `bundled` rows: their changes ship inside the app, which never applies a registry update to a bundled plugin. A row with only `kit` commits needs a release only after a breaking bump, through `release-all-plugins.sh`. Show the user what changed and ask before tagging, suggesting a patch bump from the last tag. Tag each approved plugin annotated, for the same signing reason as the app. The tag is the version, so a plugin needs no version bump or changelog edit. ```bash git tag -a plugin-<slug>-v<version> -m "plugin-<slug>-v<version>" git push origin plugin-<slug>-v<version> ``` - One tag per push: GitHub sends no push event when one push carries more than three tags, so no build starts. If several went at once, run `gh workflow run build-plugin.yml -f tags="<tag>,<tag>"`. - Wait for the app build first (`gh run list --workflow build.yml --limit 1`). Five macOS jobs run at a time, and plugin builds would queue the release's own tests. - Confirm a run started for each tag: `gh run list --workflow build-plugin.yml --limit 10`. - A published plugin release cannot take a rebuilt binary, and a rebuild never matches the published bytes. A plugin run that fails after its release is published ships as the next patch version. ### PluginKit If the release touched `Plugins/TableProPluginKit/`, run `scripts/check-pluginkit-abi.sh v<previous-version>` before publishing anything, and act on the diff as `.claude/rules/plugin-system.md` says. Any diff bumps `currentPluginKitVersion`, once per release cycle. A breaking one also raises `minimumCompatiblePluginKitVersion`, and `scripts/release-all-plugins.sh <kitVersion>` must publish before the app tag: `registry-readiness` blocks the release otherwise, and a silent install would hide drivers that stopped loading. Run the bulk release for a breaking bump only: an additive bump leaves published binaries valid, and each bulk run uses up a registry retention slot for every plugin. To get one driver fix to users who have not updated, build it against their release: `scripts/release-plugin-for-shipped-app.sh plugin-<slug>-v<version> v<appVersion>`. ## Stages 4 to 6: Announce (big only) 1. **Blog post**, by `references/blog-post.md`: one file in `resources/blog/` of the marketing site repo (`../tablepro-web` by default; confirm the path). It goes live first, because the newsletter and the X posts link to it. Once it is live, link it from the version's `<Update>` block in `docs/changelog.mdx`, on its own line before the first `###`: `[Read the announcement](https://tablepro.app/blog/<slug>)`. 2. **Newsletter**, by `references/newsletter.md`: 150 to 250 words that name the headline items and link the post. 3. **X posts**, by `references/newsletter.md`: four to six posts plus one standalone, written from the finished post and newsletter. Lint each draft, then work `references/fact-checks.md` top to bottom. Its closing blocker list gates all three; report blockers separately from the drafts. ```bash python3 .claude/skills/release/scripts/lint-draft.py --blog <web repo>/resources/blog/<slug>.md python3 .claude/skills/release/scripts/lint-draft.py <newsletter-or-x-draft.md> ``` Newsletter and X drafts go in the session scratchpad, named by version (`newsletter-v0.77.0.md`, `x-posts-v0.77.0.md`). Sending is out of scope. If the user asks to send the newsletter, warn them first: in the license backend `all` means verified subscribers only, a small fraction of the list, and `everyone` is the real list.
Voir sur GitHub