A GitHub Action that automatically converts pull requests to draft when they are not ready to be merged, and posts a comment explaining why.
Each criterion is independently configurable:
| Criterion | Input | Default |
|---|---|---|
| PR has merge conflicts | draft-on-merge-conflict |
true |
PR has any of the specified labels (e.g. wip) |
draft-on-any-labels |
(disabled) |
| PR has an unaddressed change-request review | draft-on-changes-requested |
true |
| PR has failing required CI checks | draft-on-failing-required-checks |
true |
A change-request review is considered unaddressed when a reviewer's latest review state is CHANGES_REQUESTED and the author has not yet re-requested a review from that reviewer (re-requesting adds them back to the Requested reviewers list).
For CI checks the action first tries to read the required status checks from the branch protection rules. If it lacks permission to do so it falls back to flagging any completed check run that has a failing conclusion (failure, timed_out, cancelled, action_required).
Create a workflow file in your repository, e.g. .github/workflows/draft-unfinished-prs.yml:
name: Draft unfinished PRs
on:
schedule:
- cron: '0 5 * * *' # Every day at 05:00 UTC
workflow_dispatch: # Allow manual runs from the Actions tab
permissions:
pull-requests: write # Convert to draft + post comments
checks: read # Read check run results
contents: read # Read branch protection rules
jobs:
draft-unfinished-prs:
name: Draft unfinished PRs
runs-on: ubuntu-latest
steps:
- uses: sequelize/draft-unfinished-prs@v1
with:
# All inputs below are optional — these are the defaults:
github-token: ${{ secrets.GITHUB_TOKEN }}
draft-on-merge-conflict: true
draft-on-any-labels: | # newline-separated list of labels:
wip
do not merge
draft-on-changes-requested: true
draft-on-failing-required-checks: true
post-comment: true
comment-template: '' # see "Custom comments" belowOn each run the action scans all open, non-draft pull requests and converts any that meet one or more of the unfinished criteria to draft.
| Input | Type | Default | Description |
|---|---|---|---|
github-token |
string | ${{ github.token }} |
GitHub token. Requires pull-requests: write, checks: read, contents: read. |
draft-on-merge-conflict |
boolean | true |
Convert to draft when the PR has merge conflicts. |
draft-on-any-labels |
string | '' |
Convert to draft when the PR has any of these labels. Newline- or comma-separated list. Empty string disables the check. |
draft-on-changes-requested |
boolean | true |
Convert to draft when a reviewer has unaddressed change requests. |
draft-on-failing-required-checks |
boolean | true |
Convert to draft when required CI checks are failing. |
dry-run |
boolean | false |
Run all checks but skip converting to draft and posting comments. What would have happened is printed to the Actions log. |
post-comment |
boolean | true |
Post a comment explaining why the PR was converted to draft. |
comment-template |
string | '' |
Custom comment body. Use {reasons} as a placeholder for the reason list. |
| Output | Type | Description |
|---|---|---|
drafted |
JSON string | A JSON array of PR numbers that were converted to draft in this run, e.g. [42]. |
Set comment-template to override the default comment. Use {reasons} to embed the list of reasons:
comment-template: |
Hey! This PR has been put back into draft:
{reasons}
Please fix the above and re-request review when ready. 🙏The action uses the GITHUB_TOKEN by default. Grant it:
permissions:
pull-requests: write # convert to draft + post comments
checks: read # read check run results
contents: read # read branch protection rulesNote
Draft pull requests are available for public repositories on all plans and for private repositories on GitHub Pro, Team, and Enterprise Cloud.
scheduled trigger (or workflow_dispatch)
│
▼
list all open, non-draft PRs in the repository
│
▼
for each PR:
├─ [merge conflict] → pr.mergeable === false (retries up to 3× while null)
├─ [label] → pr.labels intersects draft-on-any-labels list
├─ [changes requested] → latest review state per reviewer is CHANGES_REQUESTED
│ AND reviewer is not in requested_reviewers
└─ [failing CI] → branch protection → required check names
checks API + statuses API: any required check failed
│
▼
if any reasons found:
convert to draft + post comment explaining reasons
# Install dependencies
npm ci
# Type-check
npm run lint
# Build dist/index.js
npm run buildThe dist/ folder must be committed to the repository. The build.yml workflow rebuilds it automatically on every push to main that touches source files.