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 jsonrefreshes the cached manifest and shows the downstream automation payloadmc publish-releasepreviews or publishes provider releases from the structured release notesmc release-prpreviews or opens an idempotent provider release request; when[source.pull_requests].verified_commits = trueand 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 verifiedmc step:affected-packagesevaluates 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:
- Complete source configuration —
[source],[source.releases], and[source.pull_requests]sections - Automation CLI commands —
commit-releaseandrelease-prcommands ready to use - GitHub Actions workflows —
release.ymlandchangeset-policy.ymlfor CI/CD - 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
ReleaseRecorddescribes the durable release declaration stored in the release commit bodymc step:tag-releaseconsumes 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 mutationmc publishhandles package registries such ascrates.io,npm,jsr, andpub.devmc publish-releasehandles 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.ymlrefreshes the dedicated release PR branch on normalmainpushes- the same workflow detects when
HEADis already a merged monochange release commit, runsmc step:tag-release --from HEAD, runsmc step:publish-readiness --from HEAD --output <path>, and then runsmc 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-releasejob 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]inmonochange.toml - running a real
changeset-policyGitHub Actions workflow that shells intomc step:affected-packages - publishing the CLI npm packages from
.github/workflows/publish.ymlwith the protectedpublisherenvironment andid-token: write, withoutNODE_AUTH_TOKENorNPM_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 value | Workflow generation | Release automation | Pull/merge requests |
|---|---|---|---|---|
| GitHub | github | Yes — GitHub Actions | Yes | Yes |
| GitLab | gitlab | No — use .gitlab-ci.yml | Yes | Yes |
| Gitea | gitea | No — use Gitea Actions | Yes | Yes |
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.