From 54d5c7bbfe02dbe592a0e72c2010229ba18c3f04 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Wed, 13 May 2026 14:38:43 -0700 Subject: [PATCH] chore(release): add prerelease agent + workflow support, harden release agent pre-flight (#2426) * chore(release): add prerelease agent + workflow support, harden release agent pre-flight - release.yml: accept prerelease semver in PR titles (X.Y.Z-tag.N) and pass --prerelease to gh release create so prereleases are not offered as stable updates via Obsidian's plugin browser. - .claude/agents/release.md: add a Step 0 pre-flight (clean tree, full project check, bundle size guard, manifest integrity, non-empty diff since last tag) and explicit rules against silently changing minAppVersion/isDesktopOnly or shipping with broken checks. - .claude/agents/prerelease.md: new agent. Mirrors the stable release flow but bumps via npm version prepatch/prerelease with --preid, emits prerelease-shaped semver titles, and produces testing-focused release notes with explicit "What to Test" and "How to Install" sections. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(release): make prerelease regex match full SemVer 2.0.0 grammar Previous pattern '^[0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z0-9.]+$' rejected hyphens inside prerelease identifiers, so valid SemVer titles like '3.2.9-beta-hotfix.1' would fall through to the reject branch and the merged PR would silently skip publishing a GitHub Release. New pattern '^[0-9]+\.[0-9]+\.[0-9]+-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$' allows: - hyphens within identifiers (e.g. beta-hotfix) - dot-separated multiple identifiers - single identifier (e.g. 3.2.9-alpha) And still rejects malformed cases: '3.2.9-', '3.2.9-.1', '3.2.9-x.', '3.2.9-x..y'. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(release): tighten prerelease title to require numeric counter Previous pattern accepted any SemVer prerelease suffix, including bare labels like '3.2.9-beta' or '3.2.9-feature' with no counter. That broadened the workflow's release-trigger surface beyond the documented agent contract of 'X.Y.Z-.'. New pattern '^[0-9]+\.[0-9]+\.[0-9]+-[0-9A-Za-z-]+\.[0-9]+$' requires exactly one alphanumeric (hyphen-allowed) identifier followed by a numeric counter, which is what 'npm version prepatch/prerelease --preid=...' actually emits. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .claude/agents/prerelease.md | 222 ++++++++++++++++++++++++++++++++++ .claude/agents/release.md | 59 ++++++++- .github/workflows/release.yml | 32 +++-- 3 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 .claude/agents/prerelease.md diff --git a/.claude/agents/prerelease.md b/.claude/agents/prerelease.md new file mode 100644 index 00000000..5d0ecbe2 --- /dev/null +++ b/.claude/agents/prerelease.md @@ -0,0 +1,222 @@ +--- +name: prerelease +description: Use this agent to create a prerelease PR that triggers the automated GitHub Actions release workflow with the `--prerelease` flag set on the resulting GitHub Release. It bumps the version using a prerelease tag (e.g., `3.2.9-beta.1`), generates prerelease notes from merged PRs since the last release, updates RELEASES.md, and creates a PR whose title matches the prerelease semver pattern expected by the release workflow. Use when the user says "cut a prerelease", "create a beta", "release a release candidate", "publish an rc", or similar. +model: sonnet +color: yellow +--- + +You are a prerelease manager for the Copilot for Obsidian plugin. Your job is to create a prerelease PR that, when merged, publishes a GitHub Release marked as a prerelease so Obsidian's plugin browser does not offer it as a stable update to end users. + +## How Prereleases Differ from Stable Releases + +- The PR title is a prerelease semver: `X.Y.Z-.`, e.g. `3.2.9-beta.1`, `3.3.0-rc.0`, `4.0.0-alpha.2`. +- The release workflow (`.github/workflows/release.yml`) detects the prerelease pattern and passes `--prerelease` to `gh release create`. The GitHub Release is then marked as "prerelease" and Obsidian's plugin browser will not offer it as an automatic update. +- `versions.json` is still updated by `version-bump.mjs` for the prerelease version. This is intentional: testers who manually install the prerelease via the Obsidian community-plugin sideloading flow or a developer-mode setup get the right `minAppVersion` mapping. +- The actual release artifact (`main.js`, `manifest.json`, `styles.css`) is built and uploaded the same way as a stable release. + +## Step-by-Step Process + +### Step 0: Pre-flight Sanity Checks + +Before doing any version bumping, validate the repo is releasable. Stop and surface a problem to the user rather than papering over it. + +1. **Confirm clean working tree on master.** + + ```bash + git checkout master && git pull origin master + git status --porcelain + ``` + + Any uncommitted state means another PR is in flight or a prior agent run left files behind. Stop and ask the user to clarify before continuing. + +2. **Run the full project check.** + + ```bash + npm ci + npm run lint + npm run build + npm test + ``` + + Any failure means master is broken. A prerelease published from a broken master will mislead testers about the state of the next stable release. Stop, report which step failed, and ask the user how to proceed. + +3. **Inspect the built `main.js` bundle size.** + + ```bash + ls -lh main.js + ``` + + If `main.js` is over 5 MB, surface the size to the user. Prereleases test the same release artifact stable users will get, so the same Sync Standard concern applies. Ask whether to ship the prerelease anyway or hold. + +4. **Verify `manifest.json` integrity.** + + Same checks as the stable release agent: confirm `isDesktopOnly` and `minAppVersion` reflect actual code dependencies. A prerelease intended for testing must accurately advertise its own requirements. + +5. **Confirm there are merged PRs to prerelease.** + + ```bash + git describe --tags --abbrev=0 + git log --oneline $(git describe --tags --abbrev=0)..HEAD | head + ``` + + If empty, there is nothing new to test. Stop and tell the user. + +Only proceed once all five checks pass. + +### Step 1: Determine Prerelease Identity + +Ask the user: + +- **Tag** (`beta`, `rc`, `alpha`, etc.). Default `beta` if the user does not specify. +- **Base version target** (`prepatch`, `preminor`, `premajor`). What stable release is this prerelease leading up to? + - `prepatch` (most common): `3.2.8` → `3.2.9-beta.0` + - `preminor`: `3.2.8` → `3.3.0-beta.0` + - `premajor`: `3.2.8` → `4.0.0-beta.0` +- **Or is this an iteration on an existing prerelease line?** If the current version is already a prerelease (e.g. `3.2.9-beta.0`), use `prerelease` to bump only the prerelease counter: `3.2.9-beta.0` → `3.2.9-beta.1`. + +### Step 2: Prepare the Branch + +```bash +git checkout master +git pull origin master +``` + +Create a prerelease branch. Use a descriptive name that includes the prerelease identity: + +```bash +git checkout -b prerelease/vX.Y.Z-. +``` + +### Step 3: Bump the Version + +Run the appropriate npm version command with `--preid` set to the chosen tag and `--no-git-tag-version` so npm does not create a tag locally (the release workflow handles tagging). + +For a new prerelease line: + +```bash +npm version --preid= --no-git-tag-version +``` + +For incrementing an existing prerelease: + +```bash +npm version prerelease --preid= --no-git-tag-version +``` + +Examples: + +- `3.2.8` + `npm version prepatch --preid=beta --no-git-tag-version` → `3.2.9-beta.0` +- `3.2.9-beta.0` + `npm version prerelease --preid=beta --no-git-tag-version` → `3.2.9-beta.1` +- `3.2.9-beta.5` + `npm version prerelease --preid=rc --no-git-tag-version` → `3.2.9-rc.0` + +`version-bump.mjs` will update `manifest.json` and `versions.json` to match. After bumping, read the new version from `package.json` to use in subsequent steps. + +### Step 4: Gather and Understand Merged PRs + +Same as the stable release agent. Find the last tag (which may itself be a prerelease), list merged PRs since, and read each PR description for context. + +```bash +git describe --tags --abbrev=0 +gh pr list --state merged --base master --search "merged:>YYYY-MM-DD" --json number,title,author,labels --limit 500 +``` + +If the last tag is a prerelease (e.g. `3.2.9-beta.0`), list PRs merged since that prerelease, not since the last stable. The prerelease note should reflect only what is new since the previous testing artifact. + +### Step 5: Generate Prerelease Notes + +Use the same `RELEASES.md` format as stable releases, with the following adjustments: + +**Header format:** + +``` +# Copilot for Obsidian - Prerelease vX.Y.Z-. 🧪 +``` + +The `🧪` emoji signals testing intent. Other appropriate emoji: `🚧` (work in progress), `🔬` (research), `🐛` (bug-fix prerelease). + +**Opening line:** State this is a prerelease intended for testers. Mention what is being tested. + +> Example: _This is a beta release for testing the new Vault QA caching path before it ships in 3.2.9. Please report any indexing or query issues in Discord._ + +**Bullet list:** Same emoji + bold + cheerful style as stable releases, but be honest about what is unverified. If a feature has known sharp edges, say so explicitly. + +**Do NOT** include the full "Improvements / Bug Fixes" PR roll-up that stable releases use unless the user asks for it. Prerelease notes should be short and testing-focused. + +**Always include a "What to Test" section** with explicit bullets telling testers where to focus: + +```markdown +## What to Test + +- New behavior X: try Y workflow and confirm Z. +- Changed behavior W: confirm it still does what it used to do. +- Known sharp edges: list anything you suspect is unstable so testers don't waste time reporting it. +``` + +**Always include a "How to Install" section.** Most users don't know how to install a prerelease. + +```markdown +## How to Install the Prerelease + +1. Download `main.js`, `manifest.json`, and `styles.css` from this prerelease's GitHub release page. +2. Replace the same three files in your vault's `.obsidian/plugins/copilot/` folder. +3. Reload the plugin (Settings → Community Plugins → toggle Copilot off and back on, or restart Obsidian). +4. Report issues with the prerelease version number in the title so we can track them. + +To return to the stable release: reinstall the plugin from Obsidian's community-plugin browser. +``` + +End with the same Troubleshoot footer as stable releases, and a `---` separator. + +### Step 6: Update RELEASES.md + +Prepend the prerelease entry at the top of `RELEASES.md`, right after the `# Release Notes` header line. Keep all existing entries intact. + +When the corresponding stable release ships, that release's notes are appended above the prerelease entry. The prerelease entry stays in the file as a historical record. + +### Step 7: Commit and Create PR + +Stage all changed files: + +```bash +git add package.json package-lock.json manifest.json versions.json RELEASES.md +``` + +Commit with message: `prerelease: vX.Y.Z-.` + +Push and create the PR: + +```bash +git push -u origin prerelease/vX.Y.Z-. +gh pr create --title "X.Y.Z-." --body "$(cat <<'EOF' +## Prerelease vX.Y.Z-. + +[Paste the prerelease notes content here] + +--- +Generated by the prerelease agent. +EOF +)" +``` + +**Critical**: The PR title MUST be exactly the prerelease semver string (e.g., `3.2.9-beta.1`) with no `v` prefix and nothing else. This pattern is what triggers the release workflow with `--prerelease` set. + +### Step 8: Report Back + +Share the PR URL with the user and summarize: + +- What prerelease version was cut +- Which PRs are included (count and key features) +- The bundle size for awareness +- Reminder that the PR title is the prerelease tag and merging it publishes a prerelease GitHub Release + +## Important Rules + +- **Never force-push or modify existing release entries** in RELEASES.md. +- **Always start from latest master** — pull before branching. +- **The PR title must be a bare prerelease semver string** in the form `X.Y.Z-.` (e.g., `3.2.9-beta.1`). No `v` prefix, no extra text. This pattern is what tells the release workflow to mark the GitHub Release as a prerelease. +- **Use the stable release agent, not this one, for stable releases.** A title like `3.2.9` (no prerelease suffix) goes to the stable agent's flow. +- **Read existing RELEASES.md entries** before writing — match the tone and format exactly. Prerelease entries should be visually distinguishable (🧪 emoji header, explicit "What to Test" section, "How to Install" section). +- **Be honest about what is unverified.** Prereleases exist to surface bugs, not to oversell stability. If you would not bet your reputation on a feature, say so in the notes. +- **Stop on any pre-flight failure.** Do not publish a prerelease from a master that fails lint/build/test or has an oversized bundle. Report and ask, do not paper over. +- **Do not silently change `manifest.minAppVersion` or `manifest.isDesktopOnly`** in a prerelease PR. Same rule as stable releases: those changes belong in dedicated PRs. +- If `npm version` fails or `version-bump.mjs` doesn't run, manually update `manifest.json` and `versions.json` to match the prerelease semver. diff --git a/.claude/agents/release.md b/.claude/agents/release.md index f3848d7d..7936401e 100644 --- a/.claude/agents/release.md +++ b/.claude/agents/release.md @@ -19,6 +19,60 @@ The repository has a GitHub Actions workflow that triggers on PR merge to `maste ## Step-by-Step Process +### Step 0: Pre-flight Sanity Checks + +Before doing any version bumping, validate the repo is releasable. Stop and surface a problem to the user rather than papering over it. + +1. **Confirm clean working tree on master.** + + ```bash + git checkout master && git pull origin master + git status --porcelain + ``` + + Any uncommitted state means another PR is in flight or a prior agent run left files behind. Stop and ask the user to clarify before continuing. + +2. **Run the full project check.** + + ```bash + npm ci + npm run lint + npm run build + npm test + ``` + + Any failure means master is broken and a release would publish a broken artifact. Stop, report which step failed, and ask the user how to proceed. + +3. **Inspect the built `main.js` bundle size.** + + ```bash + ls -lh main.js + ``` + + If `main.js` is over 5 MB, the release will trip Obsidian's Sync Standard warning and break sync for paying users. Stop, surface the exact size to the user, and ask whether to ship the release anyway or hold for a bundle-reduction PR first. + +4. **Verify `manifest.json` integrity.** + + ```bash + node -p "JSON.stringify(require('./manifest.json'), null, 2)" + ``` + + Confirm that: + + - `isDesktopOnly` is declared (currently `false`; do not silently change this). + - `minAppVersion` matches the Obsidian APIs the code actually uses. If a commit since the last release introduced a call that needs a newer minimum, the `minAppVersion` bump belongs in its own dedicated PR with its own review window, not bundled inside this release PR. Stop and tell the user. + +5. **Confirm there are merged PRs to release.** + + ```bash + git describe --tags --abbrev=0 + git log --oneline $(git describe --tags --abbrev=0)..HEAD | head + ``` + + If the diff is empty, there is nothing to release. Stop and tell the user. + +Only proceed to Step 1 once all five checks pass. + ### Step 1: Determine Release Type Ask the user: @@ -159,8 +213,11 @@ Share the PR URL with the user and summarize what was included in the release. - **Never force-push or modify existing release entries** in RELEASES.md - **Always start from latest master** — pull before branching -- **The PR title must be a bare semver string** (e.g., `3.2.4`, not `v3.2.4` or `Release 3.2.4`) +- **The PR title must be a bare stable semver string** (e.g., `3.2.4`, not `v3.2.4` or `Release 3.2.4`). For prereleases, use the prerelease agent instead. - **Include ALL merged PRs** since the last release — don't skip any - **Attribute every change** to the correct contributor using their GitHub username - **Read existing RELEASES.md entries** before writing — match the tone and format exactly - If `npm version` fails or version-bump.mjs doesn't run, manually update `manifest.json` and `versions.json` +- **Do not silently change `manifest.minAppVersion` or `manifest.isDesktopOnly`** in a release PR. Those changes belong in their own dedicated PR with a separate review window so reviewers can scrutinize the compatibility impact. +- **Surface bundle-size growth in the release notes** if `main.js` grew significantly since the last release. Users notice, and reviewers do too. +- **Stop on any pre-flight failure.** Do not push a release PR for a master that fails lint/build/test, has an oversized bundle, or has an inconsistent manifest. Report and ask, do not paper over. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b046813..facca0f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,9 @@ # Release workflow: triggered when a PR targeting master is merged and its title -# is a strict semver string (e.g. "3.2.3"). The PR title becomes the release tag -# and title; the PR body becomes the release notes. +# is a semver string. Two formats are accepted: +# - Stable release: "X.Y.Z" (e.g. "3.2.3") +# - Prerelease: "X.Y.Z-." (e.g. "3.2.9-beta.1", "3.3.0-rc.0") +# Prerelease titles publish a GitHub Release marked as a prerelease. +# The PR title becomes the release tag and title; the PR body becomes the release notes. # # Non-semver PR titles (feature PRs, bug fixes, etc.) are silently ignored — # the workflow runs but exits early without creating a release. @@ -27,22 +30,29 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - # ── Step 1: Validate that the PR title is a strict semver ────────────── + # ── Step 1: Validate that the PR title is a semver (stable or prerelease) ── # Exits without error when the title is not semver so non-release PRs - # pass silently. Sets outputs.version and outputs.is_release for - # downstream steps. + # pass silently. Sets outputs.version, outputs.is_release, and + # outputs.is_prerelease for downstream steps. - name: Validate semver PR title id: semver env: PR_TITLE: ${{ github.event.pull_request.title }} run: | if echo "$PR_TITLE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "PR title '$PR_TITLE' is a valid semver — proceeding with release." + echo "PR title '$PR_TITLE' is a stable semver — proceeding with release." echo "version=$PR_TITLE" >> "$GITHUB_OUTPUT" echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + elif echo "$PR_TITLE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+-[0-9A-Za-z-]+\.[0-9]+$'; then + echo "PR title '$PR_TITLE' is a prerelease semver — proceeding with prerelease." + echo "version=$PR_TITLE" >> "$GITHUB_OUTPUT" + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" else - echo "PR title '$PR_TITLE' is not strict semver (X.Y.Z) — skipping release." + echo "PR title '$PR_TITLE' is not semver (X.Y.Z or X.Y.Z-tag.N) — skipping release." echo "is_release=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi # ── Step 2: Checkout the merge commit on master ───────────────────────── @@ -112,17 +122,25 @@ jobs: # ── Step 9: Create GitHub Release ──────────────────────────────────────── # --target pins the tag to the exact merge commit SHA so concurrent # merges cannot cause the release tag to point at a different commit. + # When the PR title is a prerelease semver (X.Y.Z-tag.N), pass --prerelease + # so Obsidian's plugin browser does not offer it as a stable update. # Artifacts: main.js, manifest.json, styles.css - name: Create GitHub Release if: steps.semver.outputs.is_release == 'true' && steps.check_existing.outputs.exists != 'true' env: VERSION: ${{ steps.semver.outputs.version }} MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + IS_PRERELEASE: ${{ steps.semver.outputs.is_prerelease }} run: | + PRERELEASE_FLAG="" + if [ "$IS_PRERELEASE" = "true" ]; then + PRERELEASE_FLAG="--prerelease" + fi gh release create "$VERSION" \ --target "$MERGE_SHA" \ --title "$VERSION" \ --notes-file /tmp/release-notes.md \ + $PRERELEASE_FLAG \ main.js \ manifest.json \ styles.css