GitHub Actions for monochange release automation.
This repository exists so monochange can own the critical parts of its workflow instead of depending directly on third-party actions, while still preserving the behavior that matters for release pull requests.
The first action in this repository is merge.
It is intentionally modeled after sequoia-pgp/fast-forward: by default it performs a fast-forward-only update of the base branch so the release commit lands unchanged. For a divergent PR, merge-method: cherry-pick replays its commits with git cherry-pick -x onto the base, publishes the result to the PR branch, and stops for fresh checks. A later run fast-forwards the checked head SHA to the base.
That means it does not create a merge commit.
Currently implemented:
merge- merge a monochange release pull request onto its base branch with fast-forward or cherry-pickfail-when- intentionally fail a workflow step with a configurable reasonsetup-monochange- resolve the monochange CLIchange-classification- propose changeset bumps from default-branch and latest-release evidencechangeset-policy- validate changeset policy for affected packagescheck- runmonochange checkrelease-preview- previewmonochange step prepare-release --dry-runrelease-record- inspect the release record at a refopen-release-request/release-pr- open a release pull requesthosted-release- create a verified release commit and pull request through monochange.dev and the monochange GitHub Apptag-release- create or report release tags from a release recordpublish-readiness- generate publish readiness data from a release recordpublish-packages- publish packages from readiness datapublish-plan- compatibility wrapper for publish rate-limit planningpost-merge-release- tag and publish after a release PR merges
Public entrypoints:
monochange/actions@v0.9withname: <variant>monochange/actions/change-classification@v0monochange/actions/merge@v0.4.0monochange/actions/check@v0.4.0monochange/actions/release-preview@v0.4.0monochange/actions/release-record@v0.4.0monochange/actions/open-release-request@v0.4.0monochange/actions/hosted-release@v0monochange/actions/tag-release@v0.4.0monochange/actions/publish-readiness@v0.4.0monochange/actions/publish-packages@v0.4.0monochange/actions/fail-when@v0.4.0
Both entrypoints run the same implementation.
monochange release pull requests are not ordinary feature PRs.
A release PR usually contains a deliberately prepared release commit. That commit should land on the target branch unchanged so the resulting history still reflects the exact release artifact that monochange prepared.
Using GitHub's normal merge UI is risky here because it can:
- create a merge commit
- encourage the wrong merge strategy
- bypass monochange-specific policy checks
- make the final history less predictable
The merge action exists to:
- preserve the release commit SHA
- keep branch history compatible with monochange release expectations
- allow a deliberate "manual merge blocker" check pattern
- support slash-command style approval flows such as
/merge - keep the merge policy inside your own GitHub Actions repository
At a high level, the action:
- resolves the target pull request
- validates that it is a release PR you actually intend to merge
- collects check runs and statuses for the PR head SHA
- verifies the expected check policy
- fetches the base and head refs with git
- verifies the base branch is an ancestor of the PR head
- optionally checks that the triggering actor has push permission
- pushes the PR head SHA directly onto the base branch
In practical terms, the final branch update is equivalent to:
git push origin <pr-head-sha>:refs/heads/<base-branch>-but only after the action has verified that this is a legal fast-forward update.
If the release branch has diverged from the base branch, the action fails.
It will not create a merge commit and it will not force-push around divergence.
That is intentional.
Use this when you want a single repository-level action entrypoint.
uses: monochange/actions@v0.9
with:
name: mergeUse this when you want a dedicated merge action entrypoint.
uses: monochange/actions/merge@v0.4.0For most consumers, the path-based form is the clearest choice.
- name: fast-forward release PR
uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}- name: fast-forward release PR
uses: monochange/actions@v0.9
with:
name: merge
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}This repository includes a ready-to-copy example workflow at:
It supports both:
workflow_dispatch/mergePR comments viaissue_comment
- lets a maintainer manually dispatch a release merge
- optionally accepts a PR number
- also listens for
/mergecomments on pull requests - uses a dedicated token such as
RELEASE_PR_MERGE_TOKEN - uses
minimum-reviewer-permission: 'admin'for safety
name: release PR merge
on:
workflow_dispatch:
inputs:
pull_request:
description: Optional release PR number. Leave empty to auto-detect the single open release PR.
required: false
type: string
comment:
description: Whether the action should post a pull request comment.
required: false
default: on-error
type: choice
options:
- always
- on-error
- never
issue_comment:
types:
- created
concurrency:
group: release-pr-merge-${{ github.event.issue.number || inputs.pull_request || github.ref_name }}
cancel-in-progress: false
jobs:
merge-release-pr:
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/merge')
)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
steps:
- name: fast-forward release PR from workflow dispatch
if: github.event_name == 'workflow_dispatch'
uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
pull-request: ${{ inputs.pull_request }}
base-branch: main
head-branch-prefix: monochange/release/
required-failing-check: release-pr-manual-merge-blocker
minimum-reviewer-permission: 'admin'
comment: ${{ inputs.comment }}
- name: fast-forward release PR from /merge comment
if: github.event_name == 'issue_comment'
uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
base-branch: main
head-branch-prefix: monochange/release/
required-failing-check: release-pr-manual-merge-blocker
minimum-reviewer-permission: 'admin'
comment: alwaysIf you want behavior close to the original fast-forward workflow pattern, use an issue_comment trigger and ask maintainers to comment:
/merge
on the release pull request.
The recommended pattern is:
- listen to
issue_comment - only run when the comment body contains
/merge - only run when the issue is actually a pull request
- keep
minimum-reviewer-permission: 'admin' - use
comment: alwaysso the PR gets visible feedback
issue_comment workflows run in the base repository context, so they can see repository secrets.
Because of that, you should not rely on the slash command match alone.
Keep this input enabled:
with:
minimum-reviewer-permission: 'admin'That makes the action verify that the user who triggered the workflow has push access to the target repository before it performs the fast-forward.
name: release PR merge
on:
issue_comment:
types:
- created
jobs:
merge-release-pr:
if: >-
github.event.issue.pull_request &&
contains(github.event.comment.body, '/merge')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
steps:
- name: fast-forward release PR
uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
base-branch: main
head-branch-prefix: monochange/release/
required-failing-check: release-pr-manual-merge-blocker
minimum-reviewer-permission: 'admin'
comment: alwaysThe action resolves the pull request in this order:
pull-requestinput, if provided- the current GitHub event payload, if the workflow runs in a PR or PR-comment context
- auto-detection of a single open PR targeting
base-branchwhose head branch starts withhead-branch-prefix
This makes the action work well for:
workflow_dispatchpull_requestissue_comment- automation that expects exactly one open release PR
If auto-detection finds zero or multiple matching PRs, the action fails rather than guessing.
By default, the action enforces a monochange-friendly release PR policy.
It requires:
- no pending checks
- no cancelled checks
- no unexpected failing checks
- exactly one intentionally failing check named
release-pr-manual-merge-blocker
This supports the common pattern where a blocker check exists only to stop humans from using GitHub's merge button directly.
with:
require-green-checks: 'false'with:
required-failing-check: ''Minimum recommended workflow permissions:
permissions:
contents: write
pull-requests: write
checks: read
statuses: readThe action may post a pull request comment depending on the comment mode.
If you always use comment: never, you may not need that permission, but keeping it is usually simplest.
In most real repositories, the default github.token is not enough for protected branch fast-forwarding.
Recommended setup:
- create a dedicated token or GitHub App installation token
- store it as
RELEASE_PR_MERGE_TOKEN - allow it to update the protected base branch
- use that secret for this action
Example:
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}| Input | Required | Default | Description |
|---|---|---|---|
name |
yes | none | Action variant to run. Currently supported: merge. |
| Input | Required | Default | Description |
|---|---|---|---|
github-token |
no | ${{ github.token }} |
Token used for GitHub API calls and the final git push. |
repository |
no | ${{ github.repository }} |
Target repository in owner/repo format. |
pull-request |
no | empty | Explicit PR number. Must be a positive integer when provided. |
base-branch |
no | main |
Expected base branch for the release PR. |
head-branch-prefix |
no | monochange/release/ |
Required prefix for the release PR head branch. |
merge-method |
no | fast-forward |
Use fast-forward to preserve the checked PR head. For divergent branches, cherry-pick replays commits with -x onto the base and updates the PR branch for fresh checks. |
required-failing-check |
no | release-pr-manual-merge-blocker |
Name of the intentionally failing blocker check. Pass an empty string to disable this special rule. |
allow-cross-repository |
no | 'false' |
Whether pull requests from forks are allowed. |
require-green-checks |
no | 'true' |
Whether all checks must be complete and successful apart from the configured blocker check. |
minimum-reviewer-permission |
no | admin |
Minimum repository role required to trigger the merge. admin (default), maintain, or push (insecure). Strongly recommended for comment-triggered workflows. |
comment |
no | on-error |
Whether to post a pull request comment: always, never, or on-error. |
dry-run |
no | 'false' |
Validate without updating either branch or running post-update hooks. |
update-branch-on-failure |
no | 'false' |
When true, rebase a divergent PR head onto the base, push it with a lease, and stop for fresh checks before a later merge run. |
post-update-script |
no | empty | Optional command after a rebase and PR branch push. Runs in the temp workspace; the action then stops for fresh checks. |
post-update-workflow |
no | empty | Optional workflow file path to dispatch via workflow_dispatch after a successful rebase and force-push. Requires actions: write permission. |
trigger-command |
no | /merge |
Slash command that triggers this action when posted as a pull request comment. Only validated when the workflow runs from an issue_comment event. |
debug |
no | 'false' |
Emit extra debug logging. |
| Output | Description |
|---|---|
result |
Successful result: fast-forwarded, updated (PR branch prepared; fresh checks and another run needed), or dry-run. |
merged |
'true' when the base branch was updated, otherwise 'false'. |
rebased |
'true' when this run rebased and pushed the PR branch; the base remains unchanged. |
pull-request-number |
Resolved PR number. |
pull-request-url |
Resolved PR URL. |
base-sha |
Base branch SHA before validation/push. |
head-sha |
Checked head SHA, or the newly prepared PR head when result is updated. |
fast-forward-sha |
The SHA pushed to the base branch, or the candidate SHA reported during dry-run mode. Kept for compatibility. |
comment |
JSON string with a body field containing the summary comment text. |
Check policy and actor permission are enforced before creating the temporary git workspace. Dry-run reports the current head and whether it can fast-forward, including when the branches diverge, without rebasing, pushing, running a script, or dispatching a workflow.
The post-update script receives only PATH, LANG, LC_ALL, TMPDIR, TMP, TEMP, and RUNNER_TEMP when set. It does not inherit action inputs, GitHub tokens, OIDC credentials, or other job environment variables.
Rebase and cherry-pick preparation return result: updated and merged: false. Wait for the PR checks on the new head-sha, then run the action again. The later run fast-forwards that exact checked SHA, including when merge-method is cherry-pick; a head already based on the base is not replayed again. The token must be able to update the PR branch.
Use a token that triggers your PR workflows, such as a GitHub App token. Pushes made with GITHUB_TOKEN generally do not start a new workflow run. Post-update scripts and workflows dispatched on the base branch do not validate the new PR head or permit an immediate merge. With check enforcement enabled, missing, pending, cancelled, or failing checks block the later merge.
- name: fast-forward release PR
id: merge
uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
- name: print results
run: |
echo "result=${{ steps.merge.outputs.result }}"
echo "merged=${{ steps.merge.outputs.merged }}"
echo "pr=${{ steps.merge.outputs.pull-request-number }}"
echo "url=${{ steps.merge.outputs.pull-request-url }}"
echo "base_sha=${{ steps.merge.outputs.base-sha }}"
echo "head_sha=${{ steps.merge.outputs.head-sha }}"
echo "ff_sha=${{ steps.merge.outputs.fast-forward-sha }}"
echo '${{ steps.merge.outputs.comment }}'- uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}- uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
pull-request: '123'- uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
dry-run: 'true'- uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
comment: always- uses: monochange/actions/merge@v0.4.0
with:
github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
allow-cross-repository: 'true'Auto-detection found zero or multiple matching PRs.
Fixes:
- pass
pull-requestexplicitly - close extra release PRs
- tighten
base-branch - tighten
head-branch-prefix
Some checks have not completed yet.
Fixes:
- wait for CI to finish
- disable strict enforcement with
require-green-checks: 'false'if that matches your policy
The blocker-check expectation did not match the repository's real check set.
Fixes:
- keep the blocker check name aligned with your workflow
- set
required-failing-check: ''to disable the special-case rule
The base branch advanced or the release branch diverged.
Fixes:
- rebase or regenerate the release branch
- set
update-branch-on-failure: 'true'to let the action rebase and push automatically - re-run the workflow after the release PR head is updated
The user who triggered the workflow does not have write-level access.
Fixes:
- have a maintainer trigger the workflow
- keep this check enabled for slash-command workflows
- disable it only if you deliberately trust another gating mechanism
The final push was rejected.
Common reasons:
- the base branch advanced after validation
- the token cannot update the protected branch
- branch protection still blocks the push
Fixes:
- re-run the workflow
- verify branch protection bypass rules for the token
- use a dedicated
RELEASE_PR_MERGE_TOKEN
A typical monochange setup looks like this:
- CI runs on
mainand keeps the release PR refreshed. - The release PR targets
mainand uses a branch likemonochange/release/.... - A dedicated blocker check fails intentionally to stop UI merges.
- A maintainer runs the merge workflow or comments
/merge. - This action validates the PR and fast-forwards
mainto the release commit. - The release commit lands unchanged.
action.yml # root action entrypoint
merge/action.yml # path-based merge entrypoint
.github/workflows/release-pr-merge.yml # ready-to-copy workflow example
src/main.ts # variant dispatcher
src/actions/merge/ # merge implementation
src/shared/ # shared helpers
dist/index.mjs # published bundle
- package manager:
pnpm - runtime baseline: Node 24
- implementation language: TypeScript
- build/test toolchain: Vite+
If this repository lives inside a larger pnpm workspace, this is often useful:
pnpm install --ignore-workspacepnpm check
pnpm test
pnpm build| Command | Purpose |
|---|---|
pnpm fmt |
format files |
pnpm check |
format/lint/typecheck |
pnpm test |
run tests |
pnpm build |
build dist/index.mjs |
pnpm all |
run check, test, and build |
Like most JavaScript GitHub Actions repositories, this repository is meant to publish the compiled dist/ bundle alongside the source.
When releasing:
- run
pnpm build - commit the updated
dist/ - tag a release such as
v0.4.0 - reference the action by tag or pinned SHA downstream
Examples:
uses: monochange/actions@v0.9uses: monochange/actions/merge@v0.4.0Intentionally fail a workflow step with a configurable reason. Useful for manual merge blockers, policy gates, and branch-protection checks that need a clear human-readable failure.
fail-when writes a job summary every time it intentionally fails. If fail-comment is provided, it also resolves the target PR and posts a formatted markdown comment. If should-fail is false, it skips without requiring a reason, token, repository, or pull request context.
- uses: monochange/actions/fail-when@v0.4.0
with:
should-fail: ${{ startsWith(github.head_ref, 'monochange/release/') }}
reason: Release PRs must be merged with the monochange /merge workflow.
fail-comment: |
This check fails intentionally so the normal GitHub merge button cannot be used.
After checks are green, comment `/merge` to fast-forward the release PR.See the full fail-when documentation for inputs, outputs, permissions, PR comment behavior, examples, and troubleshooting.
Use an existing monochange on PATH, or download the exact CLI release tracked by this action. Automatic downloads use the npm registry directly and verify the archive against the SHA-512 checksum embedded from the action's lockfile before extraction. Consumer .npmrc files cannot redirect setup. A download or integrity failure stops the action.
Set setup-monochange: false to require a preinstalled CLI, or provide a custom command. Automatic setup returns a quoted absolute command pointing to the verified binary in a private temporary directory. It requires tar (available on GitHub-hosted runners) and supports macOS, Windows, and Linux (glibc or musl) on x64 and ARM64.
- uses: monochange/actions/setup-monochange@v0.4.0
with:
setup-monochange: trueValidate that all affected packages have appropriate changesets.
- id: changed
uses: tj-actions/changed-files@v46
with:
separator: ','
- uses: monochange/actions/changeset-policy@v0
with:
changed-paths: ${{ steps.changed.outputs.all_changed_files }}
comment-on-failure: trueFailure comments use the comment field from monochange step affected-packages. When the failure message changes, the previous failure moves into a collapsed section, and once the PR passes or is skipped the comment is rewritten with a ✅ checkmark while the history is preserved.
Set from to a base git ref (for example origin/main, with a fetch-depth: 0 checkout) to compare against git history instead of an explicit path list and to enable the API classification bump alignment gate: changesets that understate the classified change type fail the policy, while higher bumps only warn. See the full changeset-policy documentation.
Propose the changeset bump for each package by comparing the pull request with both the default branch and its latest release. The action publishes versioned JSON, writes a per-package severity summary with collapsible evidence to the job summary, and creates or updates one pull request comment.
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- id: classify
uses: monochange/actions/change-classification@v0
with:
dependency-propagation: publicUse the recommendation output for the overall major, minor, patch, or none proposal. Inspect review-required, package completeness, confidence, and finding comparisons before writing a changeset. See the full change-classification documentation.
Open a release pull request with the current changeset state.
- uses: monochange/actions/release-pr@v0.4.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}Prepare the release in the workflow, then let monochange.dev create the release commit and release pull request through the monochange GitHub App. The commit is verified by GitHub, the pull request triggers the repository's normal workflows, and the job needs no personal access token.
Requires the monochange GitHub App on the repository, permissions: id-token: write, and monochange 0.17.0 or newer (0.20.0 or newer with release-command). release-command runs your own monochange.toml release command with every release step on the hosted backend.
permissions:
contents: write
pull-requests: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- id: release
uses: monochange/actions/hosted-release@v0
with:
release-command: releaseOutputs include commit-sha, verified, head-branch, base-branch, pull-request-number, and pull-request-url. See the full hosted-release documentation for inputs, error messages, and troubleshooting.
Generate a publish plan for the current release.
- uses: monochange/actions/publish-plan@v0.4.0
with:
mode: fullTag and publish after a release PR merges.
- uses: monochange/actions/post-merge-release@v0.4.0
with:
from-ref: HEAD
branch: releaseThis repository is intentionally structured for future action variants.
To add one later:
- add
src/actions/<variant>/ - dispatch from
src/main.ts - document it here
- optionally expose
<variant>/action.yml - add tests
- rebuild
dist/
That preserves both consumption styles:
monochange/actions@v0.9withname: <variant>monochange/actions/<variant>@v0.4.0