From 8dff24e71eaed61a6be8fb59d39846c148585a7c Mon Sep 17 00:00:00 2001 From: banisterious Date: Tue, 12 May 2026 15:21:28 -0700 Subject: [PATCH] feat(ci): release workflow with build-provenance attestations Adopt the same release pipeline that landed in Draft Bench, adapted to this plugin's existing scripts and assets. What lands together: - `.github/workflows/release.yml`. Tag-push trigger matching plain SemVer (`1.6.6`) and pre-release SemVer (`1.6.6-rc1`). Runs `npm ci`, `npm run lint` (the obsidianmd ruleset), and `npm run build`. Calls `actions/attest-build-provenance@v2` on `main.js`, `manifest.json`, and `styles.css` so each release asset carries a verifiable GitHub artifact attestation. Creates a draft release with the three assets attached; pre-release tags get `--prerelease`. The draft step preserves the editorial gate - release-description markdown is pasted into the draft by hand before publishing, keeping writing-style review on every release. - `.nvmrc` pinning Node 22.20.0 (matches local dev). Both CI and local environments resolve the same Node via `actions/setup-node@v4`'s `node-version-file: '.nvmrc'`. - `docs/developer/release.md` rewritten to document the CI flow: required artifacts, the trial-run procedure with `-rc1` tags, the attestation-verification command, and the recovery path for a failed CI run. Customizations from the reference workflow: - Dropped the `lint:css` step. The plugin has CSS sources but no stylelint config; adding one is real follow-up work and the community-site rescan covers CSS lint server-side until that lands. - Dropped the `test` step. No test infrastructure yet; that's Phase 4 of the audit plan. The step is straightforward to add later. Release assets are the standard Obsidian three: `main.js` (built fresh by CI), `manifest.json` (committed), `styles.css` (committed). --- .github/workflows/release.yml | 73 +++++++++++++++++++++ .nvmrc | 1 + docs/developer/release.md | 117 ++++++++++++++++++++++++++++++---- 3 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .nvmrc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c15c21d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,73 @@ +name: Release + +on: + push: + tags: + # Plain SemVer tags (release): 1.6.6, 2.0.0 + - '*.*.*' + # Pre-release tags (rc / beta / alpha): 1.6.6-rc1, 2.0.0-beta.2 + - '*.*.*-*' + +# `gh release create` needs write access; the attestation action +# needs an OIDC token and attestation upload permission. +permissions: + contents: write + id-token: write + attestations: write + +# Prevent a second run for the same tag from racing the first. +# Cancelling in-progress means a later push wins. +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint (eslint) + run: npm run lint + + - name: Build + run: npm run build + + - name: Attest provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + main.js + manifest.json + styles.css + + - name: Create draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # SemVer pre-release tags carry a hyphen after the version + # core (e.g., 1.6.6-rc1). Plain release tags do not. + PRERELEASE_FLAG="" + if [[ "$GITHUB_REF_NAME" == *-* ]]; then + PRERELEASE_FLAG="--prerelease" + fi + # Draft + empty notes: the author pastes the audited + # release-description markdown into the draft on the web + # UI before publishing. CI's job ends with assets uploaded + # and the release in draft state. + gh release create "$GITHUB_REF_NAME" \ + --title "$GITHUB_REF_NAME" \ + --draft \ + --notes "" \ + $PRERELEASE_FLAG \ + main.js manifest.json styles.css diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..442c758 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.20.0 diff --git a/docs/developer/release.md b/docs/developer/release.md index 384eff0..34e9037 100644 --- a/docs/developer/release.md +++ b/docs/developer/release.md @@ -3,7 +3,10 @@ ## Table of contents - [Required release artifacts](#required-release-artifacts) +- [Release flow (CI-driven)](#release-flow-ci-driven) +- [Trial-run procedure](#trial-run-procedure) - [Verifying a release locally](#verifying-a-release-locally) +- [Recovering from a failed run](#recovering-from-a-failed-run) ## Required release artifacts @@ -18,10 +21,85 @@ Every GitHub release for this plugin attaches exactly three files: The Obsidian in-app community catalog fetches these three URLs from a release's assets to install or update the plugin. No additional files should be attached: no source zip, no debug bundle, no archive of the -entire repository. The audit plan's Phase 0 was triggered in part by -finding a `custom-selected-word-count-v1.6.2.zip` source archive in -the repo root, which is not part of how Obsidian installs plugins and -should not appear in releases. +entire repository. + +`main.js` and `main.js.map` are gitignored. Each release is built +fresh by CI from the tagged source. `styles.css` and `manifest.json` +are committed source files. + +## Release flow (CI-driven) + +The release pipeline lives at `.github/workflows/release.yml`. The +trigger is a tag push matching plain SemVer (`1.6.6`, `2.0.0`) or a +pre-release SemVer (`1.6.6-rc1`, `2.0.0-beta.2`). On any matching +tag the workflow: + +1. Checks out the tagged commit and installs dependencies with + `npm ci`. +2. Runs `npm run lint` (ESLint, including the + `eslint-plugin-obsidianmd` ruleset). +3. Runs `npm run build` (typecheck plus esbuild production bundle). +4. Calls `actions/attest-build-provenance@v2` against `main.js`, + `manifest.json`, and `styles.css` so each release asset carries a + verifiable GitHub artifact attestation. +5. Creates a **draft** GitHub release with those three files + attached. Pre-release tags get `--prerelease`; plain SemVer tags + do not. + +The author then opens the draft release on the GitHub web UI, pastes +the audited release-description markdown, and clicks Publish. The +draft step is intentional: it preserves the editorial gate so the +release description follows the project's writing-style conventions +(minimal em-dashes, ASCII arrows, no AI attribution). + +### Cutting a release + +```sh +# On main, with a clean working tree: +npm --tag-version-prefix= version patch # or minor / major +git push origin main +git push origin # e.g. 1.6.6 +``` + +The `npm --tag-version-prefix=` flag matches the repo's plain +SemVer tag convention. The project's `version` lifecycle script +synchronizes `manifest.json` and `versions.json` with the bumped +`package.json` version. + +Once the tag is pushed, the workflow runs (~3-5 minutes) and the +draft release appears on the repo's Releases page. + +## Trial-run procedure + +Before tagging the actual next release, push a release-candidate tag +first to validate the pipeline end-to-end: + +```sh +git push # any pending commits +git tag -rc1 # e.g. 1.6.6-rc1 +git push origin -rc1 +``` + +Verify the run in the Actions tab. The workflow should: + +1. Pass all steps with green checkmarks. +2. Produce a draft release flagged "Pre-release" with the three + assets attached. +3. Carry a valid attestation. Download and verify with: + ```sh + gh release download -rc1 --pattern main.js + gh attestation verify main.js --repo banisterious/obsidian-custom-selected-word-count + ``` + Expected output: `Verification succeeded!` + +Cleanup the rc once verified: + +```sh +gh release delete -rc1 --yes --cleanup-tag +``` + +`--cleanup-tag` removes both the local and remote tags in a single +command. ## Verifying a release locally @@ -29,13 +107,28 @@ Before tagging: - `npm run build` produces a clean `main.js` (typecheck plus esbuild bundle). -- `npm run lint` runs to completion. The audit plan's Phase 1 through - Phase 3 clear the current outstanding findings progressively; new - errors should be addressed or explicitly scoped before release. +- `npm run lint` runs to completion. The catalog-acceptance ruleset + is `eslint-plugin-obsidianmd`; new errors flagged by that ruleset + should be addressed before tagging. - `CHANGELOG.md`'s `[Unreleased]` section reflects everything that - will ship with the tag, then is moved under a dated version heading - at release time. + will ship with the tag, then is moved under a dated version + heading at release time. -A future Phase 6 task adds a GitHub release workflow with build- -provenance attestations. Until then, releases are cut manually from -the maintainer's workstation. +## Recovering from a failed run + +If CI fails after a tag is pushed (lint regression, build break, +attestation step error), the published tag is wrong and the draft +release should be discarded: + +```sh +# Delete the bad tag locally and on the remote: +git tag -d +git push --delete origin + +# If a draft release was created, delete it (the assets it carries +# are stale): +gh release delete --yes +``` + +Fix the underlying issue, commit, push, then re-tag and push the +tag. The workflow re-runs from scratch.