13 KiB
Releasing ReWrite (Voice Notes)
How to cut a new release the Obsidian way, without tripping the community-plugin review. Read this before every release.
Releases are automated by .github/workflows/release.yml: pushing a version tag builds the bundle, attaches build-provenance attestations, and publishes the GitHub release. Your job is the version bump, the pre-flight checks, and pushing a correctly named tag.
TL;DR
# 0. On master, clean working tree, everything you want shipped is committed.
npm run build && npm run lint # must both pass
# 1. Bump version files (no auto commit/tag so we control the message)
npm version patch --no-git-tag-version # or minor / major
# 2. Commit the bump
git add manifest.json package.json package-lock.json versions.json
git commit -m "1.0.1" # use the new version as the subject
# 3. Tag with the BARE version (no leading v) and push
git tag -a 1.0.1 -m "Release 1.0.1"
git push origin master
git push origin 1.0.1
# 4. Watch CI, then verify provenance (see Verify below)
The tag name must equal manifest.json's version exactly. .npmrc already pins tag-version-prefix="", so npm version produces a bare tag too if you ever let it tag directly.
Hard rules (Obsidian requirements)
- No
vin the tag. The release tag must matchmanifest.jsonversioncharacter-for-character:1.0.1, neverv1.0.1. - Three loose asset files.
main.js,manifest.json,styles.cssattached as individual binary assets, never zipped. The workflow does this; do not hand-upload. - A new release needs a new version number. Obsidian's automated review only registers a change when the version increments. Re-pushing the same version does not count as a new submission. Bump the patch/minor/major rather than overwriting a published version. (To address review feedback, update the repo and publish a new GitHub release with an incremented version.)
- Version format is
x.y.zonly. Semantic Versioning, no pre-release suffixes (no1.0.0-beta, no build metadata). The initial release is1.0.0. - The directory reads
manifest.jsonat the HEAD of your default branch (master), not just the release asset. Keep master'smanifest.jsoncorrect and in sync with the released version. minAppVersionmust be >= the highest@sinceof every Obsidian API you call directly (anything not behind a runtime feature-detect). Checknode_modules/obsidian/obsidian.d.tsfor the@sinceof new APIs. Example from this project:FileManager.processFrontMatteris@since 1.4.4, which is whyminAppVersionis1.4.4. Feature-detected APIs (likeapp.secretStorage) do not raise the floor.versions.jsonmaps plugin version -> minAppVersion. Our version-bump.mjs only adds a new line when theminAppVersionvalue is not already present (i.e. when the floor actually changes). That is valid: Obsidian reads the latest version straight from the releasemanifest.json, and consultsversions.jsononly to find the newest plugin version compatible with an older app. If you raiseminAppVersion, confirm a newversions.jsonentry was written; if you keep it, no new line is expected.- Public repo + LICENSE. The repo must be public to be listed, with a real LICENSE whose copyright holder is correct (this plugin is 0BSD). The README must disclose network use, and
manifest.jsoncarriesauthor,authorUrl, and (if you take donations)fundingUrl.
Pre-flight checklist
npm run buildpasses (this istsc -noEmitthen esbuild production; a type error here is a release blocker).npm run lintpasses with zero warnings. The localeslint-plugin-obsidianmdis looser than the official review bot, so also eyeball the conflict checklist below.- Manual smoke test in a real vault for anything you touched. At minimum for a code change: record + Quick Record, run a template insert (cursor / new file / append), and on desktop start the local whisper.cpp server. Install by copying
main.js/manifest.json/styles.cssinto<Vault>/.obsidian/plugins/rewrite-voice-notes/(folder name must match the pluginid) and reloading. - Update docs for any behavioral change (CLAUDE.md, the user-facing
wiki/pages, and the README), per the doc-maintenance rules in CLAUDE.md.
Guideline-conflict checklist (what the review bot flags)
These are the recurring findings; clear them before tagging. Most are also why the items above exist.
- Plugin
id: lowercase letters and hyphens only, must not end inplugin, must not containobsidian. Locked once published; do not change it. (Ours isrewrite-voice-notes.) - No newer-than-minAppVersion APIs: see the
minAppVersionrule above. - No
eslint-disabledirectives. The bot rejects disabling its rules. If a string tripsui/sentence-case(e.g. a random example), pass it through a variable instead of a string literal; the rule only inspects literals. - Popout-window safety: use
activeDocument/activeWindowinstead ofdocument/window-as-globalThis where a popout could differ; usewindow.setTimeout/window.clearTimeout(not baresetTimeout); avoidglobalThis(usewindow). For pairedaddEventListener/removeEventListener, capture one document reference so removal targets the same object. - No
!importantin styles.css. Raise specificity, use CSS variables, or toggle via Obsidian'sel.toggle()/hide()/show()(which set inline display) instead. - Manifest
description: action-focused, <= 250 chars, ends with a period, no emoji. - Build provenance: leave releases to CI so the attestation is generated; hand-uploaded assets are unattested.
- Deferred by choice (document, do not silently regress): the
display()->getSettingDefinitionssettings migration (needs minAppVersion 1.13.0+, deferred) and full-vault enumeration (getFilesfor audio collection is necessary and disclosed in the README "Vault access" section).
See DEVCONFLICTS.md for the full history of conflicts found and how each was resolved or accepted.
What the CI workflow does
On any pushed tag, .github/workflows/release.yml:
- checks out, sets up Node 20,
npm ci, npm run build(producesmain.js;manifest.jsonandstyles.cssare already in the repo),actions/attest-build-provenance@v2over the three assets (cryptographic provenance proving they were built from source),softprops/action-gh-release@v2publishes/updates the release for that tag with the three assets.
It runs with permissions: contents: write, id-token: write, attestations: write. If you ever change the workflow, keep all three permissions or attestation fails. The workflow also relies on the repo allowing Actions to write: Settings -> Actions -> General -> Workflow permissions -> Read and write permissions must be enabled (the per-job permissions block sets the token scopes, but the repo-level toggle must also permit it).
Difference from Obsidian's sample workflow. The official guide (Release your plugin with GitHub Actions) uses the GitHub CLI to create a draft release that you publish manually after adding notes:
gh release create "$tag" --title="$tag" --draft main.js manifest.json styles.css
Ours intentionally diverges: it auto-publishes (no manual step) and adds build-provenance attestations, which the sample does not. If you ever want the draft-and-review-notes flow instead, switch the publish step back to the gh release create ... --draft form, but you then lose attestation unless you keep the attest step.
Verify (after pushing the tag)
gh run watch <run-id> --repo WiseGuru/ReWrite-Voice-Notes --exit-status # must exit 0
# Provenance check against the published asset:
gh release download 1.0.1 --repo WiseGuru/ReWrite-Voice-Notes --dir /tmp/rel --clobber
gh attestation verify /tmp/rel/main.js --repo WiseGuru/ReWrite-Voice-Notes # must exit 0
Also confirm the release page shows the bare tag (1.0.1, no v) and all three assets.
Re-releasing the same version (rare)
Only for fixing a botched release that nobody has consumed, and never once the version is accepted/depended on. Move the tag to the new commit and force-push to re-trigger CI:
git tag -d 1.0.1 && git tag -a 1.0.1 -m "Release 1.0.1"
git push origin 1.0.1 --force
For anything the community review should notice, cut a new version instead.
Submitting to the community list (first time only)
Prerequisites: a public repo containing README.md, LICENSE, and manifest.json, plus at least one published GitHub release whose tag matches the manifest version and carries main.js / manifest.json / styles.css.
The documented path is the web form, not a manual community-plugins.json PR:
- Sign in at community.obsidian.md with your Obsidian account.
- Link your GitHub account to your profile.
- Plugins -> New plugin, enter your repository URL.
- Agree to the Developer policies, then Submit.
Notes:
- The directory processes the
manifest.jsonat the HEAD of your default branch, so master must be correct. - The
idmust be unique across all published plugins and must not containobsidian(and, per the manifest rules, must not end inplugin). - When a user installs, Obsidian downloads
main.js,manifest.json, andstyles.cssfrom the GitHub release. - An automated reviewer runs the checks in the conflict checklist above. To address feedback, update the repo and publish a new GitHub release with an incremented version (do not reuse a version).
Submission requirements (verify before submitting)
From Submission requirements for plugins:
- Remove all sample/template code (leftover from
obsidian-sample-plugin); rename placeholder classes. - Command IDs must not include the plugin id (Obsidian auto-prefixes them with the id).
isDesktopOnly: trueif the plugin uses Node.js/Electron APIs (fs,crypto,os,child_process, etc.). We keep itfalseand lazy-load Node modules only behindPlatform.isDesktopfor the desktop-only whisper host, which is the accepted mixed pattern but is worth re-justifying each review.description: action-focused (not "This is a plugin..."), <= 250 chars, ends with a period, no emoji, correct casing for brands/acronyms.fundingUrl: include only if you actually accept donations.minAppVersion: a real minimum; if unsure, the latest stable build.
Broader plugin guidelines (continuous, re-check before release)
From Plugin guidelines. Most are already satisfied; treat this as a regression guard for new code:
- Use
this.app, never the globalapp/window.app. Keep console output to errors only. - Settings tab: no top-level/plugin-name heading, no the word "settings" in section names, use
setHeading()(we wrap this insectionHeading()). - DOM: never
innerHTML/outerHTML/insertAdjacentHTML; build withcreateEl/createDiv/createSpan; clear withel.empty(). - Clean up on unload via
registerEvent/registerInterval/registerDomEvent; do not detach leaves inonunload. - Commands: no default hotkeys;
callbackvscheckCallbackvseditorCheckCallbackchosen to match whether the command needs an editor. - Workspace/vault:
getActiveViewOfType(MarkdownView)overactiveLeaf;Vault.processoverVault.modifyfor background read-modify-write (Editor API for the active file);FileManager.processFrontMatterfor frontmatter; preferapp.vaultoverapp.vault.adapterexcept for plugin-config files;getFileByPath/getAbstractFileByPathover iterating;normalizePathon all constructed paths. - Styling: no hardcoded
el.style, no!important; use CSS classes + Obsidian CSS variables (--text-muted, etc.). - Mobile/popout: avoid Node/Electron APIs on mobile; avoid lookbehind in regexes; use
activeDocument/activeWindowandwindow.setTimeoutfor popout-window safety (avoid bareglobalThis). - TypeScript:
const/let(novar),async/awaitover raw Promise chains.