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 } + ] +}