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:

  • mc release --dry-run --format json refreshes the cached manifest and shows the downstream automation payload
  • mc publish-release previews or publishes provider releases from the structured release notes
  • mc release-pr previews or opens an idempotent provider release request; when [source.pull_requests].verified_commits = true and the command 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
  • mc 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 mc init --provider

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

# Initialize with GitHub automation pre-configured
mc 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 commandscommit-release and release-pr commands ready to use
  3. GitHub Actions workflowsrelease.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

mc release --dry-run --format json
mc publish-release --dry-run --format json
mc release-pr --dry-run --format json
mc step:affected-packages --format json --verify --changed-paths crates/monochange/src/lib.rs

Inspecting and repairing a recent release

GitHub automation now 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:

mc step:release-record --from v1.2.3
mc step:tag-release --from HEAD --dry-run --format json
mc repair-release --from v1.2.3 --target HEAD --dry-run
mc repair-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
  • mc step:tag-release consumes that durable record after merge and creates the declared tag set on the default branch

Use --dry-run first for repair-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 mc 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:

mc 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:

  • mc step:publish-readiness --from HEAD --output <path> checks package registries before mutation
  • mc publish handles package registries such as crates.io, npm, jsr, and pub.dev
  • mc 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 are the only ecosystem with built-in bulk trust automation today:

  • monochange checks the existing trust configuration first
  • if trust is missing, it runs npm trust github ...
  • pnpm workspaces run the trust command through pnpm exec npm trust ...
  • monochange verifies the result after running the trust command instead of assuming success

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 — 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"

[release_notes]
change_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"]
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"]
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 now 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 mc step:tag-release --from HEAD, runs mc step:publish-readiness --from HEAD --output <path>, and then runs mc publish
  • tag-triggered or downstream workflows can then build archives, create hosted releases, publish additional assets from the pushed tags, or run a separate mc 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 -- mc "${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 mc 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.