obsidian-plugin-release
Releasing an Obsidian plugin has 12 manual steps that are easy to half-do (forget versions.json, push tag before main, sign assets without provenance). This skill collapses the flow into a single intent: pick a version → ship.
Detection
The current directory is an Obsidian plugin if all of these exist at the root:
manifest.jsonwithid,version,minAppVersionkeysversions.json(the mappluginVersion → minAppVersion)main.jslisted in.gitignoreis fine — we rebuild it
Refuse to run if any of those are missing. Suggest scaffold-workflow if manifest.json exists but .github/workflows/release.yml does not.
Commands
release <version> — full ship
bun run scripts/release.ts <version> # via package.json script if user added it
# OR direct
~/.claude/skills/obsidian-plugin-release/scripts/release.ts <version>
Steps the script does (each one a hard gate — stop on failure):
- Verify clean tree — abort if uncommitted changes (unless
--allow-dirty). - Verify version is new —
versions.jsonmust not already contain it. - Bump version atomically — write the same
<version>tomanifest.json,package.json, and appendversions.jsonkeyed to currentminAppVersion. UseJSON.parse → mutate → write(preserve trailing newline + tab indent that Obsidian expects). - Build —
bun run build. Abort on non-zero. - Lint —
bunx eslint src/(skip if user has nosrc/— JS-only plugins exist). Abort on errors. - Stage —
git add manifest.json package.json versions.json main.js styles.css. Only those 5 files. - Commit — conventional message, no Co-Authored-By:
chore: release <version> - Push main —
git push origin <current-branch>. - Tag —
git tag -a <version> -m "Release <version>"(annotated becausetag.gpgSign=trueis common; annotated works either way). - Push tag —
git push origin <version>. This triggers the release workflow. - Wait for workflow —
gh run watch --exit-statusagainst the latest run on this tag. Abort if it fails. - Verify release —
gh release view <version> --json assetsmust includemain.js,manifest.json,styles.css. Attestation is verified viagh attestation verifyonmain.js.
Print the release URL + the community page (https://community.obsidian.md/plugins/<id>) + the deep link (obsidian://show-plugin?id=<id>) at the end.
release <version> --dry
Print every file that would change, the commit message, the tag message, and the workflow that would fire. No writes, no network calls.
release --check
Audit current state. Output:
- Last released version (from
versions.json) - Current
manifest.jsonversion (warn if ahead ofversions.json) - Git tree clean? Branch? Ahead/behind origin?
- Build passes locally?
- Lint passes locally?
- Workflow file present? Has attestation step?
Use this before running a release if anything feels off.
release scaffold-workflow
Drop .github/workflows/release.yml into the current plugin. Idempotent — refuses to overwrite unless --force.
Template lives at templates/release.yml. It:
- Triggers on
[0-9]+.[0-9]+.[0-9]+semver tags - Sets up Bun with frozen lockfile
- Builds via
bun run build - Generates a build-provenance attestation over
main.js,manifest.json,styles.cssusingactions/attest-build-provenance@v2 - Creates or updates the release with the 3 signed assets
- Required permissions:
contents: write,id-token: write,attestations: write
Also offer to scaffold templates/eslint.config.mjs if the user is starting from scratch — it ships with the obsidianmd plugin pre-wired and the ignore globs (web/**, assets/**, scripts/**) that match the Obsidian scanner expectations.
Edge cases
tag.gpgSign=true: always usegit tag -a -m "..."to avoid the "no tag message" error. Annotated tags work whether signing is enabled or not, so we use them unconditionally.- No
bunavailable: fall back tonpm run build+npx eslint src/. The released workflow uses Bun regardless — local dev tool is incidental. web/subdir present: the scanner used to lintweb/**and flag the Next.js landing site as a million unsafe-returns. Make sureeslint.config.mjshasweb/**inignoresand the ignores block sits beforeobsidianmd.configs.recommended(the obsidianmd config spreadsfiles: ["**/*.ts", "**/*.tsx"]which otherwise picksweb/up). See references/scanner-warnings.md.- Plugin not yet listed on Obsidian Community: the workflow still runs. The release exists, the attestation exists, the deep link
obsidian://show-plugin?id=<id>only works once Obsidian's directory crawler picks it up (≤24h after submission). - First release ever: the script accepts
--firstand skips the "version is new" check. Useful when bumping from0.0.0to0.1.0for the first submission.
Anti-patterns (don't)
- Don't bypass the tag. The workflow is the source of truth for attestation. Manual
gh release createskips the OIDC signing step and the Obsidian scanner will flag missing provenance. - Don't squash
versions.json. It's a permanent ledger. Appending only. - Don't push tag before main. Workflow checks out the tag commit; if main isn't there yet, the build references commits that don't exist on the default branch.
- Don't release from a dirty tree. The committed
main.jsmust match the builtmain.jsfrom the source. If they diverge,build-provenancewon't fail but reproducibility (Build verified: byte-for-byte) will.
Files
- scripts/release.ts — main entry, runs the 12 steps
- templates/release.yml — GitHub Actions release workflow
- templates/eslint.config.mjs — Obsidian scanner-friendly eslint config
- references/scanner-warnings.md — common Obsidian scanner findings + how to fix
- references/changelog-style.md — recommended commit + release notes format
Related skills
skill-creator— for authoring new skills (this one was made with it)skillkit— local analytics for skill usage