Skip to content

outfitter-dev/release-action

Repository files navigation

Outfitter Release Action

Reusable, policy-gated release automation for Outfitter projects. One mechanism, maintained once, consumed by every @outfitter/* repo (and eventually Trails and Skillset).

A Changesets "Version Packages" PR opens and updates the pending release. When that PR merges, a verify-don't-trust policy engine independently re-derives the facts — branch, bot identity, the exact generated diff, CI status, changelog, version delta, and registry state — and only then publishes. A human label states intent; the machine verifies reality. Anything ambiguous routes to a manual-approval environment instead of publishing.

Generalized for multi-package, lockstep-versioned workspaces published with bun publish (so workspace:/catalog: ranges resolve). See ADR-0001 in the monorepo for the design rationale.

Usage

Add one workflow to a consumer repo:

# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  release:
    uses: outfitter-dev/release-action/.github/workflows/release.yml@v1
    with:
      # Bumps versions + writes changelogs from pending changesets.
      version-command: bunx changeset version
      # Optional build before publish.
      build-command: bun run build
      # CI check-run names that must pass before an auto publish.
      required-checks: "check"
    secrets:
      npm-token: ${{ secrets.NPM_TOKEN }}

The action does the rest: opens/maintains the version PR, and — once it merges — plans, gates, publishes (auto or manual), and cuts the tagged GitHub release.

How it works

push main
   │
   ▼
version ─────────────► opens/updates the "Version Packages" PR (changesets/action),
   │  has changesets?   labels it with conservative release intent
   │  no  (PR merged)
   ▼
plan ────────────────► reads the npm registry; is there anything unpublished?
   │
   ▼
policy ──────────────► verify-don't-trust: re-derives every fact and decides
   │                    auto │ manual │ none │ block
   ├── auto ──► publish-auto   (environment: npm-auto, NPM_TOKEN)
   └── manual ► publish-manual (environment: npm, protected approval)
   │
   ▼
github-release ──────► tag-authoritative GitHub release at the version commit

Label model

Intent is expressed with labels on the version PR (and stack:boundary on the source PRs that contributed the changesets):

  • Publish: publish:auto · publish:manual · publish:block · publish:none
  • Channel: channel:stable · channel:preview · channel:canary
  • Release: release:major · release:minor · release:patch
  • Stack (source PRs): stack:boundary

The decision resolves, in order, to block (conflicting/unknown labels, registry drift, or explicit publish:block), none (publish:none with an audit reason), manual (the default, or publish:manual), or auto (only publish:auto and every gate condition holds).

The auto-publish gate

publish:auto is a request, not an authorization. Automatic publish happens only when all of these hold — otherwise the release downgrades to manual approval (it never silently publishes):

  • the generated diff is exactly the expected version-bump file set (the discovered packages' package.json + CHANGELOG.md, plus a consumed .changeset/*.md) and nothing else;
  • every consumed changeset source PR carries stack:boundary;
  • repository and branch match the expected release context;
  • channel is stable and the dist-tag is latest;
  • the version PR is the bot's PR with the expected title and identity;
  • the changelog contains the new version heading;
  • the version delta is positive and consistent with any release:* label;
  • the required CI checks passed at the exact release SHA.

Versioning model

Lockstep: all publishable packages share one version and bump together. This keeps dist-tag selection, registry-completeness, and tagging to a single tuple. Independent per-package versioning is not supported yet.

Setup checklist

A consumer repo needs:

  1. Changesets configured (.changeset/config.json).
  2. An NPM_TOKEN secret (granular automation token) — Bun's publisher resolves workspace:/catalog:, which npm OIDC cannot, so a token is used rather than Trusted Publishing.
  3. Two environments: npm-auto (used by the auto path) and npm (protected, manual approval).
  4. The required-checks input naming the CI check-run(s) that gate an auto publish.

Development

bun install
bun run check        # format + markdown + typecheck + tests
bun run build        # bundle src/ -> dist/index.js (committed; the Action runs it)

dist/index.js is committed on purpose: GitHub Actions runs the bundled action directly, without installing dependencies.

About

Reusable release Action for Outfitter projects — changesets version PR + verify-don't-trust, policy-gated, multi-package publish

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors