Skip to content

Commit 99103bd

Browse files
committed
Initial release
1 parent 4f903cb commit 99103bd

20 files changed

Lines changed: 25786 additions & 1 deletion

.github/workflows/build.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Build dist
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'src/**'
9+
- 'package.json'
10+
- 'package-lock.json'
11+
- 'tsconfig.json'
12+
13+
permissions:
14+
contents: write
15+
16+
jobs:
17+
build:
18+
name: Build and commit dist
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
cache: 'npm'
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Build
32+
run: npm run build
33+
34+
- name: Commit dist/
35+
run: |
36+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
37+
git config --local user.name "github-actions[bot]"
38+
git add dist/
39+
git diff --staged --quiet || git commit -m "chore: rebuild dist [skip ci]"
40+
git push
41+

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules/
2+
lib/
3+
*.js.map
4+
*.d.ts
5+
*.d.ts.map
6+
licenses.txt
7+
8+
# Don't ignore dist — it must be committed for the action to work
9+
!dist/
10+

.idea/.gitignore

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/draft-unfinished-prs.iml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,138 @@
11
# 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.

action.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: 'Draft Unfinished PRs'
2+
description: 'Automatically converts pull requests to draft when they are not ready to be merged'
3+
author: 'draft-unfinished-prs'
4+
branding:
5+
icon: 'git-pull-request'
6+
color: 'yellow'
7+
inputs:
8+
github-token:
9+
description: >
10+
GitHub token used to interact with the GitHub API.
11+
Requires: pull-requests:write, checks:read, contents:read.
12+
required: false
13+
default: '${{ github.token }}'
14+
draft-on-merge-conflict:
15+
description: 'Convert PR to draft when it has merge conflicts.'
16+
required: false
17+
default: 'true'
18+
draft-on-any-labels:
19+
description: >
20+
Convert PR to draft when it has any of these labels.
21+
Accepts a newline-separated or comma-separated list of label names.
22+
Leave empty to disable this check.
23+
required: false
24+
default: ''
25+
draft-on-changes-requested:
26+
description: >
27+
Convert PR to draft when a reviewer has submitted a "Changes Requested" review
28+
and the PR author has not yet re-requested a review from that reviewer.
29+
required: false
30+
default: 'true'
31+
draft-on-failing-required-checks:
32+
description: >
33+
Convert PR to draft when required CI checks are failing.
34+
The action will attempt to read required checks from branch protection rules.
35+
If it cannot (e.g. insufficient permissions), it falls back to flagging any failing check.
36+
required: false
37+
default: 'true'
38+
post-comment:
39+
description: 'Post a comment on the PR explaining why it was converted to draft.'
40+
required: false
41+
default: 'true'
42+
comment-template:
43+
description: >
44+
Custom comment template. Supports the following placeholders:
45+
{reasons} — a markdown list of reasons why the PR was drafted.
46+
Leave empty to use the default template.
47+
required: false
48+
default: ''
49+
outputs:
50+
drafted:
51+
description: >
52+
JSON array of PR numbers that were converted to draft in this run.
53+
Empty array if no PRs were drafted.
54+
runs:
55+
using: 'node20'
56+
main: 'dist/index.js'

build.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Build script — bundles src/index.ts into a single dist/index.js (CJS)
2+
// using esbuild, which handles both CJS and ESM input packages.
3+
import { build } from 'esbuild';
4+
import { mkdir } from 'node:fs/promises';
5+
6+
await mkdir('dist', { recursive: true });
7+
8+
await build({
9+
entryPoints: ['src/index.ts'],
10+
bundle: true,
11+
platform: 'node',
12+
target: 'node20',
13+
// Output as ESM — Node 20 (used by GitHub Actions) natively supports it.
14+
// package.json has "type": "module" so dist/index.js is treated as ESM.
15+
format: 'esm',
16+
// Some transitive CJS deps use dynamic require() — provide a shim so they
17+
// still work when wrapped inside an ESM bundle.
18+
banner: {
19+
js: `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);`,
20+
},
21+
outfile: 'dist/index.js',
22+
// Keep the bundle self-contained — no external deps needed at runtime.
23+
external: [],
24+
// Source map for easier debugging of action failures.
25+
sourcemap: false,
26+
// Tree-shake dead code.
27+
treeShaking: true,
28+
// Suppress noisy "annotations" warnings from mixed CJS/ESM packages.
29+
logLevel: 'info',
30+
});
31+
32+
33+

0 commit comments

Comments
 (0)