Zero to hero
Wire rational-release into a fresh repository and ship your first tagged release.
End to end, this walkthrough takes about ten minutes — most of which is GitHub
catching up with you.
1 · Prerequisites
- A GitHub repository you own (private or public).
- A JSON manifest with a
versionfield —deno.json,package.json, orjsr.jsonall work. - Squash-merge enabled on the repo. The release-PR workflow assumes
type(scope): description-shaped subjects, which squash-merge produces from the PR title.
You don’t need to install anything locally. The CLI ships as a JSR module and runs from the workflows directly.
2 · Set up the repo
Create a deno.json with a starting version:
{
"name": "@you/widget",
"version": "0.0.0"
}
A CHANGELOG.md is optional — rational-release auto-bootstraps a
Keep-a-Changelog skeleton on first run. If you’d rather provide your own,
drop a file in with at least:
# Changelog
## [Unreleased]
### Added
### Changed
### Fixed
### Removed
3 · Drop in the workflows
Three thin wrappers under .github/workflows/. None take more than a few lines.
validate-pr.yml
Reports on (or gates) PR titles against the conventional-commits spec.
name: Validate PR
on:
pull_request:
types: [opened, edited, synchronize, reopened]
jobs:
title:
uses: sigmadigitalza/rational-release/.github/workflows/validate-pr.yml@v1
with:
gate: true # fail the check on bad titles; default is report-only
prepare-release.yml
Fires on every push to main. Computes the next semver, regenerates
[Unreleased], applies the bump to the manifest, force-pushes a
release/vX.Y.Z branch, and opens (or updates) a Release vX.Y.Z PR.
name: Prepare Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
prepare:
uses: sigmadigitalza/rational-release/.github/workflows/prepare-release.yml@v1
with:
pre1-cap: true # while < 1.0, treat feat!: as minor
pre-tasks: |
deno task test
cut-release.yml
Fires when a release/v* PR is merged. Tags vX.Y.Z, finalises the
changelog, and opens a GitHub Release.
name: Cut Release
on:
pull_request:
types: [closed]
branches: [main]
permissions:
contents: write
pull-requests: read
jobs:
cut:
uses: sigmadigitalza/rational-release/.github/workflows/cut-release.yml@v1
4 · Allow Actions to open PRs
In Settings → Actions → General → Workflow permissions:
- Pick Read and write permissions.
- Tick Allow GitHub Actions to create and approve pull requests.
Without that second box, prepare-release will hit a 403 when it
tries to open the release PR. The error is unambiguous when it happens.
5 · Land your first feature
Open a PR with a conventional-commit-shaped title:
feat: add <something useful>
Squash-merge it. The squash commit subject becomes
feat: add <something useful> (#1), which is what
prepare-release reads on the next push to main.
Within a minute or so, you’ll see:
- A
release/v0.1.0branch (orv1.0.0if you setpre1-cap: falseand pushed a breaking change). - A Release v0.1.0 PR with the manifest bump and a populated
[Unreleased]section.
6 · Merge the release PR
Review the PR. If something looks off, push more commits to main —
prepare-release will force-update the release branch on each push and the
PR will reflect the new state.
When you’re happy, squash-merge it. cut-release picks it up:
- Promotes
[Unreleased]→[0.1.0] - YYYY-MM-DDin the changelog. - Tags
v0.1.0onmain. - Creates a GitHub Release whose body is the
[0.1.0]section.
That’s a complete release cycle. Every subsequent feature follows the same path.
7 · Add automatic publishing
Tags pushed by cut-release use the default GITHUB_TOKEN, and GitHub
intentionally does not fire downstream push workflows from those tags. A
naïve on: push: tags: [v*] publish workflow will never run.
Trigger your publish workflow with workflow_run instead:
name: Publish to JSR
on:
workflow_run:
workflows: ["Cut Release"]
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: |
version="${HEAD_BRANCH#release/v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::bad 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 }
- run: deno publish
The workflows: entry must match the name: field in
cut-release.yml exactly — Cut Release in the example above.
workflow_run jobs run in a privileged context, so we derive the version from
head_branch and check out the freshly-pushed tag rather than
head_sha — this avoids the CodeQL actions/untrusted-checkout
pattern. See Advanced → Publish flow for a fuller version
that also verifies the tag against the manifest.
You’re done
From here, the loop is: write conventional-commit PR titles, squash-merge them, review the auto-opened release PR, squash-merge it. Everything else — semver bump, changelog generation, tag, release notes, publish — runs itself.
For artefacts, mirror paths, custom pre-tasks, or fitting the workflows around a multi-package repo, head to the advanced examples.