|
1 | 1 | # draft-unfinished-prs |
2 | | -A github action that automatically drafts PRs that are not finished |
| 2 | + |
| 3 | +A GitHub Action that automatically converts pull requests to **draft** when they are not ready to be merged, and posts a comment explaining why. |
| 4 | + |
| 5 | +## What counts as "unfinished"? |
| 6 | + |
| 7 | +Each criterion is independently configurable: |
| 8 | + |
| 9 | +| Criterion | Input | Default | |
| 10 | +|-----------------------------------------------------|------------------------------------|--------------| |
| 11 | +| PR has **merge conflicts** | `draft-on-merge-conflict` | `true` | |
| 12 | +| PR has any of the specified **labels** (e.g. `wip`) | `draft-on-any-labels` | *(disabled)* | |
| 13 | +| PR has an **unaddressed change-request** review | `draft-on-changes-requested` | `true` | |
| 14 | +| PR has **failing required CI checks** | `draft-on-failing-required-checks` | `true` | |
| 15 | + |
| 16 | +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). |
| 17 | + |
| 18 | +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`). |
| 19 | + |
| 20 | +## Usage |
| 21 | + |
| 22 | +Create a workflow file in your repository, e.g. `.github/workflows/draft-unfinished-prs.yml`: |
| 23 | + |
| 24 | +```yaml |
| 25 | +name: Draft unfinished PRs |
| 26 | + |
| 27 | +on: |
| 28 | + schedule: |
| 29 | + - cron: '0 5 * * *' # Every day at 05:00 UTC |
| 30 | + workflow_dispatch: # Allow manual runs from the Actions tab |
| 31 | + |
| 32 | +permissions: |
| 33 | + pull-requests: write # Convert to draft + post comments |
| 34 | + checks: read # Read check run results |
| 35 | + contents: read # Read branch protection rules |
| 36 | + |
| 37 | +jobs: |
| 38 | + draft-unfinished-prs: |
| 39 | + name: Draft unfinished PRs |
| 40 | + runs-on: ubuntu-latest |
| 41 | + steps: |
| 42 | + - uses: sequelize/draft-unfinished-prs@v1 |
| 43 | + with: |
| 44 | + # All inputs below are optional — these are the defaults: |
| 45 | + github-token: ${{ secrets.GITHUB_TOKEN }} |
| 46 | + draft-on-merge-conflict: true |
| 47 | + draft-on-any-labels: | # newline-separated list of labels: |
| 48 | + wip |
| 49 | + do not merge |
| 50 | + draft-on-changes-requested: true |
| 51 | + draft-on-failing-required-checks: true |
| 52 | + post-comment: true |
| 53 | + comment-template: '' # see "Custom comments" below |
| 54 | +``` |
| 55 | +
|
| 56 | +On 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. |
| 57 | +
|
| 58 | +## Inputs |
| 59 | +
|
| 60 | +| Input | Type | Default | Description | |
| 61 | +|------------------------------------|---------|-----------------------|------------------------------------------------------------------------------------------------------------------------------| |
| 62 | +| `github-token` | string | `${{ github.token }}` | GitHub token. Requires `pull-requests: write`, `checks: read`, `contents: read`. | |
| 63 | +| `draft-on-merge-conflict` | boolean | `true` | Convert to draft when the PR has merge conflicts. | |
| 64 | +| `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. | |
| 65 | +| `draft-on-changes-requested` | boolean | `true` | Convert to draft when a reviewer has unaddressed change requests. | |
| 66 | +| `draft-on-failing-required-checks` | boolean | `true` | Convert to draft when required CI checks are failing. | |
| 67 | +| `post-comment` | boolean | `true` | Post a comment explaining why the PR was converted to draft. | |
| 68 | +| `comment-template` | string | `''` | Custom comment body. Use `{reasons}` as a placeholder for the reason list. | |
| 69 | + |
| 70 | +## Outputs |
| 71 | + |
| 72 | +| Output | Type | Description | |
| 73 | +|-----------|-------------|-----------------------------------------------------------------------------------| |
| 74 | +| `drafted` | JSON string | A JSON array of PR numbers that were converted to draft in this run, e.g. `[42]`. | |
| 75 | + |
| 76 | +## Custom comments |
| 77 | + |
| 78 | +Set `comment-template` to override the default comment. Use `{reasons}` to embed the list of reasons: |
| 79 | + |
| 80 | +```yaml |
| 81 | +comment-template: | |
| 82 | + Hey! This PR has been put back into draft: |
| 83 | +
|
| 84 | + {reasons} |
| 85 | +
|
| 86 | + Please fix the above and re-request review when ready. 🙏 |
| 87 | +``` |
| 88 | + |
| 89 | +## Required permissions |
| 90 | + |
| 91 | +The action uses the `GITHUB_TOKEN` by default. Grant it: |
| 92 | + |
| 93 | +```yaml |
| 94 | +permissions: |
| 95 | + pull-requests: write # convert to draft + post comments |
| 96 | + checks: read # read check run results |
| 97 | + contents: read # read branch protection rules |
| 98 | +``` |
| 99 | + |
| 100 | +> [!NOTE] |
| 101 | +> Draft pull requests are available for **public repositories** on all plans and for **private repositories** on GitHub Pro, Team, and Enterprise Cloud. |
| 102 | + |
| 103 | +## How it works |
| 104 | + |
| 105 | +``` |
| 106 | +scheduled trigger (or workflow_dispatch) |
| 107 | + │ |
| 108 | + ▼ |
| 109 | +list all open, non-draft PRs in the repository |
| 110 | + │ |
| 111 | + ▼ |
| 112 | +for each PR: |
| 113 | + ├─ [merge conflict] → pr.mergeable === false (retries up to 3× while null) |
| 114 | + ├─ [label] → pr.labels intersects draft-on-any-labels list |
| 115 | + ├─ [changes requested] → latest review state per reviewer is CHANGES_REQUESTED |
| 116 | + │ AND reviewer is not in requested_reviewers |
| 117 | + └─ [failing CI] → branch protection → required check names |
| 118 | + checks API + statuses API: any required check failed |
| 119 | + │ |
| 120 | + ▼ |
| 121 | + if any reasons found: |
| 122 | + convert to draft + post comment explaining reasons |
| 123 | +``` |
| 124 | +
|
| 125 | +## Development |
| 126 | +
|
| 127 | +```bash |
| 128 | +# Install dependencies |
| 129 | +npm ci |
| 130 | +
|
| 131 | +# Type-check |
| 132 | +npm run lint |
| 133 | +
|
| 134 | +# Build dist/index.js |
| 135 | +npm run build |
| 136 | +``` |
| 137 | + |
| 138 | +The `dist/` folder must be committed to the repository. The `build.yml` workflow rebuilds it automatically on every push to `main` that touches source files. |
0 commit comments