From 4d1ceb72ce207f5f33110b28b7974dd716b292a5 Mon Sep 17 00:00:00 2001 From: SongshGeo Date: Wed, 5 Aug 2026 11:38:23 +0200 Subject: [PATCH] ci: release automatically when the release PR is merged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #22 merged to main and nothing shipped: release.yml only fires on a pushed tag, and the tag was a manual `npm version` step that is easy to forget. Two fixes sat on main unreleased until someone noticed. release-please now recomputes the next version from the conventional commits on main and keeps a release PR open with the bumped package.json, manifest.json, manifest-beta.json and a generated CHANGELOG.md. Merging that PR is the release: it tags, publishes, attaches main.js/manifest.json/styles.css, and appends the versions.json entry. An ordinary merge — docs, chores — cannot mint a version, which is why this is not simply "tag every push to main". versions.json is filled in by version-bump.mjs, the same script the manual flow uses: its key is the version itself, so no generic file updater can write it. Obsidian reads that file from the default branch rather than from the release, so landing it one commit after the tag is harmless. release.yml stays as the manual escape hatch. The tag release-please creates cannot reach it — GitHub does not re-trigger workflows for refs pushed with GITHUB_TOKEN — so the two paths can never publish the same release twice. Co-Authored-By: Claude Opus 5 --- .github/workflows/release-please.yml | 121 +++++++++++++++++++++++++++ .github/workflows/release.yml | 6 ++ .release-please-manifest.json | 3 + MAINTAINING.md | 26 ++++-- release-please-config.json | 31 +++++++ 5 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..8b08a51 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,121 @@ +# Automated releases, driven by conventional commits. +# +# Every push to main, release-please recomputes the next version from the +# commits since the last tag (`fix:` → patch, `feat:` → minor, `!`/BREAKING → +# major, all within the prerelease line configured in release-please-config.json) +# and keeps a "chore(main): release " PR open with the bumped +# package.json / manifest.json / manifest-beta.json and a generated CHANGELOG.md. +# Merging that PR is the release: release-please tags it and publishes the +# GitHub release, then the jobs below attach the built plugin files and record +# the version in versions.json. +# +# Nothing is released by merging an ordinary PR — only by merging the release +# PR — so a docs-only merge cannot mint a version. +# +# The tag release-please creates does NOT fire .github/workflows/release.yml: +# GitHub does not re-trigger workflows for refs pushed with GITHUB_TOKEN. That +# file remains the manual escape hatch for a hand-pushed tag; this one is the +# automated path. Keep the two in sync if the build or the asset list changes. +name: Release Please + +on: + push: + branches: ["main"] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + version: ${{ steps.release.outputs.version }} + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # The release exists but is empty until this runs: Obsidian installs a plugin + # from these three files, so a release without them is unusable. + publish-assets: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + + - name: Use Node.js 20.x + uses: actions/setup-node@v5 + with: + node-version: "20.x" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build plugin + run: npm run build + + # Same assertion release.yml makes: manifest.json ships inside the release + # and Obsidian trusts its version field, so a drift between it and the tag + # would install as the wrong version. + - name: Verify manifest version matches tag + run: | + tag="${{ needs.release-please.outputs.tag_name }}" + manifest_version=$(node -p "require('./manifest.json').version") + if [ "$tag" != "$manifest_version" ]; then + echo "::error::Tag '$tag' does not match manifest.json version '$manifest_version'." + exit 1 + fi + + - name: Attach plugin files to the release + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ needs.release-please.outputs.tag_name }}" \ + main.js manifest.json styles.css --clobber + + # versions.json maps every published plugin version to the minimum Obsidian + # version it needs. release-please cannot write it — the key is the version + # itself, not a fixed path — so version-bump.mjs (the same script the manual + # `npm version` flow uses) appends the entry afterwards. Obsidian reads this + # file from the default branch, not from the release, so landing it one commit + # after the tag is fine. + record-version: + needs: [release-please, publish-assets] + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: main + + - name: Use Node.js 20.x + uses: actions/setup-node@v5 + with: + node-version: "20.x" + + - name: Record the release in versions.json + env: + npm_package_version: ${{ needs.release-please.outputs.version }} + run: node version-bump.mjs + + - name: Commit if anything changed + run: | + if git diff --quiet; then + echo "versions.json already records ${{ needs.release-please.outputs.version }}." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add manifest.json manifest-beta.json versions.json + git commit -m "chore: record ${{ needs.release-please.outputs.version }} in versions.json" + git push origin main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0290ab4..b3a99e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,9 @@ +# The manual release path: publishes whatever tag you push by hand, after +# `npm version `. The automated path is .github/workflows/release-please.yml, +# which tags and publishes when its release PR is merged — a tag it creates does +# not reach this workflow (GitHub does not re-trigger on GITHUB_TOKEN pushes), so +# the two cannot both publish one release. Keep the build and asset list here in +# step with that file. name: Release on: diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..5fd05df --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "2.4.0-beta.3" +} diff --git a/MAINTAINING.md b/MAINTAINING.md index 5bf3d06..6f748b8 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -87,12 +87,26 @@ and skips entirely when no bundle is installed. - Beta channel: install via [BRAT](https://github.com/TfTHacker/obsidian42-brat) against this repo; BRAT reads `manifest-beta.json`. -- Cut a release: `npm version ` → `version-bump.mjs` syncs `manifest.json` + - `versions.json` (and `manifest-beta.json`) → push the tag. `.github/workflows/release.yml` verifies - the tag equals the manifest version and publishes `main.js`, `manifest.json`, `styles.css`. A `-` - in the tag marks it a GitHub prerelease. -- Keep `manifest.json`, `manifest-beta.json`, and `package.json` versions in sync — the release - workflow fails the build if the tag and `manifest.json` disagree. +- **Normal path — merge the release PR.** Every push to `main`, + `.github/workflows/release-please.yml` recomputes the next version from the conventional commits + since the last tag and keeps a `chore(main): release ` PR open, containing the bumped + `package.json` / `manifest.json` / `manifest-beta.json` and a generated `CHANGELOG.md`. Merging + that PR *is* the release: it tags, publishes the GitHub release, attaches `main.js`, + `manifest.json`, `styles.css`, and appends the `versions.json` entry. Merging any other PR never + cuts a version. +- **Write commit subjects release-please can read.** `fix:` → patch, `feat:` → minor, `!` or a + `BREAKING CHANGE:` footer → major — all within the prerelease line configured in + `release-please-config.json` (currently `2.4.0-beta.N`). To pin a version by hand, put + `Release-As: 2.5.0` in a commit body. To graduate the beta line to a stable `2.4.0`, drop + `"prerelease": true` (and `"versioning": "prerelease"`) from that config in the same PR. +- **Manual path — push a tag.** `npm version ` → `version-bump.mjs` syncs + `manifest.json` + `versions.json` (and `manifest-beta.json`) → push the tag. + `.github/workflows/release.yml` verifies the tag equals the manifest version and publishes the + same three files. A `-` in the tag marks it a GitHub prerelease. Use this only when you need a + release the commit history would not produce; afterwards set `.release-please-manifest.json` to + the version you published, or release-please will keep proposing it. +- Keep `manifest.json`, `manifest-beta.json`, and `package.json` versions in sync — both release + workflows fail the build if the tag and `manifest.json` disagree. ## Localization (i18n) diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..8fb289c --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "packages": { + ".": { + "package-name": "paperout-to-authors", + "extra-files": [ + { "type": "json", "path": "manifest.json", "jsonpath": "$.version" }, + { "type": "json", "path": "manifest-beta.json", "jsonpath": "$.version" } + ] + } + }, + "include-v-in-tag": false, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "prerelease-type": "beta", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "refactor", "section": "Refactoring" }, + { "type": "docs", "section": "Documentation" }, + { "type": "revert", "section": "Reverts" }, + { "type": "build", "section": "Build & Tooling" }, + { "type": "ci", "section": "Build & Tooling" }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "style", "section": "Styling", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true } + ] +}