Codify the release process so we stop drifting version files and stop leaving the repo advertising a version with no published release (the inconsistency that plausibly got the plugin auto-removed from the store). - scripts/release.sh: one-shot, self-verifying release. Bumps manifest.json, package.json AND versions.json in lockstep, runs tests+build, pushes, tags (no v prefix), waits for CI, PUBLISHES the draft immediately, then verifies root manifest == released asset == tag. Includes a store-listing health check. - tests/version-consistency.test.ts: guard test — fails the build if the three version files ever disagree (catches a forgotten versions.json bump before tagging). - docs/PUBLISHING.md: rewritten script-first; adds versions.json (was omitted), the publish-immediately rule, and a 'if de-listed from the store' runbook (ask #plugin-dev, community.obsidian.md portal, the 'entry already exists' gotcha). - CLAUDE.md: quick-summary now points at the script and the three files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
Publishing a New Release
name: publish-release description: How to cut and publish a new OpenAugi plugin version safely, keep it consistent with the Obsidian community store, and what to do if the plugin gets de-listed. when to use: Any time you ship a new version of the plugin (features, fixes) or need to relist it in the community store.
TL;DR — use the script
# 1. Write the release notes first (they become the GitHub release body):
# docs/release-notes/X.Y.Z.md
# 2. Then run:
./scripts/release.sh X.Y.Z
release.sh does the whole thing and verifies it: checks preconditions →
runs tests + build → bumps manifest.json, package.json, and
versions.json in lockstep → commits, pushes, tags → waits for CI → publishes
the draft immediately → verifies that the repo-root manifest, the released
manifest asset, and the tag all equal X.Y.Z. It stops loudly on any mismatch.
Everything below explains why it does what it does, and the manual fallback.
The three things Obsidian actually checks
manifest.jsonat the repo-root HEAD — Obsidian reads only theversionfield here to decide "what's the latest version."- A GitHub release whose tag == that version (no
vprefix), withmain.js,manifest.json, andstyles.cssattached as assets. Obsidian downloads the files from the release, not from the repo tree. versions.json— maps each plugin version → minimum Obsidian version. Used to decide whether a given user's Obsidian is new enough to be offered the update.
If the root manifest advertises a version that has no matching published release, the plugin is in an inconsistent state — and Obsidian's automated validation can drop it from the store. Never leave that window open. (This is what bit us on 0.6.0.)
Version files — all THREE must move together
| File | Field | Notes |
|---|---|---|
manifest.json |
version |
source of truth for the release tag |
package.json |
version |
keep in sync (npm/build hygiene) |
versions.json |
add "X.Y.Z": "<minAppVersion>" |
easy to forget — it's why updates/relisting break |
The tests/version-consistency.test.ts guard test fails the build if these ever
disagree. npm test therefore catches a forgotten versions.json bump before
you tag.
Historical note: earlier versions of this doc listed only
manifest.jsonandpackage.json.versions.jsonwas missing entirely, which broke thenpm versionscript and left the store metadata incomplete. Don't regress.
Pre-release checklist
- Features/fixes merged to master;
git statusclean; local ==origin/master npm testpasses (includes the version-consistency guard) — see TESTING.mdnpm run buildclean (tsc + esbuild)- Manually tested in a real vault (
npm run dev, reload plugin, exercise the change) docs/CODEBASE_MAP.md/README.mdupdated for any behavior change- Release notes written to
docs/release-notes/X.Y.Z.md
Release notes format
Write docs/release-notes/X.Y.Z.md before releasing. Claude: diff commits since
the previous tag and focus on user-facing changes.
# X.Y.Z — <one-line theme>
## Added / Changed / Fixed / Deprecated
- <user-facing change>: <why the user cares>
## Notes
- <breaking changes or upgrade steps, if any>
Manual fallback (if you can't use the script)
Only if release.sh can't run. Do the steps in this order and don't stop
before publishing:
# 1. Bump ALL THREE (or run: npm version X.Y.Z — see note below)
# manifest.json .version, package.json .version, versions.json add "X.Y.Z":"<minAppVersion>"
npm test && npm run build # guard test must be green
git add manifest.json package.json versions.json docs/release-notes/X.Y.Z.md
git commit -m "Release X.Y.Z"
git push origin master
git tag -a X.Y.Z -m "X.Y.Z" # annotated, no 'v' prefix
git push origin X.Y.Z # triggers .github/workflows/release.yml → draft
# 2. Wait for the workflow, then PUBLISH RIGHT AWAY (don't leave it a draft):
gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')" --exit-status
gh release edit X.Y.Z --draft=false --latest --notes-file docs/release-notes/X.Y.Z.md
# 3. Verify root manifest, released asset, and tag all say X.Y.Z:
curl -sL https://raw.githubusercontent.com/bitsofchris/openaugi-obsidian-plugin/master/manifest.json | grep version
curl -sL https://github.com/bitsofchris/openaugi-obsidian-plugin/releases/download/X.Y.Z/manifest.json | grep version
npm version X.Y.Zalso works:.npmrcsetstag-version-prefix=""(so the tag has nov), and theversionnpm script runsversion-bump.mjswhich updatesmanifest.json+versions.json, while npm bumpspackage.json. It commits and tags for you — but it does not push or publish, so you still owe steps 1–3 above (push, wait, publish, verify). The script does all of it.
Community-store listing health
The plugin is listed in Obsidian's store via
obsidianmd/obsidian-releases.
That repo's community-plugins.json is now an automated mirror of Obsidian's
internal list (commits read chore: Mirror community plugins and themes), so you
can't fix listing by PR'ing that file — it gets overwritten.
Check whether we're currently listed:
curl -sL https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json | grep -i openaugi
- Listed → core Settings → Community plugins → "Check for updates" works for users.
- Not listed → core update check can't see us; users must use BRAT or a manual install. See the relisting steps below.
If the plugin gets removed from the store
This happened on 2026-07-07 (removed in mirror commit 85db6f23, bundled with
several unrelated plugins — an automated batch removal). The mirror commits carry
no per-plugin reason and Obsidian doesn't file an issue on your repo, so:
- Fix consistency first (so a resubmit passes validation): run a clean
release with
release.sh— root manifest version must equal a published release with the three assets,versions.jsonpresent,README/LICENSEpresent. (All true as of 0.6.0.) - Ask Obsidian why + request reinstatement — don't blindly resubmit an
auto-removal. Post in the Obsidian Discord
#plugin-devchannel (fastest; plugin-review team is active there) or the forum Developers: Plugin & API category. Cite: repo URL, removal date2026-07-07, mirror commit85db6f23. - If they tell you to resubmit: the current path is the web portal at community.obsidian.md → sign in with your Obsidian account → link GitHub → Plugins → New plugin → enter the repo URL. (The old "PR to community-plugins.json" flow is dead now that the file is bot-mirrored.)
- Gotcha — "An entry already exists for this repository": because the old
entry is archived rather than deleted, resubmitting the same repo URL can be
rejected. Ask a moderator in
#plugin-devto delete the archived entry, then resubmit.
Sources: Obsidian plugin submission docs; obsidian-releases repo (mirror architecture); forum thread "Cannot resubmit plugin after archiving: 'An entry already exists for this repository'".
Troubleshooting
Tag already exists / wrong version tagged
git tag -d X.Y.Z
git push origin --delete X.Y.Z # if already pushed
# fix versions, re-run ./scripts/release.sh X.Y.Z
Workflow failed — gh run view <id>, fix, delete the tag, re-run the script.
Release stuck as draft — gh release edit X.Y.Z --draft=false --latest.