Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,29 @@ jobs:
node-version: "22"
- run: node --test scripts/*.test.mts

# The composite action itself, from this checkout, through the published merge-when: valid input reaches merge.sh, which finds no open pull request at this run's own commit (a merge ref or a commit on main) and stops.
action:
name: Action
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
checks: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./
with:
# Never used: no open pull request has this run's own commit at its head.
merge-token: unused
head-sha: ${{ github.sha }}
required-check: Required Checks

# A single stable branch-protection target aggregating every job that runs on every pull request, so the required status check doesn't need updating in GitHub's branch protection settings whenever a job is added, renamed, or split -- only this job's own needs: list changes, in the same commit as whatever job it now covers. if: always() so this job still runs (and can fail) even when one of its dependencies failed outright, rather than being skipped itself; a dependency that never ran for this event (commitlint on a plain push to main) reports "skipped", not "failure" or "cancelled", so it doesn't block release below.
required-checks:
name: Required Checks
if: always()
needs: [commitlint, actionlint, typecheck, lint, format, test]
needs: [commitlint, actionlint, typecheck, lint, format, test, action]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/merge-when-green.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Merge when green

# Runs merge-when-green beside this repository's own ready-when-green.yml, as its README suggests. A pull request labelled `automerge` is rebase-merged with the organisation's GitHub App once CI's "Required Checks" job has passed; a draft is left until ready-when-green.yml marks it ready, and the ready_for_review event that causes starts this workflow again.

on:
workflow_run:
workflows: [CI]
types: [completed]
pull_request_target:
types: [labeled, ready_for_review]

concurrency:
group: merge-when-green-${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha }}
cancel-in-progress: false

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

jobs:
merge:
if: >-
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
|| (github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'automerge'))
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: ExaDev/merge-when-green@v1
with:
app-id: ${{ secrets.GH_APP_ID }}
app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
required-check: Required Checks
30 changes: 30 additions & 0 deletions .github/workflows/ready-when-green.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Ready when green

# This repository uses its own action, from the default branch's copy (workflow_run runs the default branch's workflows, and the checkout below is of that same branch, never of pull request code). A draft labelled `automerge` is marked ready for review with the organisation's GitHub App once CI's "Required Checks" job has passed; the ready_for_review event that starts then runs merge-when-green.yml, which merges it.

on:
workflow_run:
workflows: [CI]
types: [completed]

concurrency:
group: ready-when-green-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

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

jobs:
ready:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./
with:
app-id: ${{ secrets.GH_APP_ID }}
app-private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
required-check: Required Checks
76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,77 @@
# ready-when-green

Marks labelled draft pull requests ready for review once a required check has passed on their head commit.
Marks a labelled draft pull request ready for review once a named check has passed on its head commit. It is [`merge-when`](https://github.com/ExaDev/merge-when) with `then: ready` and the conditions fixed to "labelled and the named check green"; use `merge-when` directly to choose other conditions.

## When to use this

Open pull requests as drafts while CI is still deciding, and let them become reviewable, and mergeable, only once it is green. That suits agents and bots that open a pull request as soon as a branch exists, and anyone who would rather reviewers and merge automation never see a red pull request. Run it beside [`merge-when-green`](https://github.com/ExaDev/merge-when-green), which leaves drafts alone: this action marks the draft ready, the `ready_for_review` event that causes starts `merge-when-green` again, and it merges.

A pull request is marked ready when all of these hold: it carries the opt-in label, it is a draft, it still has the commit being checked at its head, and the named check run succeeded on that commit. One that is already ready is left alone.

## Use

The action has no triggers of its own, so a workflow supplies them. Run it when CI finishes, in a workflow of its own beside the one that runs `merge-when-green`:

```yaml
name: Ready when green

on:
workflow_run:
workflows: [CI]
types: [completed]

concurrency:
group: ready-when-green-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

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

jobs:
ready:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: ExaDev/ready-when-green@v1
with:
merge-token: ${{ secrets.MERGE_TOKEN }}
required-check: Required checks
```

The `merge-when-green` workflow needs `ready_for_review` among its `pull_request_target` types, as its own example already has, so that a pull request this action marks ready is merged at once:

```yaml
on:
workflow_run:
workflows: [CI]
types: [completed]
pull_request_target:
types: [labeled, ready_for_review]
```

Three things make the pair work:

- **CI has to run on drafts.** The required check must report on a draft's head commit, so don't skip CI jobs with `if: github.event.pull_request.draft == false` or start CI only on `ready_for_review`; GitHub's default `pull_request` types already include drafts.
- **The token must not be `GITHUB_TOKEN`.** GitHub starts no workflow from an event that token caused, so a pull request it marked ready would never start the merge. Give `merge-token`, or `app-id` and `app-private-key`; the action refuses to run without one.
- **The same label opts in to both.** With the default `automerge`, a draft labelled once is readied and then merged. Give a different `label` to have pull requests readied without being merged.

Marking a pull request ready is not pinned to a commit the way the merge is, so a push landing between the check and the call is readied with it. A pull request readied wrongly goes back to draft with `gh pr ready <number> --undo`. Do not add a checkout step to a workflow that runs on `pull_request_target`.

## Inputs

| Input | Default | Meaning |
| --------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `required-check` | required | Name of the check run that must have succeeded on the head commit. Point it at one aggregate job that `needs` every other job and runs with `if: always()`, so it passes only when they all did. |
| `label` | `automerge` | Label that opts a pull request in. |
| `merge-token` | | An API token (a personal access token or an app token) that marks the pull request ready. It needs write on pull requests. |
| `app-id`, `app-private-key` | | A GitHub App to mint an installation token from, instead of `merge-token`. The app needs write on pull requests. |
| `read-token` | `github.token` | Token used to read pull requests and checks. |
| `head-sha` | event's head commit | Commit to act on. |

## Development

This repository readies its own labelled drafts with the action (`.github/workflows/ready-when-green.yml`) and merges them with `merge-when-green` beside it (`.github/workflows/merge-when-green.yml`), both with the organisation's GitHub App. The work itself is done by `merge-when`, which holds the script and its tests; `npm test` here runs the unit tests for the release plugins. CI also runs the composite action from the checkout, and commitlint, actionlint, typecheck, lint and format checks, aggregated into one `Required Checks` job.

Releases are made by semantic-release from conventional commits on `main`: it tags the version, publishes the GitHub Release, updates `CHANGELOG.md` and moves the major tag (`v1`) that consumers pin to.
41 changes: 41 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Ready when green
description: Mark labelled draft pull requests ready for review once a required check passes, so a merge workflow can take them on.
author: ExaDev
branding:
icon: eye
color: green

inputs:
required-check:
description: Name of the check run that must have succeeded on the pull request's head commit. Point it at one aggregate job that needs every other job.
required: true
label:
description: Label that opts a pull request in.
default: automerge
merge-token:
description: An API token that marks the pull request ready, a personal access token or a GitHub App installation token. Not GITHUB_TOKEN, which starts no ready_for_review workflows.
app-id:
description: A GitHub App's id, as an alternative to merge-token; an installation token is minted for this repository.
app-private-key:
description: The private key of the GitHub App named by app-id.
read-token:
description: Token used to read pull requests and checks.
default: ${{ github.token }}
head-sha:
description: Commit to act on. Defaults to the head of the workflow_run or pull_request_target event.
default: ${{ github.event.workflow_run.head_sha || github.event.pull_request.head.sha }}

runs:
using: composite
steps:
- uses: ExaDev/merge-when@v1
with:
then: ready
when: |
label: ${{ inputs.label }}
check: ${{ inputs.required-check }}
merge-token: ${{ inputs.merge-token }}
app-id: ${{ inputs.app-id }}
app-private-key: ${{ inputs.app-private-key }}
read-token: ${{ inputs.read-token }}
head-sha: ${{ inputs.head-sha }}
Loading