Advanced examples
Patterns for fitting rational-release around real projects:
mirrored changelogs, attached artefacts, gated pre-tasks, JSR / npm publish triggers,
and using the CLI directly outside the workflows.
Pre-tasks: gating the release PR
pre-tasks is a newline-separated shell script that runs before the
release PR is generated. If any line exits non-zero, the workflow fails and the
release PR is not opened or updated. Use it as your release gate.
jobs:
prepare:
uses: sigmadigitalza/rational-release/.github/workflows/prepare-release.yml@v1
with:
pre-tasks: |
deno fmt --check
deno lint
deno check src/mod.ts
deno test --allow-read --allow-net
If you have a heavier integration suite, run it here too — the worst case is the release PR doesn’t open until the suite is green.
Mirroring the changelog into a docs site
Many projects keep a docs site that wants the changelog rendered alongside other pages.
mirror-paths takes src:dst pairs and copies after changelog
generation, so the doc page tracks the source of truth on every release-PR update.
with:
mirror-paths: |
CHANGELOG.md:docs/changelog.md
CHANGELOG.md:website/src/pages/changelog.md
Adding generated files to the release commit
If your pre-tasks regenerates a search index, an OpenAPI spec, or a
bundled CSS file, those files need to be part of the prepare commit so they ship
with the release. List the paths in commit-paths:
with:
pre-tasks: |
deno task build # writes ui/search-index.json and dist/openapi.json
commit-paths: |
ui/search-index.json
dist/openapi.json
The version bump in the manifest, the changelog, and any mirrored paths are
always staged automatically. commit-paths is purely additive.
Attaching artefacts to the GitHub Release
cut-release can run a build step after tagging and attach the result as
release-asset downloads. Useful for binary distributions, source tarballs, or
anything you’d normally upload by hand.
jobs:
cut:
uses: sigmadigitalza/rational-release/.github/workflows/cut-release.yml@v1
with:
artefact-task: deno task package
artefact-paths: dist/*.tar.gz dist/checksums.txt
artefact-paths is a glob list. Anything matched is uploaded to the GitHub
Release alongside the auto-generated source archives.
Triggering JSR / npm publish from cut-release
Tags pushed by cut-release use the default GITHUB_TOKEN. GitHub
intentionally does not fire downstream push workflows
from tokens-pushed events — loop prevention. So a publish workflow listening on
push: tags: [v*] will never run.
The fix is workflow_run — allowed to fire from the same token-pushed event.
workflow_run jobs run in a privileged context (they have repo secrets and, for
JSR, id-token: write), so we don’t blindly check out the triggering
workflow’s head_sha — CodeQL flags that as
actions/untrusted-checkout. Instead, derive the version from the triggering
branch (release/vX.Y.Z), check out the freshly-pushed tag, and verify the
tag matches the manifest before publishing:
name: Publish to JSR
on:
workflow_run:
workflows: ["Cut Release"] # must match name: in cut-release.yml exactly
types: [completed]
permissions:
contents: read
id-token: write
jobs:
publish:
if: >-
github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'release/v')
runs-on: ubuntu-latest
steps:
- id: ver
env: { HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} }
run: |
set -euo pipefail
version="${HEAD_BRANCH#release/v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::bad branch ${HEAD_BRANCH}"; exit 1; }
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with: { ref: refs/tags/${{ steps.ver.outputs.tag }} }
- uses: denoland/setup-deno@v2
with: { deno-version: v2.x }
- env: { TAG: ${{ steps.ver.outputs.tag }} }
run: |
set -euo pipefail
v=$(deno eval --quiet --allow-read=deno.json 'console.log(JSON.parse(Deno.readTextFileSync("deno.json")).version)')
[ "v${v}" = "$TAG" ] || { echo "::error::tag $TAG != manifest v${v}"; exit 1; }
- run: deno publish
For npm, swap the last three steps for:
- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- env: { TAG: ${{ steps.ver.outputs.tag }} }
run: |
v=$(node -p "require('./package.json').version")
[ "v${v}" = "$TAG" ] || { echo "::error::tag $TAG != manifest v${v}"; exit 1; }
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Non-default manifest paths
Default is deno.json with $.version. Override either:
with:
manifest-path: package.json
manifest-jsonpath: $.version # default; shown for clarity
manifest-jsonpath takes a dot-path: $.version,
$.workspaces.api.version, etc. Array indexing and filters are not supported —
the version field for any single package is always at a dotted-path location in the wild.
Using the CLI standalone
The CLI underneath the workflows ships on JSR and uses only node:*
standard-library imports, so it runs on Deno, Node, and Bun unchanged. Useful for
one-off tasks — computing the next version locally, extracting a single
changelog section, or scripting outside GitHub Actions.
# Compute the next version from current commits
git log v1.2.3..HEAD --pretty=%s | \
deno run -A jsr:@sigmadigitalza/rational-release \
next-version deno.json
# Extract a single release’s notes
deno run -A jsr:@sigmadigitalza/rational-release \
extract-section CHANGELOG.md 1.2.3
The full subcommand list:
| Subcommand | Purpose |
|---|---|
next-version |
Read the current version from the manifest, walk commit subjects from stdin
(or --commits-file), print the next semver. |
read-version |
Print the current version from the manifest. |
set-version |
Update the version field in the manifest, preserving indentation and trailing newline. |
changelog-generate |
Rewrite the [Unreleased] section from a JSON list of merged PRs.
Pass --bootstrap to create a Keep-a-Changelog skeleton if missing. |
changelog-finalise |
Promote [Unreleased] to [X.Y.Z] - date and prepend a
fresh empty block. |
extract-section |
Print the body of a single [X.Y.Z] section to stdout. Useful for
release-notes bodies. |
Cloud agents and JSR
If you run releases from a cloud agent that operates behind a network allowlist (GitHub-hosted Copilot agents, sandboxed CI runners), make sure the agent can reach both of these hosts:
https://dl.deno.land/— the agent installs Deno from here before any CLI subcommand can run.https://jsr.io/— the workflows resolve the CLI asjsr:@sigmadigitalza/rational-releasefrom JSR.
For Copilot specifically, add both URLs in
Settings → Copilot → Coding agent → Custom allowlist on the
consumer repository (admins only). Without them, Copilot's review/PR-edit jobs will
fail with a firewall block on dl.deno.land and never reach the CLI.
On unrestricted GitHub-hosted Actions runners this is already the default, so no
change is needed there.
Pinning to an exact version
Workflows reference @v1 by default — a moving tag that re-points
on additive changes within the v1 input shape. If you want immutability:
uses: sigmadigitalza/rational-release/.github/workflows/prepare-release.yml@v1.0.0
Pinning loses you the implicit security patches that ride with @v1, but
you trade that for total reproducibility. Either is reasonable; pick the trade
consciously.