Add a step that signs main.js, manifest.json, and styles.css with the
workflow's OIDC identity and publishes the attestation to Sigstore's
public transparency log via actions/attest-build-provenance@v2.
Why: Obsidian's plugin review tooling flags release assets without an
attestation as a soft warning. Attestations cryptographically prove the
asset was built by this workflow on this commit, defending against an
attacker who gains release-asset-replace permissions and silently swaps
in a compromised main.js. Verification post-release:
gh attestation verify main.js --owner logancyang --repo logancyang/obsidian-copilot
Placement: the new step runs AFTER the prerelease manifest swap and
BEFORE gh release create. That way the attested manifest.json is
exactly the bytes that get uploaded — for stable releases that's
the committed manifest.json; for prereleases it's the in-runner
copy of manifest-beta.json.
Permissions: adds attestations: write and id-token: write to the job.
The id-token: write permission is what allows the workflow to request
an OIDC token from GitHub; the attest action uses that token as proof
of identity when signing. No secrets to manage.
Free for public repos.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): make prerelease tooling stop poisoning master's manifest.json
Background: Obsidian's community plugin store reads manifest.json from master
to decide which GitHub Release to serve installers. The prerelease agent's
npm version bump was rewriting manifest.json with a prerelease version, which
broke plugin installs until hotfix #2428 reverted master's manifest.json to
the stable version.
This PR fixes the underlying tooling so it can't recur.
Changes:
- version-bump.mjs now branches on whether npm_package_version is a prerelease
(contains a hyphen). Stable: writes to manifest.json + versions.json. Prerelease:
writes to manifest-beta.json + versions.json, leaving manifest.json untouched.
Seeds manifest-beta.json from manifest.json when it doesn't exist yet.
On a stable bump that finds a stale manifest-beta.json, removes it (git rm)
because the new stable supersedes the in-flight beta. Stages the right files
itself; package.json no longer needs the explicit git-add step.
- package.json: simplifies the "version" lifecycle script to just
"node version-bump.mjs" since the script now stages files itself.
- release.yml:
* "Verify version matches manifest" step now selects the file based on
is_prerelease (manifest.json for stable, manifest-beta.json for prerelease).
For stable runs nothing changes.
* Adds a "Prepare release-asset manifest" step before the release upload that
copies manifest-beta.json over manifest.json IN THE RUNNER only when the
release is a prerelease. This makes the uploaded manifest.json asset carry
the prerelease version so testers sideloading the assets get a consistent
manifest. The committed master manifest.json is never touched by the
workflow (the workflow doesn't push back).
- .claude/agents/release.md: adds a Step 0 pre-flight assertion that master's
manifest.json.version equals the latest non-prerelease GitHub Release tag.
Catches drift loudly before doing any work. Notes the manifest-beta.json
auto-removal in the rules.
- .claude/agents/prerelease.md: documents the manifest.json / manifest-beta.json
split. Adds the same Step 0 drift assertion. Updates the git add step to
stage manifest-beta.json instead of manifest.json. Adds an explicit guard
rule that manifest.json must not change during a prerelease bump.
Verified locally by exercising version-bump.mjs in a throwaway repo:
- Stable 3.2.8 -> 3.2.9: writes manifest.json + versions.json, no beta side
- Prerelease 3.2.8 -> 3.2.9-beta.0: creates manifest-beta.json, manifest.json untouched
- Prerelease iteration 3.2.9-beta.0 -> 3.2.9-beta.1: updates manifest-beta.json,
manifest.json untouched
- Stable supersedes beta 3.2.9-beta.1 -> 3.2.9: writes manifest.json,
git-rm's manifest-beta.json
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(release): address Codex review on PR #2429
1. release.yml: when verifying a prerelease, also assert that the committed
manifest.json on the merge commit still equals the latest non-prerelease
GitHub Release tag. This catches an old-style version bump or hand edit
that accidentally modified master's manifest.json — without this guard,
the verify step only inspected manifest-beta.json so a poisoned
manifest.json could merge unnoticed and break the Obsidian plugin store.
2. version-bump.mjs: don't fail stable bumps when manifest-beta.json exists
on disk but is untracked (or has local modifications). Check git
trackedness first; use `git rm -f` only when tracked, fall back to a
plain unlink for working-tree-only files. Logs which path it took.
Verified both scenarios in a throwaway repo:
- Untracked manifest-beta.json + stable bump: unlinked, no git rm error
- Tracked manifest-beta.json + stable bump: staged deletion via git rm -f
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(release): use /releases/latest API for stable-drift guard
gh release list defaults to 30 results, so a cluster of 30+ prereleases
since the last stable would make the prerelease drift guard return empty
and fail valid prerelease publishes. Switch to GitHub's /releases/latest
endpoint instead, which by design returns only the most-recent
non-prerelease, non-draft release in a single call regardless of how
many prereleases have accumulated.
Applied the same fix to the Step 0 drift check in both the release agent
and prerelease agent doc for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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-<tag>.<N>'.
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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: add automated release workflow on PR merge
Triggers when a PR targeting master is merged with a strict semver title
(e.g., "3.2.3"). Builds the plugin and creates a GitHub Release with
main.js, manifest.json, and styles.css attached. Non-semver PR titles
are silently skipped. Includes manifest.json version match validation
and shell injection protection for release notes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pin release checkout to merge commit SHA
Avoids race condition where a concurrent PR merge could cause the release
workflow to build a different commit than the one that triggered it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prevent shell injection in release workflow and add idempotency check
Use env vars instead of inline ${{ }} expansion for PR title and version
outputs to prevent shell injection. Add pre-check to skip release creation
if the version tag already exists.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pin release tag to merge commit SHA via --target flag
Ensures gh release create tags exactly the merge commit, preventing
a source/binary mismatch if concurrent merges land before the step runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>