| name | draft-release |
| description | Cut a new release of this Spicetify extension — work out the version bump from what has merged since the last tag, write the release notes in this repo's house style, commit the version bump, then tag and draft the GitHub release. Use when the user wants to release, cut a version, bump the version, or draft release notes. |
Drafting a release
The thing users actually install is hidePodcasts.js committed at the repo root on
main. push.yml rebuilds it on every push to main and auto-commits it as "Built
new version", and the README tells people to copy that file into their Extensions
folder; Marketplace reads manifest.json from main too. So main is released the
moment anything merges — the tag and the GitHub release are documentation written
after the fact, not the mechanism.
There is no dist branch, despite push.yml being titled "Push new version to dist
branch". Do not go looking for one.
One practical consequence: there is no "unrelease". If the notes are wrong you edit
them; if the code is wrong you ship another commit.
The shape of it
| |
|---|
| 1 | Gather the facts |
| 2 | Propose the version, confirm with the user |
| 3 | Draft the notes, show them to the user |
| 4 | Commit the version bump, ask before pushing to main |
| 5 | Verify the committed bundle matches main |
| 6 | Tag vX.Y.Z |
| 7 | gh release create --draft |
| — | The user publishes |
One hard stop: never publish the release. That is the user's call. Step 4 pushes
straight to main, so confirm before doing it.
1. Gather the facts
./.claude/skills/draft-release/gather.sh
Read-only, safe to run any time. It prints the last tag, every commit and PR since
it, each PR's author and labels, detected new contributors, and the
Spotify/Spicetify versions from the running client.
If it warns that package.json disagrees with the last tag, stop and work out
why before bumping — usually it means a bump was committed but never tagged, and
the fix is to tag the existing commit rather than bump again.
2. Propose the version
Semver against what the PRs actually do, not against their fix:/feat: prefixes:
- minor — any new user-facing capability. 3.2.0 was a minor for hiding podcasts
and audiobooks in search results.
- patch — fixes, dependency bumps, internals only. 3.1.1–3.1.4 were all patches.
- major — a breaking change to how the extension is configured or installed.
3.0.0 was the last one. Ask before assuming it.
A Spotify-breakage fix is a patch even though it is the whole reason for the
release — that is what 3.1.2 and 3.1.3 were.
Say which PRs drove the call, then confirm the number with the user before touching
anything.
3. Draft the notes
This is the part that takes judgement — everything else is mechanical. Read the
actual PRs. Do not just reformat PR titles.
The current format is hand-written prose sections, with GitHub's generated list
appended below a rule. gh release view v3.2.0 is the model:
Spotify 1.2.94 renamed and restructured several things the extension relied on, and
podcasts had started reappearing on the search pages. This release restores that, and
extends hiding to search results.
## Fixed
* **Podcast and audiobook category cards on the Search page were showing again.** Spotify
replaced the class those rules targeted with a generated one, so they silently stopped
matching. They're now matched on URL, which has stayed stable across releases.
## Added
* **Podcasts and episodes are now hidden in search results**, not just the filter pill.
## Internal
* Toolchain modernized: ESLint 10 (flat config), pnpm 11, Node 24.
## Known limitations
Home page shelves are unchanged in this release. If podcasts still appear there, try
**aggressive mode** in the profile menu, and please include your shelf IDs in an issue.
---
## What's Changed
...generated by GitHub...
Note this is a deliberate step up from 3.1.0–3.1.4, which shipped the bare generated
list with no prose at all. Do not copy that older pattern from gh release view; the
shape above is the current convention.
The rules that actually matter:
An unheaded opening paragraph saying what the release is for. A reader deciding
whether to update should need nothing else. Where the cause is Spotify's doing rather
than ours, say so — it explains why a working extension broke on its own.
Group under ## Fixed / ## Added / ## Internal, and drop any section with
nothing in it. ## Known limitations is worth keeping whenever something obvious is
still broken — it heads off duplicate issues, which is what it did for home shelves
in 3.2.0.
Bullets are per user-visible symptom, not per PR. #208 produced several bullets
in 3.2.0 because it fixed several distinct symptoms. One PR can be several bullets;
several PRs can collapse into one.
Lead each bullet with a bold sentence naming the symptom, then explain the cause.
"Nothing was being hidden on either search page. The extension waits for a page to
finish loading, and the markers it waited on no longer exist" — the reader knows the
extension, not the codebase.
## Internal gets one rolled-up line, not a bullet per chore PR. Dependabot bumps
collapse into it, or vanish entirely if that is the whole release. Do not give the
version bump itself a line.
Do not hand-write per-bullet by @author in <url> credits. GitHub's generated
## What's Changed section already credits every PR and its author, including
Dependabot, and duplicating it in the prose is what 3.2.0 avoided. Mention a
contributor by name in the prose only when the story warrants it.
There is no standing "Tested against" line — 3.2.0 cites the Spotify version
inline in the sentence about what broke. gather.sh prints the running client's
versions for you to work in where relevant.
Because the hiding rules depend on Spotify's DOM, a release that fixes breakage is
worth verifying in a live client before the notes claim it is fixed. That is the
spicetify-drive skill — and note its hard rule: never play a podcast or
audiobook while testing.
Show the user the draft and take edits before committing anything.
4. Commit the version bump
Only package.json changes. manifest.json carries no version, and
hidePodcasts.js is regenerated by CI.
Bumps land as a direct commit to main whose entire message is the version
number — 3.2.0, 3.1.4, 3.1.3. Not a chore: prefix, not a PR:
git checkout main && git pull
git commit -am "X.Y.Z"
Ask before pushing — this goes straight to main with no review, and pushing is
what makes it a release.
git push origin main
Then wait for push.yml to finish before step 5.
5. Verify the committed bundle is current
The tag should point at something users can already install.
./.claude/skills/draft-release/verify-build.sh
It ends in a single PASS — safe to tag. / FAIL — do not tag… verdict, matching
its exit code. Read that line rather than the section above it: the content check can
legitimately print "the committed bundle matches" while an earlier check has already
failed, so a mid-script pass is not an all-clear.
Do not judge this by whether a "Built new version" commit followed the bump.
push.yml ends in git-auto-commit-action, which only commits when the built output
changes. The version is not embedded in the bundle, so a bump changes nothing and
the workflow succeeds while committing nothing. v3.2.0 is exactly this: 8546bb7 is
a lone one-line package.json change with no rebuild after it, and the bundle was
correct the whole time.
So the script checks content: it confirms a successful push.yml run for main's
current SHA, rebuilds out-of-tree with spicetify-creator --minify, and diffs
against the artifact committed on origin/main. The build is byte-reproducible, so a
match is exact and any difference is a real problem — stop and work out why before
tagging.
It builds to a temp directory on purpose. pnpm build:local writes to the repo root
and would overwrite the tracked artifact you are trying to compare against.
6. Tag
Tag what users can actually install — the commit whose build is on main. Usually
that is the version bump, because the bump is usually the last thing to land.
It is not a rule. If anything merged after the bump, tag that instead:
git log --oneline -5 origin/main
Recent history has both: v3.1.4 and v3.2.0 sit on their bump commits, while v3.1.2
and v3.1.3 sit on the "Built new version" commit that followed.
git fetch origin
git tag vX.Y.Z <sha>
git push origin vX.Y.Z
Plain lightweight tags. v prefix on the tag — v3.1.0 through v3.2.0 all carry
it. Older tags (3.0.0, 2.8.0) do not, and v.2.7.2 has a stray dot; ignore both,
gather.sh already handles them.
7. Create the draft
The release title is the bare version, without the v — tag v3.2.0, title
3.2.0. gh defaults the title to the tag, so pass --title explicitly or you will
publish a v3.2.0-titled release that breaks the pattern.
Write the prose to notes.md in the scratchpad, not the repo — this project keeps no
CHANGELOG, and the release body is the only changelog there is.
gh release create vX.Y.Z --draft --title "X.Y.Z" \
--notes "$(cat notes.md)" --generate-notes
--generate-notes appends GitHub's ## What's Changed, ## New Contributors and
**Full Changelog** below your prose, which is how 3.2.0's body was built. Use
--notes, not --notes-file: gh documents the prepending behaviour for --notes
only.
No release assets. Unlike the sibling name-that-tune repo, no release here has
ever carried a zip — 3.0.0 through 3.2.0 all have none. Users copy hidePodcasts.js
from main or install through Marketplace. Do not invent one.
Confirm the body came out right before handing over:
gh release view vX.Y.Z --json isDraft,name,body --jq '{isDraft, name, hasGenerated: (.body | contains("What'"'"'s Changed"))}'
If hasGenerated is false, the flags did not combine — recreate with the prose and
gh api-generated notes pasted together by hand.
Give the user the URL and let them publish.
Gotchas
gather.sh reads origin/main, not your local main. It fetches first, so a
stale local checkout will not silently produce short notes.
- "Built new version" commits are noise in the commit range. They are CI
committing the bundle; they never get a bullet.
- Version bump commits land in the next release's commit range — a bare
3.2.0 message with no PR number — and must be excluded from its notes.
- New-contributor detection assumes lowest PR number = first contribution. True
for this repo's history; it would misfire on someone whose first PR was closed and
reopened.
- Squash merges renumber history. Confirm the squashed content matches
(
git diff <old-commit> origin/main should be empty) and then rebase, rather than
resolving by hand.