Compare commits

..

2 commits

Author SHA1 Message Date
Chris Lettieri
cc62c4d591 release tooling: scripts/release.sh + version-consistency guard test
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>
2026-07-07 15:04:53 -04:00
Chris Lettieri
09892d2ef3 chore: sync package.json to 0.6.0; add versions.json
package.json version drifted from manifest; add the missing versions.json
(version -> minAppVersion) that version-bump.mjs and community-store
submission both require. Neither affects Obsidian's update check for the
current install path, but both were inconsistent locations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:48:24 -04:00
6 changed files with 305 additions and 119 deletions

View file

@ -138,16 +138,19 @@ Tests cover: filename utils, OpenAI prompt building, link extraction, BFS traver
- Enable verbose logging in development
### Publishing
See [docs/PUBLISHING.md](docs/PUBLISHING.md) for the complete release process.
See [docs/PUBLISHING.md](docs/PUBLISHING.md) for the complete release process,
including community-store listing health and how to relist if de-listed.
**Quick summary:**
1. Update version in `manifest.json` and `package.json`
2. Commit and push to master
3. Tag with matching version: `git tag -a X.Y.Z -m "X.Y.Z" && git push origin X.Y.Z`
4. Generate release notes (Claude: compare commits since last tag, focus on user-facing changes)
5. Edit draft release on GitHub and publish
**Just run the script** (it bumps all three version files, publishes, and verifies):
```bash
# write docs/release-notes/X.Y.Z.md first, then:
./scripts/release.sh X.Y.Z
```
*** Be sure to update `manifest.json` version number as part of PR ***
**Non-negotiables** (the guard test `tests/version-consistency.test.ts` enforces these):
- Bump **all THREE** version files together: `manifest.json`, `package.json`, **and `versions.json`** (add `"X.Y.Z": "<minAppVersion>"`). Forgetting `versions.json` is the classic mistake.
- Tag == `manifest.version`, no `v` prefix.
- **Publish the draft immediately** after CI — never leave the repo advertising a version with no published release (that inconsistency can get the plugin auto-removed from the store).
## Important Considerations

View file

