Skip to content
monochangePublic

About

GitHub Actions for monochange release automation

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

114 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

monochange actions

codecov

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.

What is in this repository?

Currently implemented:

  • merge - merge a monochange release pull request onto its base branch with fast-forward or cherry-pick
  • fail-when - intentionally fail a workflow step with a configurable reason
  • setup-monochange - resolve the monochange CLI
  • change-classification - propose changeset bumps from default-branch and latest-release evidence
  • changeset-policy - validate changeset policy for affected packages
  • check - run monochange check
  • release-preview - preview monochange step prepare-release --dry-run
  • release-record - inspect the release record at a ref
  • open-release-request / release-pr - open a release pull request
  • hosted-release - create a verified release commit and pull request through monochange.dev and the monochange GitHub App
  • tag-release - create or report release tags from a release record
  • publish-readiness - generate publish readiness data from a release record
  • publish-packages - publish packages from readiness data
  • publish-plan - compatibility wrapper for publish rate-limit planning
  • post-merge-release - tag and publish after a release PR merges

Public entrypoints:

  • monochange/actions@v0.9 with name: <variant>
  • monochange/actions/change-classification@v0
  • monochange/actions/merge@v0.4.0
  • monochange/actions/check@v0.4.0
  • monochange/actions/release-preview@v0.4.0
  • monochange/actions/release-record@v0.4.0
  • monochange/actions/open-release-request@v0.4.0
  • monochange/actions/hosted-release@v0
  • monochange/actions/tag-release@v0.4.0
  • monochange/actions/publish-readiness@v0.4.0
  • monochange/actions/publish-packages@v0.4.0
  • monochange/actions/fail-when@v0.4.0

Both entrypoints run the same implementation.


Why monochange needs this

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

How the merge action works

At a high level, the action:

  1. resolves the target pull request
  2. validates that it is a release PR you actually intend to merge
  3. collects check runs and statuses for the PR head SHA
  4. verifies the expected check policy
  5. fetches the base and head refs with git
  6. verifies the base branch is an ancestor of the PR head
  7. optionally checks that the triggering actor has push permission
  8. 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.

Important consequence

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.


Available entrypoints

Root action with variant dispatch

Use this when you want a single repository-level action entrypoint.

uses: monochange/actions@v0.9
with:
  name: merge

Path-based action

Use this when you want a dedicated merge action entrypoint.

uses: monochange/actions/merge@v0.4.0

For most consumers, the path-based form is the clearest choice.


Quick start

Simplest usage

- name: fast-forward release PR
  uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}

Root-entrypoint equivalent

- name: fast-forward release PR
  uses: monochange/actions@v0.9
  with:
    name: merge
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}

Ready-to-copy workflow

This repository includes a ready-to-copy example workflow at:

It supports both:

  • workflow_dispatch
  • /merge PR comments via issue_comment

What the example workflow does

  • lets a maintainer manually dispatch a release merge
  • optionally accepts a PR number
  • also listens for /merge comments on pull requests
  • uses a dedicated token such as RELEASE_PR_MERGE_TOKEN
  • uses minimum-reviewer-permission: 'admin' for safety

Example file contents

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

/merge comment trigger

If 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.

Recommended pattern

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: always so the PR gets visible feedback

Security note

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.

Minimal slash-command example

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

Pull request resolution

The action resolves the pull request in this order:

  1. pull-request input, if provided
  2. the current GitHub event payload, if the workflow runs in a PR or PR-comment context
  3. auto-detection of a single open PR targeting base-branch whose head branch starts with head-branch-prefix

This makes the action work well for:

  • workflow_dispatch
  • pull_request
  • issue_comment
  • automation that expects exactly one open release PR

If auto-detection finds zero or multiple matching PRs, the action fails rather than guessing.


Check policy

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.

Disable strict green-check enforcement

with:
  require-green-checks: 'false'

Disable the special failing-check expectation

with:
  required-failing-check: ''

Permissions and token requirements

Minimum recommended workflow permissions:

permissions:
  contents: write
  pull-requests: write
  checks: read
  statuses: read

Why pull-requests: write?

The 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.

Recommended token setup

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 }}

Inputs

Root action only

Input Required Default Description
name yes none Action variant to run. Currently supported: merge.

Shared merge inputs

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.

Outputs

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.

Branch updates and fresh checks

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.

Example: consume outputs

- 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 }}'

Common usage patterns

Auto-detect the only open release PR

- uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}

Fast-forward a specific PR number

- uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
    pull-request: '123'

Dry-run validation only

- uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
    dry-run: 'true'

Always post a PR comment

- uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
    comment: always

Allow cross-repository PRs

- uses: monochange/actions/merge@v0.4.0
  with:
    github-token: ${{ secrets.RELEASE_PR_MERGE_TOKEN }}
    allow-cross-repository: 'true'

Failure modes and troubleshooting

"Expected exactly one open release pull request..."

Auto-detection found zero or multiple matching PRs.

Fixes:

  • pass pull-request explicitly
  • close extra release PRs
  • tighten base-branch
  • tighten head-branch-prefix

"Pull request still has pending checks..."

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

"Expected exactly one failing check named ..."

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

"Cannot fast-forward ... is not a direct ancestor ..."

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

"Actor @... does not have push permission ..."

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

"Fast-forward push failed ..."

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

Suggested monochange workflow shape

A typical monochange setup looks like this:

  1. CI runs on main and keeps the release PR refreshed.
  2. The release PR targets main and uses a branch like monochange/release/....
  3. A dedicated blocker check fails intentionally to stop UI merges.
  4. A maintainer runs the merge workflow or comments /merge.
  5. This action validates the PR and fast-forwards main to the release commit.
  6. The release commit lands unchanged.

Repository layout

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

Development

Tooling

  • package manager: pnpm
  • runtime baseline: Node 24
  • implementation language: TypeScript
  • build/test toolchain: Vite+

Install dependencies

If this repository lives inside a larger pnpm workspace, this is often useful:

pnpm install --ignore-workspace

Validate locally

pnpm check
pnpm test
pnpm build

Commands

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

Publishing notes

Like most JavaScript GitHub Actions repositories, this repository is meant to publish the compiled dist/ bundle alongside the source.

When releasing:

  1. run pnpm build
  2. commit the updated dist/
  3. tag a release such as v0.4.0
  4. reference the action by tag or pinned SHA downstream

Examples:

uses: monochange/actions@v0.9
uses: monochange/actions/merge@v0.4.0

fail-when

Intentionally 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.


setup-monochange

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

changeset-policy

Validate 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: true

Failure 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.


change-classification

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

Use 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.


release-pr

Open a release pull request with the current changeset state.

- uses: monochange/actions/release-pr@v0.4.0
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}

hosted-release

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

Outputs 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.


publish-plan

Generate a publish plan for the current release.

- uses: monochange/actions/publish-plan@v0.4.0
  with:
    mode: full

post-merge-release

Tag and publish after a release PR merges.

- uses: monochange/actions/post-merge-release@v0.4.0
  with:
    from-ref: HEAD
    branch: release

Adding more actions later

This repository is intentionally structured for future action variants.

To add one later:

  1. add src/actions/<variant>/
  2. dispatch from src/main.ts
  3. document it here
  4. optionally expose <variant>/action.yml
  5. add tests
  6. rebuild dist/

That preserves both consumption styles:

  • monochange/actions@v0.9 with name: <variant>
  • monochange/actions/<variant>@v0.4.0

About

GitHub Actions for monochange release automation

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages