Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GitHub automation

monochange keeps source-provider automation layered on top of the same PrepareRelease result used for normal release planning.

That means one set of .changeset/*.md inputs can drive all of these commands and automation flows consistently:

  • monochange step prepare-release --dry-run --format json refreshes the cached manifest and shows the downstream automation payload
  • monochange step publish-release previews or publishes provider releases from the structured release notes
  • monochange step open-release-request previews or opens an idempotent provider release request; when [source.pull_requests].verified_commits = true and the step runs on GitHub Actions for the configured repository, the GitHub provider pushes a normal release branch commit as a fallback and then only moves the branch to a Git Database API replacement commit when GitHub reports that replacement as verified
  • monochange step affected-packages evaluates pull-request changeset policy from CI-supplied changed paths and labels without requiring a config-defined wrapper command

Quick start with monochange init --provider

The fastest way to configure GitHub automation is using the --provider flag during initialization:

# Initialize with GitHub automation pre-configured
monochange init --provider github

# The generated monochange.toml includes:
# - [source] section with GitHub releases and pull request settings
# - CLI commands for commit-release and release-pr
# - GitHub Actions workflows in .github/workflows/

This single command generates:

  1. Complete source configuration - [source], [source.releases], and [source.pull_requests] sections
  2. Automation CLI commands - commit-release and release-pr commands ready to use
  3. GitHub Actions workflows - release.yml and changeset-policy.yml for CI/CD
  4. Auto-detected repository info - parses your git remote to pre-fill owner and repo

CLI commands

monochange step prepare-release --dry-run --format json
monochange step publish-release --dry-run --format json
monochange step open-release-request --dry-run --format json
monochange step affected-packages --format json --verify --changed-paths crates/monochange/src/lib.rs

Inspecting and repairing a recent release

GitHub automation has a repair-oriented history flow in addition to the existing manifest-driven execution flow.

Use these commands when you need to inspect, tag, or repair a just-created release:

monochange step release-record --from v1.2.3
monochange step tag-release --from HEAD --dry-run --format json
monochange step retarget-release --from v1.2.3 --target HEAD --dry-run
monochange step retarget-release --from v1.2.3 --target HEAD

The important distinction is:

  • the cached release manifest still describes the execution-time release plan for automation
  • ReleaseRecord describes the durable release declaration stored in the release commit body
  • monochange step tag-release consumes that durable record after merge and creates the declared tag set on the default branch

Use --dry-run first for monochange step retarget-release. It is a destructive workflow because it retargets release tags.

If immutable registry artifacts have already been published, prefer cutting a new patch release instead of retargeting the source release.

Tag-release JSON for follow-up workflows

When a post-merge workflow needs to trigger follow-up release work, prefer monochange step tag-release --from HEAD --format json and read the release tag by package or group id from the top-level tags object:

{
	"tags": {
		"main": "v1.2.3",
		"sdk": "sdk/v1.2.3"
	}
}

name/version examples such as sdk/v1.2.3 correspond to a tag template like {{ name }}/v{{ version }}.

The tags object is intentionally flat because package ids and group ids share the same monochange namespace. A workspace cannot have both a package and a group with the same id, so workflows do not need separate tags.packages and tags.groups branches or prefixed lookup keys. This makes automation stable and explicit: use .tags.<id> for the package or group whose release should drive the next step.

A package or group might not be released in a particular release commit. Handle that by checking whether tags has an entry for the id you care about. If there is no tag attached to that id, you can assume that release did not include that package or group and skip that follow-up workflow.

For example, a repository with [group.main] can trigger a downstream GitHub release workflow from the main group tag with:

monochange step tag-release --from HEAD --format json >/tmp/tag-report.json
tag="$(jq -r '.tags.main // empty' /tmp/tag-report.json)"

if [ -z "$tag" ]; then
  echo "No main group tag found in tag-report.json, skipping release trigger"
  exit 0
fi

gh workflow run release.yml --ref "$tag" -f tag="$tag"

Avoid indexing tagResults[0] for workflow control. tagResults remains the audit log of tag operations, while tags is the stable id-addressable map for automation.

Package publishing and trusted publishing

Package publishing is separate from provider release publishing:

  • monochange step publish-readiness --from HEAD --output <path> checks package registries before mutation
  • monochange step publish-packages handles package registries such as crates.io, npm, jsr, and pub.dev
  • monochange step publish-release handles hosted source-provider releases such as GitHub releases

When publish.trusted_publishing is enabled, monochange can derive GitHub trust metadata from the workflow runtime and the configured [source] block. npm packages get the richest built-in diagnostics:

  • monochange rejects long-lived npm token environment variables for trusted-publishing runs
  • monochange verifies that the current GitHub Actions OIDC context matches the configured trusted-publisher context
  • if npm trust needs repair, monochange reports the exact npm trust github ... command to run manually or in separate tooling
  • real monochange step publish-packages runs do not execute npm trust or npm trust list; trusted-publishing npm publishes use the npm CLI directly

For crates.io, jsr, and pub.dev, monochange reports the setup URL for the package and requires manual trusted-publishing setup before the next built-in release publish. Placeholder publishing can still proceed so the package exists before that manual step.

For exact registry-side setup steps and field mappings, see Trusted publishing and OIDC.

For full GitHub and GitLab CI examples by ecosystem, including npm, Cargo, Deno/JSR, and Dart/pub.dev, see Advanced: CI, package publishing, and release PR flows.

Release notes, GitHub releases, and release PRs

[defaults.changelog]
path = "{{ path }}/changelog.md"
format = "keep_a_changelog"

[changelog]
templates = [
	"#### {{ summary }}\n\n{{ details }}\n\n{{ context }}",
	"#### {{ summary }}\n\n{{ context }}",
	"#### {{ summary }}\n\n{{ details }}",
	"- {{ summary }}",
]

[group.main.changelog]
path = "changelog.md"
format = "monochange"

[source]
provider = "github"
owner = "ifiokjr"
repo = "monochange"

[source.releases]
enabled = true
source = "monochange"

[source.releases]
branches = ["main"]
enforce_for_tags = true
enforce_for_publish = true
enforce_for_commit = false
changeset_context_timeout_seconds = 120

[source.pull_requests]
enabled = true
branch_prefix = "monochange/release"
base = "main"
title = "chore(release): prepare release"
labels = ["release", "automated"]
auto_merge = false

[cli.publish-release]
help_text = "Prepare a release and publish provider releases"

[[cli.publish-release.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.publish-release.steps]]
type = "PrepareRelease"
inputs = ["format"]

[[cli.publish-release.steps]]
type = "PublishRelease"

[[cli.publish-release.steps]]
type = "CommentReleasedIssues"

[cli.release-pr]
help_text = "Prepare a release and open or update a provider release request"

[[cli.release-pr.inputs]]
name = "format"
type = "choice"
choices = ["text", "json", "json-min"]
default = "text"

[[cli.release-pr.steps]]
type = "PrepareRelease"
inputs = ["format"]

[[cli.release-pr.steps]]
type = "OpenReleaseRequest"
inputs = ["format"]

When you want fine-grained changelog formatting instead of the default {{ context }} block, GitHub-backed release notes can reference individual metadata fields such as {{ change_owner_link }}, {{ review_request_link }}, {{ introduced_commit_link }}, {{ closed_issue_links }}, and {{ related_issue_links }}. Those variables render markdown links when host URLs are available, so generated changelogs can point directly at the responsible actor, the PR, and linked issues. The source changeset path stays available through {{ changeset_path }}, but {{ context }} keeps that transient file path out of the default rendered note.

[source]
provider = "github"
owner = "ifiokjr"
repo = "monochange"

[changesets.affected]
enabled = true
required = true
skip_labels = ["no-changeset-required"]
comment_on_failure = true
changed_paths = [
	"crates/**",
	".github/**",
	"Cargo.toml",
	"Cargo.lock",
	"devenv.nix",
	"devenv.yaml",
	"devenv.lock",
	"monochange.toml",
	"codecov.yml",
	"deny.toml",
	"scripts/**",
	"npm/**",
	"skills/**",
]
ignored_paths = [
	".changeset/**",
	"docs/**",
	"specs/**",
	"readme.md",
	"CONTRIBUTING.md",
	"license",
]

name = "docs"
trigger = "release_published"
workflow = "docs-release"
environment = "github-pages"
release_targets = ["main"]
requires = ["main"]
metadata = { site = "github-pages" }

name = "format"
type = "choice"
choices = ["text", "json"]
default = "text"

type = "PrepareRelease"

[cli.affected]
help_text = "Evaluate pull-request changeset policy"

[[cli.affected.inputs]]
name = "format"
type = "choice"
choices = ["text", "json"]
default = "text"

[[cli.affected.inputs]]
name = "changed_paths"
type = "string_list"
required = true

[[cli.affected.inputs]]
name = "label"
type = "string_list"

[[cli.affected.steps]]
type = "AffectedPackages"

Release and npm publish workflows

monochange includes a release workflow modeled around long-running release PR refresh plus post-merge tagging:

  • .github/workflows/release.yml refreshes the dedicated release PR branch on normal main pushes
  • the same workflow detects when HEAD is already a merged monochange release commit, runs monochange step tag-release --from HEAD, runs monochange step publish-readiness --from HEAD --output <path>, and then runs monochange step publish-packages
  • tag-triggered or downstream workflows can then build archives, create hosted releases, publish additional assets from the pushed tags, or run a separate monochange step publish-release job when you still want manifest-driven hosted-release publication

That split keeps tag creation on the default branch side of the merge and lets downstream automation consume the exact durable release metadata that monochange stored in git history.

For release asset workflows, prefer tag or manual dispatch triggers over draft release.created triggers. Draft releases do not reliably emit release.created, and immutable releases need every archive to be uploaded and attested before the release is finalized. A hardened GitHub release asset job should request contents: write, id-token: write, and attestations: write, upload the .tar.gz and .zip archives, then attest the archive files directly instead of treating checksum files as a substitute.

After a release finishes, verify an archive with GitHub’s attestation CLI:

gh attestation verify monochange-x86_64-unknown-linux-gnu-v1.2.3.tar.gz \
  --repo monochange/monochange

For release repair, GitHub is also the first provider with hosted-release retarget sync support. monochange uses the durable release record plus tag names from that record to keep the hosted release view aligned with moved tags.

GitHub Actions policy workflow

name: changeset-policy

on:
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
      - labeled
      - unlabeled

concurrency:
  group: changeset-policy-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  check:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
    steps:
      - name: checkout repository
        uses: actions/checkout@v6

      - name: setup
        uses: ./.github/actions/devenv
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

      - name: collect changed files
        id: changed
        uses: tj-actions/changed-files@v46

      - name: run changeset policy
        env:
          PR_LABELS_JSON: ${{ toJson(github.event.pull_request.labels.*.name) }}
          CHANGED_FILES: ${{ steps.changed.outputs.all_changed_files }}
        shell: bash
        run: |
          set -euo pipefail

          mapfile -t labels < <(jq -r '.[]' <<<"$PR_LABELS_JSON")
          args=(step affected-packages --format json --verify)

          for path in $CHANGED_FILES; do
            args+=(--changed-paths "$path")
          done

          for label in "${labels[@]}"; do
            args+=(--label "$label")
          done

          devenv shell -- monochange "${args[@]}" | tee policy.raw
          awk 'BEGIN { capture = 0 } /^\{/ { capture = 1 } capture { print }' policy.raw > policy.json
          jq -e '.status != "failed"' policy.json >/dev/null

Dogfooding on the monochange repository

The monochange repository itself can dogfood this model by:

  • declaring [source], [source.releases], and [source.pull_requests] in monochange.toml
  • running a real changeset-policy GitHub Actions workflow that shells into monochange step affected-packages
  • publishing the CLI npm packages from .github/workflows/publish.yml with the protected publisher environment and id-token: write, without NODE_AUTH_TOKEN or NPM_TOKEN

For monochange’s own npm packages, register every package under the GitHub trusted-publishing context monochange/monochange, workflow file publish.yml, and environment publisher before the first tokenless publish:

  • @monochange/cli
  • @monochange/cli-darwin-arm64
  • @monochange/cli-darwin-x64
  • @monochange/cli-linux-arm64-gnu
  • @monochange/cli-linux-arm64-musl
  • @monochange/cli-linux-x64-gnu
  • @monochange/cli-linux-x64-musl
  • @monochange/cli-win32-arm64-msvc
  • @monochange/cli-win32-x64-msvc

After publishing, verify npm provenance from the package page or with npm’s provenance metadata for the released version. The expected publisher identity is the publish.yml workflow in monochange/monochange; a run that lacks npm trusted-publishing setup should fail instead of falling back to a long-lived registry token.

Supported providers

The --provider flag supports three source providers:

Provider--provider valueWorkflow generationRelease automationPull/merge requests
GitHubgithubYes: GitHub ActionsYesYes
GitLabgitlabNo: use .gitlab-ci.ymlYesYes
GiteagiteaNo: use Gitea ActionsYesYes

All providers configure the [source] section in monochange.toml with appropriate settings for releases and pull/merge requests. GitLab and Gitea require manual CI configuration since they don’t support GitHub Actions workflow files.

If you are comparing provider-specific CI layouts or designing a long-running release PR branch, continue with Advanced: CI, package publishing, and release PR flows.