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.

What you’ll do
  1. Check the prerequisites
  2. Create a repo with a manifest and CHANGELOG
  3. Drop in the three wrapper workflows
  4. Allow Actions to open PRs
  5. Land your first feature commit
  6. Merge the release PR
  7. Wire up automatic publishing

1 · Prerequisites

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:

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:

6 · Merge the release PR

Review the PR. If something looks off, push more commits to mainprepare-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:

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.