@ -1,149 +1,170 @@
# Publishing a New Release
This guide walks through the complete process of publishing a new OpenAugi plugin 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.
## Pre-Release Checklist
---
Before starting the release process:
- [ ] All features/fixes are merged to master
- [ ] **Automated tests pass** (`npm test`) — see [TESTING.md](TESTING.md)
- [ ] New features have test coverage (add to `tests/`)
- [ ] Code builds without errors (`npm run build`)
- [ ] Type checking passes (`npm run typecheck`)
- [ ] Manual testing completed in test vault (`/Users/chris/zk-for-testing`)
- [ ] CODEBASE_MAP.md is up to date with any architectural changes
## Release Process
### Step 1: Bump the Version
Update the version number in **both** files (they must match):
1. `manifest.json` - Update the `"version"` field
2. `package.json` - Update the `"version"` field
Use semantic versioning:
- **MAJOR** (1.0.0): Breaking changes
- **MINOR** (0.1.0): New features, backwards compatible
- **PATCH** (0.0.1): Bug fixes, backwards compatible
### Step 2: Commit the Version Bump
## TL;DR — use the script
```bash
git add manifest.json package.json
git commit -m "Bump version to X.Y.Z"
git push origin master
# 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
```
### Step 3: Create and Push the Git Tag
`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.
The tag **must** match the version in `manifest.json` exactly.
Everything below explains *why* it does what it does, and the manual fallback.
```bash
git tag -a X.Y.Z -m "X.Y.Z"
git push origin X.Y.Z
```
## The three things Obsidian actually checks
Notes:
- `-a` creates an [annotated tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging#_creating_tags) (required for Obsidian releases)
- `-m` specifies the tag message - must match the version number
1. **`manifest.json` at the repo-root HEAD** — Obsidian reads only the `version`
field here to decide "what's the latest version."
2. **A GitHub release whose tag == that version** (no `v` prefix), with
`main.js`, `manifest.json`, and `styles.css` attached as assets. Obsidian
downloads the files from the *release*, not from the repo tree.
3. **`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.
### Step 4: Wait for GitHub Actions
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.)
The workflow will automatically:
1. Build the plugin
2. Create a draft release with the built artifacts
## Version files — all THREE must move together
Monitor progress at: https://github.com/bitsofchris/openaugi-obsidian-plugin/actions
| 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** |
### Step 5: Generate Release Notes
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.
**Claude should generate release notes** by comparing the new tag to the previous release:
> Historical note: earlier versions of this doc listed only `manifest.json` and
> `package.json`. `versions.json` was missing entirely, which broke the
> `npm version` script and left the store metadata incomplete. Don't regress.
1. Look at all commits since the last release tag
2. Identify user-facing changes (features, fixes, improvements)
3. Create release notes in this format:
## Pre-release checklist
- [ ] Features/fixes merged to master; `git status` clean; local == `origin/master`
- [ ] `npm test` passes (includes the version-consistency guard) — see [TESTING.md](TESTING.md)
- [ ] `npm run build` clean (tsc + esbuild)
- [ ] Manually tested in a real vault (`npm run dev`, reload plugin, exercise the change)
- [ ] `docs/CODEBASE_MAP.md` / `README.md` updated 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.
```markdown
## TL;DR
[One-sentence summary of the most important change(s)]
# X.Y.Z — <one-line theme>
## What's New
## Added / Changed / Fixed / Deprecated
- <user-facing change>: <why the user cares>
### Features
- [Feature 1]: [Brief description of what it does and why users care]
- [Feature 2]: [Brief description]
### Bug Fixes
- [Fix 1]: [What was broken and how it's fixed]
### Improvements
- [Improvement 1]: [What's better now]
## How to Use
[For any new features, include brief usage instructions]
## Upgrade Notes
[Any breaking changes or things users need to know when upgrading]
## Notes
- <breaking changes or upgrade steps, if any>
```
Write this out to a markdown file.
## Manual fallback (if you can't use the script)
### Step 6: Publish the Release
1. Go to https://github.com/bitsofchris/openaugi-obsidian-plugin/releases
2. Find the draft release created by the workflow
3. Click **Edit**
4. Paste the generated release notes
5. Click **Publish release**
### Step 7: Update Documentation
Ensure all docs reflect the new release:
- [ ] `docs/CODEBASE_MAP.md` - Architecture changes
- [ ] `CLAUDE.md` - Any new development guidelines
- [ ] `README.md` - User-facing documentation (if applicable)
## Quick Reference
Only if `release.sh` can't run. Do the steps **in this order** and don't stop
before publishing:
```bash
# Full release flow (example for version 0.4.0)
npm test
npm run build
npm run typecheck
# 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
# Commit version bump
git add manifest.json package.json
git commit -m "Bump version to 0.4.0"
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
# Tag and push
git tag -a 0.4.0 -m "0.4.0"
git push origin 0.4.0
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
# Then: Edit draft release on GitHub and publish
# 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.Z` also works: `.npmrc` sets `tag-version-prefix=""` (so the
> tag has no `v`), and the `version` npm script runs `version-bump.mjs` which
> updates `manifest.json` + `versions.json`, while npm bumps `package.json`. It
> commits and tags for you — but it does **not** push or publish, so you still
> owe steps 13 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`](https://github.com/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:
```bash
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:
1. **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.json` present, `README`/`LICENSE`
present. (All true as of 0.6.0.)
2. **Ask Obsidian why + request reinstatement** — don't blindly resubmit an
auto-removal. Post in the Obsidian Discord **`#plugin-dev`** channel (fastest;
plugin-review team is active there) or the forum **Developers: Plugin & API**
category. Cite: repo URL, removal date `2026-07-07`, mirror commit `85db6f23`.
3. **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.)
4. **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-dev` to 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
**Tag already exists / wrong version tagged**
```bash
# Delete local tag
git tag -d X.Y.Z
# Delete remote tag (if pushed)
git push origin --delete X.Y.Z
git push origin --delete X.Y.Z # if already pushed
# fix versions, re-run ./scripts/release.sh X.Y.Z
```
### Wrong version tagged
1. Delete the incorrect tag (see above)
2. Fix the version in manifest.json/package.json
3. Commit and re-tag
**Workflow failed** — `gh run view <id>`, fix, delete the tag, re-run the script.
### Workflow failed
1. Check the Actions tab for error details
2. Fix the issue
3. Delete the tag and re-push after fixing
**Release stuck as draft** — `gh release edit X.Y.Z --draft=false --latest`.

View file

@ -1,6 +1,6 @@
{
"name": "open-augi",
"version": "0.5.2",
"version": "0.6.0",
"description": "",
"main": "main.js",
"scripts": {

114
scripts/release.sh Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
#
# release.sh — publish a new OpenAugi plugin version, safely and verifiably.
#
# Usage: ./scripts/release.sh X.Y.Z
#
# Why this exists: doing releases by hand drifted manifest.json / package.json /
# versions.json out of sync (0.6.0 shipped with a stale package.json and no
# versions.json) and left a window where the repo advertised a version that had
# no published release — which is plausibly why the plugin was auto-removed from
# the community store. This script bumps all three files in lockstep, then does
# push → tag → wait-for-CI → PUBLISH → verify in one shot so that window never
# opens. See docs/PUBLISHING.md.
#
set -euo pipefail
REPO="bitsofchris/openaugi-obsidian-plugin"
die() { printf '\n\033[31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
step(){ printf '\n\033[1m▸ %s\033[0m\n' "$*"; }
VERSION="${1:-}"
[[ -n "$VERSION" ]] || die "usage: ./scripts/release.sh X.Y.Z"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be X.Y.Z (no 'v' prefix): got '$VERSION'"
cd "$(git rev-parse --show-toplevel)"
NOTES="docs/release-notes/${VERSION}.md"
# ── Preconditions ────────────────────────────────────────────────────────────
step "Preconditions"
command -v gh >/dev/null || die "gh CLI not found"
gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)"
[[ "$(git branch --show-current)" == "master" ]] || die "must be on master"
[[ -z "$(git status --porcelain)" ]] || die "working tree not clean — commit or stash first"
git fetch -q origin master
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/master)" ]] || die "local master differs from origin/master — push/pull first"
git rev-parse "$VERSION" >/dev/null 2>&1 && die "tag $VERSION already exists locally"
[[ -f "$NOTES" ]] || die "release notes not found: $NOTES (write them first — they become the GitHub release body)"
ok "on master, clean, synced; notes present; tag is free"
# ── Quality gate ─────────────────────────────────────────────────────────────
step "Tests + build"
npm test
npm run build
ok "tests + build pass"
# ── Bump all three version files in lockstep ─────────────────────────────────
step "Bump manifest.json / package.json / versions.json → $VERSION"
node - "$VERSION" <<'NODE'
const fs = require('fs');
const v = process.argv[2];
const write = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, '\t') + '\n');
const manifest = JSON.parse(fs.readFileSync('manifest.json', 'utf8'));
manifest.version = v;
write('manifest.json', manifest);
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = v;
write('package.json', pkg);
const versions = JSON.parse(fs.readFileSync('versions.json', 'utf8'));
versions[v] = manifest.minAppVersion; // version -> min Obsidian version
write('versions.json', versions);
console.log(` manifest ${manifest.version} · package ${pkg.version} · versions[${v}]=${versions[v]}`);
NODE
# The guard test now proves the three files agree before we tag anything.
npx vitest run tests/version-consistency.test.ts
ok "version files consistent"
# ── Commit + push + tag ──────────────────────────────────────────────────────
step "Commit, push, tag"
git add manifest.json package.json versions.json "$NOTES"
git commit -q -m "Release $VERSION"
git push -q origin master
git tag -a "$VERSION" -m "$VERSION" # no 'v' prefix — Obsidian requires tag == manifest version
git push -q origin "$VERSION"
ok "pushed master + tag $VERSION (CI release workflow triggered)"
# ── Wait for CI to build the draft release ───────────────────────────────────
step "Waiting for release workflow"
sleep 5
RUN_ID="$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RUN_ID" --exit-status >/dev/null || die "release workflow failed — see: gh run view $RUN_ID"
ok "workflow succeeded (draft release created)"
# ── Publish immediately (close the inconsistency window) ─────────────────────
step "Publishing release $VERSION"
gh release edit "$VERSION" --draft=false --latest --notes-file "$NOTES" >/dev/null
ok "release published"
# ── Verify remote consistency ────────────────────────────────────────────────
step "Verifying"
RAW="https://raw.githubusercontent.com/${REPO}"
DL="https://github.com/${REPO}/releases/download/${VERSION}"
root_v="$(curl -sfL "$RAW/master/manifest.json" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).version))')"
rel_v="$(curl -sfL "$DL/manifest.json" | node -e 'process.stdin.on("data",d=>console.log(JSON.parse(d).version))')"
[[ "$root_v" == "$VERSION" ]] || die "root manifest on master is '$root_v', expected '$VERSION'"
[[ "$rel_v" == "$VERSION" ]] || die "released manifest asset is '$rel_v', expected '$VERSION'"
is_draft="$(gh release view "$VERSION" --json isDraft --jq .isDraft)"
[[ "$is_draft" == "false" ]] || die "release is still a draft"
ok "root manifest = release asset = tag = $VERSION; release is published"
# ── Store-listing health (informational) ─────────────────────────────────────
step "Community-store listing check"
if curl -sfL "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/master/community-plugins.json" | grep -qi '"openaugi"'; then
ok "plugin is currently listed in the Obsidian community store"
else
printf '\033[33m! Not in the community store list. Users update via BRAT or manual install.\n'
printf ' To relist, see docs/PUBLISHING.md → "If the plugin gets removed from the store".\033[0m\n'
fi
printf '\n\033[32m✓ Released %s\033[0m → https://github.com/%s/releases/tag/%s\n' "$VERSION" "$REPO" "$VERSION"

View file

@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
// Guard test for the release contract. Obsidian resolves the "latest version"
// from manifest.json at the repo root, then downloads the GitHub release whose
// tag equals that version. If manifest.json / package.json / versions.json ever
// disagree, releases go out inconsistent — which is how the 0.6.0 release shipped
// with a stale package.json and a MISSING versions.json, and (plausibly) how the
// plugin got auto-removed from the community store. This test makes that drift a
// red build instead of a production incident. See docs/PUBLISHING.md.
const root = path.resolve(__dirname, '..');
const readJson = (rel: string) =>
JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
const SEMVER = /^\d+\.\d+\.\d+$/; // x.y.z only — no "v" prefix, no pre-release suffix
describe('version consistency (release contract)', () => {
const manifest = readJson('manifest.json');
const pkg = readJson('package.json');
const versions = readJson('versions.json');
it('manifest.json version is valid semver (x.y.z, no v prefix)', () => {
expect(manifest.version).toMatch(SEMVER);
});
it('package.json version matches manifest.json', () => {
expect(pkg.version).toBe(manifest.version);
});
it('versions.json contains the current manifest version', () => {
expect(Object.keys(versions)).toContain(manifest.version);
});
it('versions.json maps the current version to manifest.minAppVersion', () => {
expect(versions[manifest.version]).toBe(manifest.minAppVersion);
});
it('every versions.json key is valid semver', () => {
for (const v of Object.keys(versions)) {
expect(v, `versions.json key "${v}"`).toMatch(SEMVER);
}
});
});

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"0.6.0": "1.8.9"
}