diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..69f75a8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +# Requires a `RELEASE_TOKEN` secret with `contents: write`. +# +# The default `GITHUB_TOKEN` can create a GitHub Release, but releases created +# by that token do not trigger downstream workflows. This workflow uses a +# dedicated token so the existing `publish.yml` release trigger still publishes +# to JSR and npm automatically. +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: denoland/setup-deno@v2 + + - uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Configure git author + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Install semantic-release + run: | + npm install --global \ + semantic-release \ + @semantic-release/changelog \ + @semantic-release/commit-analyzer \ + @semantic-release/exec \ + @semantic-release/git \ + @semantic-release/github \ + @semantic-release/release-notes-generator + + - name: Create GitHub release + run: semantic-release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..0bd8cf8 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,33 @@ +{ + "branches": [ + "main" + ], + "tagFormat": "v${version}", + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "deno run -A scripts/update_release_version.ts ${nextRelease.version}" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md", + "deno.jsonc" + ], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a8a2ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `@okikio/observables` will be documented in this file. + +Releases are generated from Conventional Commits by the `Release` workflow, so +each published version updates this changelog and the `version` field in +`deno.jsonc` together. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..d617b7a --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,102 @@ +# Release automation + +This repository now uses `semantic-release` to turn Conventional Commits on +`main` into version bumps, changelog updates, and published GitHub releases. +That keeps the existing publish workflow focused on the two package registries: +JSR and npm. + +## Why this replaces Forge here + +`okikio/undent` uses `@roka/forge` as a Deno task plus three workflows: + +1. a manual **bump** workflow that opens a version/changelog PR +2. a **release** workflow that creates a draft GitHub Release after that PR is + merged +3. a **publish** workflow that runs after the draft is manually published + +That flow works well for Deno workspaces, but it is heavier than this repo +needs: + +- it adds a bump PR before every release +- it adds a second manual draft-release step before publishing +- it only supports `deno.json`, while this repo currently uses `deno.jsonc` +- it still needs a personal access token so the draft release can trigger the + publish workflow + +For a single-package library with an existing `publish.yml` release trigger, +`semantic-release` is the smallest way to get automatic SemVer bumps and +autogenerated changelogs without that extra PR and draft-release ceremony. + +## Chosen release flow + +```text +push to main + │ + ├─► CI validates fmt, lint, types, docs, tests, and npm build + │ + └─► Release workflow runs semantic-release + │ + ├─► analyzes Conventional Commits + ├─► calculates the next SemVer version + ├─► updates CHANGELOG.md + ├─► updates deno.jsonc version + ├─► commits chore(release): x.y.z [skip ci] + └─► publishes a GitHub Release + │ + └─► publish.yml pushes the same version to JSR and npm +``` + +The workflow uses a dedicated `RELEASE_TOKEN` secret because releases created by +the default GitHub Actions token do not trigger downstream `release` workflows. +Using a token with `contents: write` keeps the existing `publish.yml` trigger +working. + +## Files involved + +- `.github/workflows/release.yml` runs `semantic-release` on pushes to `main` +- `.releaserc.json` controls versioning, changelog generation, and release + commits +- `scripts/update_release_version.ts` rewrites `deno.jsonc` during the release + prepare step +- `CHANGELOG.md` becomes the generated release history +- `.github/workflows/publish.yml` stays responsible for JSR and npm publishing + +## Alternatives considered + +### `release-please` + +Good option if you want a reviewable release PR. It updates changelogs and +versions reliably, but its release-PR model is still close to the Forge flow you +said you do not like. + +### `changesets` + +Excellent for monorepos and coordinated multi-package releases. It is more +manual here because every releasable PR needs a changeset file, which adds more +release bookkeeping than a single-package library needs. + +### `release-it` + +Flexible and mature, especially when combined with the conventional-changelog +plugin. It needs more custom scripting than `semantic-release` to infer the next +version from commit history and to keep `deno.jsonc` in sync. + +### JSR and Deno-native options + +- `@roka/forge`: best fit for Deno workspaces, but the bump-PR plus + draft-release flow is more hands-on than this repo wants +- `@deno/bump-workspaces`: promising for Deno workspaces, but it is + workspace-first and centered on pull-request driven bumps +- `@eser/codebase/release`: Deno-native and capable of version bump + changelog + orchestration, but you choose the bump type manually instead of deriving it + from commit history +- `@tene/version-bump`: good low-level version bump utility, but it does not + cover changelog generation and GitHub release creation by itself + +## One-time setup + +1. Add a `RELEASE_TOKEN` repository secret with `contents: write` +2. Keep branch protection pointed at the normal CI workflow +3. Merge Conventional Commit messages into `main` + +After that, the release workflow can stay hands-off for normal releases. diff --git a/scripts/update_release_version.ts b/scripts/update_release_version.ts new file mode 100644 index 0000000..e3140bb --- /dev/null +++ b/scripts/update_release_version.ts @@ -0,0 +1,24 @@ +/** + * Rewrites the version field in deno.jsonc for semantic-release. + */ + +const [version] = Deno.args; + +if (typeof version !== "string" || version.length === 0) { + throw new Error("Expected the next release version as the first argument."); +} + +const config_path = new URL("../deno.jsonc", import.meta.url); +const config_text = await Deno.readTextFile(config_path); +const version_pattern = /("version"\s*:\s*")[^"]*(")/; + +if (!version_pattern.test(config_text)) { + throw new Error('Expected deno.jsonc to contain a "version" field.'); +} + +const next_config_text = config_text.replace( + version_pattern, + `$1${version}$2`, +); + +await Deno.writeTextFile(config_path, next_config_text);