A GitHub Action that automatically marks a draft pull request as ready for review once all required checks pass.
- You open a draft PR and add a trigger label (default:
Mark Ready When Ready) - The action validates that the token has all required permissions upfront — if
anything is missing, it fails immediately with a clear error showing the exact
permissions:block to add - It checks preconditions — if the PR isn't a draft or doesn't have the
label, it exits immediately with
result=skipped - If the repo has no required checks configured, the action skips gracefully
- Otherwise, it watches for all required checks to complete successfully
- It pauses briefly, then watches again to catch any late-arriving checks
- It verifies results via the GitHub GraphQL API (paginated, handles large check suites)
- It confirms the PR has no merge conflicts
- If everything looks good, it marks the PR as ready for review and removes the label
This is especially useful for repos with long CI suites — open your PR as a draft, slap on the label, and walk away. The PR will be marked ready for review only when CI is fully green.
name: Mark PR Ready When Ready
on:
pull_request:
types: [opened, edited, labeled, unlabeled, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
mark-ready:
runs-on: ubuntu-latest
permissions:
checks: read
contents: write
pull-requests: write
statuses: read
steps:
- uses: kenyonj/mark-ready-when-ready@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}That's it. The action validates permissions upfront, checks for the trigger
label and draft status internally — if anything isn't right, you'll get a
clear error message or the action exits with result=skipped.
Important: The workflow must include
contents: write— without it,GITHUB_TOKENcannot call themarkPullRequestReadyForReviewGraphQL mutation and will fail withResource not accessible by integration. Permissions can be set at either the workflow or job level. As of v1.1.3, the action validates permissions upfront and will tell you exactly what's missing.
The action checks for the trigger label and draft status internally, so a
job-level if: is not strictly required. However, without it GitHub still
allocates a runner, boots it, and downloads the action before the
precondition check can exit — that startup cost adds up on busy repos.
Adding an if: to the job lets GitHub evaluate the condition from the event
payload before allocating a runner, skipping the job entirely at zero
cost:
jobs:
mark-ready:
runs-on: ubuntu-latest
permissions:
checks: read
contents: write
pull-requests: write
statuses: read
if: |
contains(github.event.pull_request.labels.*.name, 'Mark Ready When Ready') &&
github.event.pull_request.draft == true
steps:
- uses: kenyonj/mark-ready-when-ready@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}This is recommended for repos with frequent PR activity. For smaller repos
the difference is negligible and you can omit the if: for simplicity.
For organizations or repos where GITHUB_TOKEN permissions are restricted by
policy, you can use a GitHub App token instead:
steps:
- name: Generate token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Mark ready when ready
uses: kenyonj/mark-ready-when-ready@v1
with:
github-token: ${{ steps.generate-token.outputs.token }}| Input | Required | Default | Description |
|---|---|---|---|
github-token |
Yes | — | Token with permission to read checks and write to pull requests |
label |
No | Mark Ready When Ready |
The label that triggers the action |
pause-seconds |
No | 20 |
Seconds to pause between verification rounds (catches late-arriving checks) |
polling-interval-seconds |
No | 10 |
Seconds between REST API polls when the fallback is active |
polling-timeout-seconds |
No | 1800 |
Maximum seconds to wait for checks during REST API fallback polling |
remove-label |
No | true |
Whether to remove the trigger label after marking the PR ready |
| Output | Description |
|---|---|
result |
ready, failing-checks, conflicting, or skipped |
failing-checks |
Names of required checks that failed (empty string if all passed) |
The result output is skipped when:
- The PR is not a draft
- The trigger label is not present
- The repo has no required checks configured
The workflow calling this action needs these permissions for GITHUB_TOKEN:
permissions:
checks: read
contents: write # required for markPullRequestReadyForReview
pull-requests: write
statuses: readNote:
contents: writeis required even though the action doesn't modify repository contents. Without it,GITHUB_TOKENcannot call themarkPullRequestReadyForReviewGraphQL mutation.As of v1.1.3, the action validates these permissions at the start of every run. If any are missing, it fails immediately with a clear error message showing the exact
permissions:block to add to your workflow. Permissions can be set at either the workflow level or the job level.
The action works on private repos across all GitHub plans. On GitHub Free,
the gh pr checks command fails because its internal GraphQL query accesses
a restricted field (checkSuite.workflowRun). When this happens, the action
automatically falls back to REST API polling — no configuration required.
The fallback polls the /commits/{sha}/check-runs and /commits/{sha}/status
REST endpoints until all checks complete, then hands off to the GraphQL
verification step (which uses a different query that works on all plans).
You can tune the fallback with polling-interval-seconds and
polling-timeout-seconds if your CI suite is particularly long or you want
tighter polling:
- uses: kenyonj/mark-ready-when-ready@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
polling-interval-seconds: "5"
polling-timeout-seconds: "3600" # wait up to 1 hourThe action uses a "trust but verify" approach:
- Permission validation — verifies the token has all required permissions
(
contents:write,pull-requests:write,checks:read,statuses:read) and fails fast with an actionable error message if any are missing - Precondition check — exits early if the PR isn't a draft or the trigger label isn't present
gh pr checks --watchwatches for required checks to complete (skips gracefully if no required checks are configured) — automatically falls back to REST API polling on private repos where GraphQL fields are restricted- A configurable pause catches checks that are re-triggered or start late
gh pr checks --watchruns again to confirm everything is still green- A GraphQL query independently verifies that no required check suites have
failing conclusions (
ACTION_REQUIRED,TIMED_OUT,CANCELLED,FAILURE,STARTUP_FAILURE) and no required commit statuses are in aFAILUREstate - The mergeable state is checked to ensure the PR doesn't have conflicts
This multi-layered approach prevents marking a PR as ready when there are transient or late-arriving failures.