Skip to content

Commit a1aa22d

Browse files
authored
Merge pull request #577 from nextcloud/chore/ci/stale-action
chore(ci-action): smart CI actions to sort github issues
2 parents befc665 + c469cc9 commit a1aa22d

4 files changed

Lines changed: 229 additions & 0 deletions

File tree

.github/issue-bot-config.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
triage:
4+
first_ping_after_days: 7 # days to triage without reminder
5+
repeat_every_days: 7 # repeat reminder after X days
6+
escalation_ping: 3 # on which reminder it will be escalated
7+
escalation_mentions:
8+
- "@DaphneMuller"
9+
10+
question:
11+
reminder_after_days: 7 # days to remove "question" tag, and apply tag to mark the issue as available to close.
12+
missing_info_label: "missing information"
13+
maintainers_allowlist: # their comments DO NOT clear the "question" label
14+
- "oleksandr-nc"
15+
- "kyteinsky"
16+
- "andrey18106"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Flag stale "question" issues
4+
5+
on:
6+
schedule:
7+
- cron: '0 2 * * *' # 02:00 UTC daily
8+
workflow_dispatch:
9+
10+
permissions:
11+
issues: write
12+
contents: read
13+
14+
jobs:
15+
mark-stale:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
19+
- name: Load question config via yq
20+
id: cfg
21+
run: |
22+
QUESTION=$(yq eval -o=json '.question' .github/triage-issue-bot-config.yml)
23+
printf "question<<EOF\n%s\nEOF\n" "$QUESTION" >> $GITHUB_OUTPUT
24+
25+
- name: Label stale question issues
26+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
27+
with:
28+
github-token: ${{ secrets.GITHUB_TOKEN }}
29+
script: |
30+
const cfg = JSON.parse(process.env.INPUT_QUESTION || '{}');
31+
const { data: issues } = await github.rest.issues.listForRepo({
32+
...context.repo,
33+
labels: 'question',
34+
state: 'open',
35+
per_page: 100
36+
});
37+
38+
const now = Date.now();
39+
for (const issue of issues) {
40+
const events = await github.rest.issues.listEventsForTimeline({
41+
...context.repo,
42+
issue_number: issue.number,
43+
per_page: 100
44+
});
45+
const humans = events.data
46+
.filter(e => e.event === 'commented' && !e.actor.type.includes('Bot'))
47+
.map(e => new Date(e.created_at).getTime());
48+
const lastHuman = humans.length
49+
? Math.max(...humans)
50+
: new Date(issue.created_at).getTime();
51+
const ageDays = (now - lastHuman) / (24*3600*1000);
52+
53+
if (ageDays >= cfg.reminder_after_days) {
54+
await github.rest.issues.removeLabel({
55+
...context.repo,
56+
issue_number: issue.number,
57+
name: 'question'
58+
}).catch(() => {});
59+
await github.rest.issues.addLabels({
60+
...context.repo,
61+
issue_number: issue.number,
62+
labels: [cfg.missing_info_label]
63+
});
64+
}
65+
}
66+
env:
67+
INPUT_QUESTION: ${{ steps.cfg.outputs.question }}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Clear "question" & "missing information"
4+
5+
on:
6+
issue_comment:
7+
types: [created]
8+
9+
permissions:
10+
issues: write
11+
12+
jobs:
13+
clear-labels:
14+
if: github.event.issue.state == 'open'
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
18+
- name: Load question config via yq
19+
id: cfg
20+
run: |
21+
QUESTION=$(yq eval -o=json '.question' .github/triage-issue-bot-config.yml)
22+
printf "question<<EOF\n%s\nEOF\n" "$QUESTION" >> $GITHUB_OUTPUT
23+
24+
- name: Remove stale labels
25+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
26+
with:
27+
github-token: ${{ secrets.GITHUB_TOKEN }}
28+
script: |
29+
const cfg = JSON.parse(process.env.INPUT_QUESTION || '{}');
30+
const allow = cfg.maintainers_allowlist.map(u => u.toLowerCase());
31+
const user = context.payload.comment.user.login.toLowerCase();
32+
if (allow.includes(user)) return;
33+
34+
const labels = context.payload.issue.labels.map(l => l.name);
35+
for (const name of ['question', 'question-reminded', cfg.missing_info_label]) {
36+
if (labels.includes(name)) {
37+
await github.rest.issues.removeLabel({
38+
...context.repo,
39+
issue_number: context.issue.number,
40+
name
41+
}).catch(() => {});
42+
}
43+
}
44+
env:
45+
INPUT_QUESTION: ${{ steps.cfg.outputs.question }}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Issue triage reminders
4+
5+
on:
6+
schedule:
7+
- cron: '0 2 * * *' # 02:00 UTC daily
8+
workflow_dispatch:
9+
10+
permissions:
11+
issues: write
12+
contents: read
13+
14+
jobs:
15+
remind:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
19+
- name: Load triage config via yq
20+
id: cfg
21+
run: |
22+
TRIAGE=$(yq eval -o=json '.triage' .github/triage-issue-bot-config.yml)
23+
printf "triage<<EOF\n%s\nEOF\n" "$TRIAGE" >> $GITHUB_OUTPUT
24+
25+
- name: Find stale issues
26+
id: search
27+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
28+
with:
29+
github-token: ${{ secrets.GITHUB_TOKEN }}
30+
script: |
31+
const cfg = JSON.parse(process.env.INPUT_TRIAGE || '{}');
32+
const since = new Date(
33+
Date.now() - cfg.first_ping_after_days * 24*3600*1000
34+
).toISOString();
35+
36+
const q = [
37+
`repo:${context.repo.owner}/${context.repo.repo}`,
38+
`is:issue is:open`,
39+
`updated:<${since}`
40+
].join(' ');
41+
42+
const res = await github.rest.search.issuesAndPullRequests({
43+
q, per_page: 100
44+
});
45+
core.setOutput('issues', JSON.stringify(
46+
res.data.items.map(i => i.number)
47+
));
48+
env:
49+
INPUT_TRIAGE: ${{ steps.cfg.outputs.triage }}
50+
51+
- name: Ping & rotate labels
52+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
53+
with:
54+
github-token: ${{ secrets.GITHUB_TOKEN }}
55+
script: |
56+
const issues = JSON.parse(process.env.ISSUES || '[]');
57+
const cfg = JSON.parse(process.env.INPUT_TRIAGE || '{}');
58+
59+
for (const num of issues) {
60+
const { data: issue } = await github.rest.issues.get({
61+
...context.repo, issue_number: num
62+
});
63+
64+
const labels = issue.labels.map(l => l.name);
65+
if (labels.some(n => !/^reminder-\d+$/.test(n))) continue;
66+
67+
const cutoff = new Date(
68+
Date.now() - cfg.repeat_every_days * 24*3600*1000
69+
);
70+
if (new Date(issue.updated_at) > cutoff) continue;
71+
72+
const remLabel = labels.find(n => /^reminder-\d+$/.test(n));
73+
const current = remLabel ? parseInt(remLabel.split('-')[1],10) : 0;
74+
const next = current + 1;
75+
76+
const mention = next >= cfg.escalation_ping
77+
? '\n\n' + (cfg.escalation_mentions || []).join(' ')
78+
: '';
79+
80+
await github.rest.issues.createComment({
81+
...context.repo,
82+
issue_number: num,
83+
body: `🔔 Friendly reminder – please triage this issue.${mention}`
84+
});
85+
86+
if (remLabel) {
87+
await github.rest.issues.removeLabel({
88+
...context.repo,
89+
issue_number: num,
90+
name: remLabel
91+
}).catch(() => {});
92+
}
93+
await github.rest.issues.addLabels({
94+
...context.repo,
95+
issue_number: num,
96+
labels: [`reminder-${next}`]
97+
});
98+
}
99+
env:
100+
ISSUES: ${{ steps.search.outputs.issues }}
101+
INPUT_TRIAGE: ${{ steps.cfg.outputs.triage }}

0 commit comments

Comments
 (0)