The release/X.Y.Z → main promotion flow diverged next (beta.N) from main (stable) on every release, so the automatic main→next sync hit a version-file conflict each time and needed manual resolution (the 1.1.3 / 1.1.4 pain). New flow: bump next to the stable version via a PR INTO next, then promote next → main directly. main and next never diverge on the version files, so the post-release sync is a clean auto-merge. - version-check.yml: a plain X.Y.Z head on a `next` PR is now accepted as a promotion-staging shape (validated by the stable all-manifests-agree rule); the beta root-pinned rule applies only to `-beta.N` heads. Backward compatible — normal beta PRs are unaffected. - release-flow.md + CONTRIBUTING.md: rewrite the promotion + hotfix flow to "bump on next → promote next → main", drop the release/ branch, and route hotfixes through next too (main only merges from next). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Contributing
Thanks for your interest. The project is built around a few hard conventions that make the contribution loop predictable. Please read this once before opening your first PR.
Repository layout
plugin/ Obsidian plugin (TypeScript, esbuild, vitest)
server/ obsidian-remote-server daemon (Go, fsnotify)
proto/ Shared JSON-RPC method + error definitions (TS + Go in lockstep)
docs/ Architecture notes (shadow vault, perf, testing strategy, …)
deploy/ Turn-key sshd container (Docker compose) for trying the plugin
docker/ Test-only sshd container for the integration suite
See README.md for the user-facing story.
Dev setup
Prerequisites:
- Node.js 20+ for the plugin (
plugin/package.json) - Go 1.25+ for the daemon (
server/go.mod'sgodirective) - Docker for the integration test suite (optional for plugin-only changes)
# Plugin
cd plugin
npm ci
npm test # ~570 unit tests, ~3 s
npx tsc --noEmit # type-check
npm run dev # esbuild watch into <plugin>/main.js
# Server
cd server
make build # builds bin/obsidian-remote-server (host platform)
make cross # builds 4 platforms into dist/
go test ./...
# Full integration (Docker required)
cd plugin
npm run sshd:start
npm run test:integration
npm run sshd:stop
# Plugin-side perf bench (separate from `test:integration`)
npm run test:integration:bench
# E2E against a real Obsidian window (Playwright + CDP)
# Requires: Obsidian installed + Docker test sshd running + plugin built.
# Linux/macOS only locally; Windows is workflow-only (Xvfb-dependent).
OBSIDIAN_PATH=/path/to/obsidian npm run build && npm run sshd:start
npm run test:e2e # all specs (smoke + sync + reflect + demo)
npm run test:e2e:reflect # just the M11 remote-→-Obsidian reflection set
npm run sshd:stop
A working dev vault path can be configured via REMOTE_SSH_DEV_VAULT
env var; npm run build:full then ships the plugin into that vault.
The E2E suite also runs nightly via .github/workflows/e2e.yml
(03:00 UTC / 12:00 JST). Trigger off-cycle from GitHub Actions →
"E2E smoke (Playwright + Obsidian)" → "Run workflow".
Branch + commit convention
Branches
feat/<short-name>— new featurefix/<short-name>— bug fixchore/<short-name>— repo hygiene, dependency bumps, refactors with no user-visible changedocs/<short-name>— documentation-only changes
One branch = one PR = one logical change. We squash-merge, so the branch's commits don't need to be pristine — but the PR title and body do.
Branching model — next (beta) + main (stable)
This repo runs a two-channel release model. Pick the right base for your PR:
| You're doing… | Base your PR on | Version shape |
|---|---|---|
| New feature / refactor / bug fix / docs / chore | next ← almost everything |
X.Y.Z-beta.N |
| Emergency hotfix for a shipped stable release | next (fast-tracked, then promote) |
X.Y.Z |
| Promotion of accumulated betas to stable (release cut) | next (bump to stable, then next → main) |
X.Y.Z (suffix dropped) |
Why this exists:
nextis the beta channel. BRAT users (obsidian42-bratwith this repo's slug +--beta) follow this branch viamanifest-beta.json— every merge tonextpublishes avX.Y.Z-beta.NGitHub Release marked as prerelease, and BRAT auto-updates them on the next launch.mainis the stable channel. Each merge publishes a cleanvX.Y.ZGitHub Release marked as the latest, which is what the Obsidian Community Plugins store + the README install link resolve to. Async: main → nextPR is opened and auto-merged automatically by.github/workflows/sync-main-to-next.yml, so the histories rejoin without manual intervention.- The version shape is the single source of truth for the channel:
release.ymlreadsplugin/manifest.jsonand chooses prerelease vs stable based on whether the version contains-beta..version-check.ymlenforces that the right shape lives on the right branch.
Cutting a promotion (next → main)
main only ever merges from next — there is no release/* branch. Bump
next to the stable version first (on a normal branch, PR'd into next),
then promote next to main:
# 1. Bump next to the stable version.
git checkout -b release-X.Y.Z next
cd plugin
npm run bump:stable # 1.0.44-beta.5 → 1.0.44 (drops the -beta suffix)
git commit -am "release: X.Y.Z (stable)"
git push origin release-X.Y.Z
gh pr create --base next --head release-X.Y.Z --title "release: X.Y.Z (stable)"
The bump runs the full suite (commitlint, version-check, lint, type-check,
tests) on its PR into next — version-check.yml accepts a plain X.Y.Z
on next as a promotion-staging shape, and release.yml skips publishing a
stable version pushed to next (the main push is what releases). No admin
override, no separate release branch.
After it merges (next now carries the stable X.Y.Z), promote directly:
gh pr create --base main --head next --title "promote: next → main (X.Y.Z)"
On the main push: release.yml publishes vX.Y.Z stable + cosign-signed
binaries; sync-main-to-next.yml opens + auto-merges main → next. Because
next and main are now the same X.Y.Z, that sync has no version
conflict — which is exactly why the bump lives on next and not on a branch
that diverges from it. Then start the next cycle with npm run bump:beta:start
(X.Y.Z → X.Y.(Z+1)-beta.0) on a feat/... branch.
Commit messages — Conventional Commits, enforced
Format: type(scope): subject
feat(plugin): Phase D-γ / F18 — error toast taxonomy + Notice / log integration (0.4.51)
fix(server): unbreak Go + Dockerfile after Dependabot rollups (0.4.52)
ci(release): grouped CHANGELOG via git-cliff (0.4.57)
type∈buildchorecidocsfeatfixperfrefactorrevertstyletestreleasescopeis free-form; common scopes:plugin,server,proto,deploy,ci,release,security.- Subject can mix cases (
Phase D-γis fine; commitlint'ssubject-caseis intentionally relaxed — seecommitlint.config.mjs). - Header cap is 144 characters (the standard 100 doesn't fit our
Phase X.Y — long subject (0.4.NN)style). - The
(0.4.NN)suffix matches the version bump (see below). - A trailing
Co-Authored-By:line is fine; it's stripped from the auto-generated changelog (cliff.tomlcommit_preprocessors).
The commitlint.yml workflow checks every commit in the PR. A typo in commit #3 of a 5-commit PR fails the gate; please fix in-place + force-push rather than tacking on a fix typo commit.
Version bumps
The version shape selects the channel: X.Y.Z-beta.N lives on next, plain X.Y.Z lives on main. Three npm scripts cover the lifecycle:
cd plugin
# Day-to-day work on next (most PRs):
npm run bump:beta # 1.0.44-beta.3 → 1.0.44-beta.4
# First commit of a new beta cycle (after main has just absorbed
# a stable release and next == main):
npm run bump:beta:start # 1.0.44 → 1.0.45-beta.0
# Promotion (drop the suffix; only run when ready to cut stable):
npm run bump:stable # 1.0.44-beta.5 → 1.0.44
Each script:
- updates
plugin/package.json+plugin/manifest.json - updates the right root manifest:
- beta bump → only
manifest-beta.json(BRAT --beta consumers) - stable bump → both
manifest.json(Obsidian Community Plugins) andmanifest-beta.json, plus theversions.jsoncompatibility map
- beta bump → only
- stages every changed file via the npm
versionlifecycle hook
Commit the staged files alongside your code change.
version-check.yml is branch-aware: beta PRs into next must keep manifest.json (root) pinned to the last stable — except a stable promotion-staging bump (a plain X.Y.Z landed on next), which is validated with the same all-manifests-agree rule as a main PR. PRs into main must carry a plain X.Y.Z (no beta suffix) with all manifests in agreement. If your PR sits open while another lands on the same base, rebase + run the same bump: script again.
Pull request etiquette
- Reuse
.github/pull_request_template.md— the prompts are there for a reason. - Add a Test plan checklist; mark each item once it actually passed locally.
- Run
npx tsc --noEmit -p tsconfig.jsonandnpx vitest runbefore pushing — CI runs them anyway, but the feedback loop is faster locally. - For server changes, also run
go test ./...fromserver/. - Stack PRs are welcome — open the dependent PR with
--base <prior-branch>. Once the prior merges, GitHub auto-rebases the dependent PR's base tonext(ormainfor stack PRs targeting the stable channel).
Where things live
- Architecture decisions →
docs/en/architecture/*.md. The shadow-vault rationale (and why the priorreconcileFileroute was abandoned) is indocs/en/architecture/shadow-vault.md. - Test strategy →
docs/en/contributing/testing-strategy.md. Phase A (multi-client convergence), B (multi-OS), C (sync-latency + UI reflect) are codified there. - Performance numbers → the
perf-baselineorphan branch'sbaseline.ndjson, refreshed nightly bybench.yml. - Issue tracker → for bug reports, feature requests, design questions.
- Security issues → see SECURITY.md, not the public issue tracker.
Code style
- TypeScript:
strictNullChecks: true,isolatedModules: true. No ESLint config required for now (was scoped out as low-value during early development). - Go:
gofmt -w .(thegosecjob insecurity.ymlwill flag obvious issues). - Comments: explain why, not what. The codebase prefers a few generous block comments at module / class boundaries over per-line noise.
- New tests should mirror the surface they cover:
src/util/Foo.ts↔tests/Foo.test.ts.
Releases
Tagged releases (X.Y.Z format) trigger .github/workflows/release.yml:
- Test gate (typecheck + vitest)
- Server-binary build × 4 platforms + cosign keyless sign
- Plugin bundle build + bundle-size check
- Grouped changelog generated by
git-cliff(cliff.toml) - GitHub Release created with all artefacts attached
You don't usually create releases by hand; release.yml watches both branches and selects channel by version shape — every merge to next publishes a vX.Y.Z-beta.N prerelease, every merge to main publishes a vX.Y.Z stable. See Branching model for the promotion flow.
Thanks again — and welcome.