Skip to content

Commit 1c9fc3b

Browse files
committed
feat(stale): add stale action
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 66abe3e commit 1c9fc3b

4 files changed

Lines changed: 230 additions & 1 deletion

File tree

‎.github/release-please-config.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
"actions/release-please": {
2828
"component": "release-please",
2929
"initial-version": "0.0.1"
30+
},
31+
"actions/stale": {
32+
"component": "stale",
33+
"initial-version": "0.0.1"
3034
}
3135
},
3236
"plugins": [
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"actions/semconv/pull-request": "0.0.1",
3-
"actions/release-please": "0.0.2"
3+
"actions/release-please": "0.0.2",
4+
"actions/stale": "0.0.1"
45
}

‎actions/stale/README.md‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# stale
2+
3+
Marks issues and pull requests that have gone quiet, then closes them if they
4+
stay quiet. A thin, opinionated wrapper around
5+
[`actions/stale`](https://github.com/actions/stale).
6+
7+
## Usage
8+
9+
The caller owns the schedule.
10+
11+
```yaml
12+
name: Stale
13+
14+
on:
15+
schedule:
16+
- cron: '0 3 * * *'
17+
workflow_dispatch:
18+
19+
permissions: {}
20+
21+
jobs:
22+
stale:
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 10
25+
permissions:
26+
actions: write
27+
issues: write
28+
pull-requests: write
29+
steps:
30+
- uses: TrogonStack/github-actions/actions/stale@<sha> # vX.Y.Z
31+
```
32+
33+
`actions: write` is not optional. Upstream keeps its progress in the Actions
34+
cache so a run capped by `operations-per-run` resumes where the last one
35+
stopped, and managing that cache entry needs the permission. Without it the
36+
cache delete fails with a 403, every run replays the same prefix of the
37+
backlog, and anything past that prefix is never closed.
38+
39+
Run it once with `debug-only: true` before pointing it at a repository with
40+
years of backlog, so the first real run is not a surprise.
41+
42+
## Labels
43+
44+
| Label | Meaning |
45+
| --- | --- |
46+
| `stale:discard` | Applied by this action. Any activity removes it. |
47+
| `stale:keep` | Applied by a human. Exempts the issue or pull request forever. |
48+
49+
Neither label needs to exist beforehand; `stale:discard` is created on first
50+
use. Create `stale:keep` yourself so it is available in the label picker.
51+
52+
The names are not inputs. One vocabulary across every repository is the reason
53+
this action exists, and a rename in one repository is the drift it was built to
54+
remove.
55+
56+
## Inputs
57+
58+
| Input | Default | Description |
59+
| --- | --- | --- |
60+
| `days-before-stale` | `60` | Days without activity before marking stale. |
61+
| `days-before-close` | `7` | Days after marking before closing. `-1` marks but never closes. |
62+
| `operations-per-run` | `100` | API operations budget for one run. A run that hits the cap resumes from where it stopped. |
63+
| `debug-only` | `false` | Report what would happen and change nothing. |
64+
| `token` | `github.token` | Needs write access to actions, issues, and pull requests. |
65+
66+
## Fixed behaviour
67+
68+
These are not inputs, on purpose.
69+
70+
- The label names are `stale:discard` and `stale:keep` everywhere.
71+
- Activity removes the stale label. Activity is the entire signal the action
72+
runs on, so it has to be able to clear it.
73+
- Issues close as `not_planned`, which avoids the completed badge on something
74+
nobody did.
75+
- Anything on a milestone is exempt. Work on a milestone is planned work,
76+
however quiet it has gone.
77+
- Oldest first, so a capped operations budget is spent on the worst offenders
78+
rather than on whatever is newest.
79+
- Branches are never deleted. Deleting someone else's branch on a timer is not
80+
ours to do.
81+
82+
If you need one of these to differ, that is a conversation about the default
83+
rather than an input to add.
84+
85+
## Outputs
86+
87+
| Output | Description |
88+
| --- | --- |
89+
| `staled-issues-prs` | JSON array of what was marked stale this run. |
90+
| `closed-issues-prs` | JSON array of what was closed this run. |
91+
92+
## Messages
93+
94+
Closing is not a verdict, and the posted messages say so. What they ask for
95+
differs by kind, on purpose.
96+
97+
An issue is closed because nobody is building it, so the message asks for a
98+
pull request. It deliberately does not offer "leave a comment" as the way to
99+
revive one: a comment clears the stale label without moving the issue any
100+
closer to done, which is how a backlog gets kept alive without getting
101+
shipped.
102+
103+
A pull request is already the contribution, so the message asks for a commit
104+
instead, and points at `stale:keep` for the case where the hold-up is a review
105+
on our side rather than the author.
106+
107+
Keep that split if you change the wording.
108+
109+
## Bumping
110+
111+
A comment clears the stale label and buys another full cycle, on issues and on
112+
pull requests alike. That is upstream behaviour and it is not configurable:
113+
114+
- `remove-stale-when-updated` controls whether an update removes the label, and
115+
a comment counts as an update.
116+
- Turning it off does not help. The close gate in `actions/stale` is
117+
`if (!issueHasCommentsSinceStale && !issueHasUpdateInCloseWindow)`, so any
118+
human comment after the label blocks closing whatever that option is set to.
119+
- `ignore-pr-updates` measures from `created_at` instead of `updated_at`, which
120+
would mark every long lived pull request stale no matter how much work it is
121+
getting.
122+
123+
So a determined bump wins, and the messages say plainly that it is a limitation
124+
rather than the intended path: on a pull request the thing that counts is a
125+
commit, and a bump leaves the work exactly as unfinished as it was.
126+
127+
Making commits the real currency would mean taking the pull request lifecycle
128+
off upstream and measuring it from the head commit date. That is a bigger
129+
change than this action wants to be today.

‎actions/stale/action.yml‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Stale
2+
description: >-
3+
Marks issues and pull requests that have gone quiet, then closes them if they
4+
stay quiet.
5+
author: TrogonStack
6+
7+
inputs:
8+
days-before-stale:
9+
description: Days without activity before an issue or pull request is marked stale.
10+
required: false
11+
default: '60'
12+
days-before-close:
13+
description: >-
14+
Days to wait after marking stale before closing. Set to -1 to mark stale
15+
but never close.
16+
required: false
17+
default: '7'
18+
operations-per-run:
19+
description: >-
20+
API operations budget for a single run. Raise it for a busy repository,
21+
lower it to stay well inside the rate limit.
22+
required: false
23+
default: '100'
24+
debug-only:
25+
description: >-
26+
Report what would happen without commenting, labelling, or closing
27+
anything. Worth one run before enabling this on an old repository.
28+
required: false
29+
default: 'false'
30+
token:
31+
description: >-
32+
Token used to comment, label, and close. Needs write access to issues and
33+
pull requests.
34+
required: false
35+
default: ${{ github.token }}
36+
37+
outputs:
38+
staled-issues-prs:
39+
description: JSON array of the issues and pull requests marked stale this run.
40+
value: ${{ steps.stale.outputs.staled-issues-prs }}
41+
closed-issues-prs:
42+
description: JSON array of the issues and pull requests closed this run.
43+
value: ${{ steps.stale.outputs.closed-issues-prs }}
44+
45+
runs:
46+
using: composite
47+
steps:
48+
- name: Mark and close stale issues and pull requests
49+
id: stale
50+
uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
51+
with:
52+
repo-token: ${{ inputs.token }}
53+
54+
days-before-stale: ${{ inputs.days-before-stale }}
55+
days-before-close: ${{ inputs.days-before-close }}
56+
57+
# The label names are deliberately not inputs: one vocabulary across
58+
# every repository is the reason this action exists.
59+
stale-issue-label: 'stale:discard'
60+
stale-pr-label: 'stale:discard'
61+
exempt-issue-labels: 'stale:keep'
62+
exempt-pr-labels: 'stale:keep'
63+
64+
stale-issue-message: |
65+
No activity for ${{ inputs.days-before-stale }} days, so this is labelled `stale:discard` and closes in ${{ inputs.days-before-close }} days.
66+
67+
Closing is not a no. It means nobody is building it. The way forward is a pull request, not a comment. Add `stale:keep` if you are actively working on it.
68+
69+
stale-pr-message: |
70+
No activity for ${{ inputs.days-before-stale }} days, so this is labelled `stale:discard` and closes in ${{ inputs.days-before-close }} days.
71+
72+
Push a commit. A bump comment also clears the label, but that is a limitation of the bot, not the path: it leaves the work as unfinished as it was.
73+
74+
If it is blocked on us, say what it is waiting on and add `stale:keep`.
75+
76+
close-issue-message: |
77+
Closed as inactive, not rejected. A pull request implementing it is welcome without reopening this first.
78+
79+
close-pr-message: |
80+
Closed as inactive. The branch is untouched, so nothing is lost. Reopen it when you are ready to push the next commit.
81+
82+
operations-per-run: ${{ inputs.operations-per-run }}
83+
debug-only: ${{ inputs.debug-only }}
84+
85+
# Opinions this action does not hand back to the caller.
86+
# Activity is the whole signal, so activity has to clear the label.
87+
remove-stale-when-updated: true
88+
# "not_planned" avoids the completed badge on something nobody did.
89+
close-issue-reason: not_planned
90+
# Work on a milestone is planned work, however quiet it has gone.
91+
exempt-all-milestones: true
92+
# Oldest first, so the operations budget is spent on the worst offenders.
93+
ascending: true
94+
# Deleting someone else's branch on a timer is not ours to do.
95+
delete-branch: false

0 commit comments

Comments
 (0)