-
Notifications
You must be signed in to change notification settings - Fork 0
129 lines (117 loc) · 5.34 KB
/
triage-bot.yml
File metadata and controls
129 lines (117 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Triage Bot — auto-classify unresolved review threads, resolve the
# non-blocking ones, leave needs-human ones to keep the PR blocked.
#
# Closes the gap where native auto-merge fires the moment CI goes green,
# even if Copilot / CodeRabbit later post review comments. With
# branch protection `required_conversation_resolution: true` (or a
# ruleset that requires it) any unresolved thread keeps the PR at
# mergeStateStatus=BLOCKED until explicitly resolved.
#
# Decision rules (v1, deterministic — upgrade to LLM is the next iteration):
# - body contains `[triage:auto-resolve]` → dismiss
# - body matches /^(nit:|nitpick:|praise:)/i → dismiss
# - author is Copilot, body < 200 chars → dismiss
# - anything else → needs-human (default: conservative)
#
# Synced from github-settings-automation/templates/triage-bot.yml by the
# weekly enforce-repo-settings sweep. Do not hand-edit per-repo.
#
# Prereqs in each consuming repo:
# 1. Secret TRIAGE_PAT — classic PAT or GitHub App installation token
# with `pull_requests: write`. The default GITHUB_TOKEN cannot
# `resolveReviewThread` on threads it didn't author and returns
# "Resource not accessible by integration". Validated in
# ANcpLua/triage-bot-playground PR #1 + #2 (2026-05-18).
# 2. Branch protection / ruleset on the default branch with
# `required_conversation_resolution: true`. Without it the gate
# doesn't exist and the workflow only adds report comments.
name: Triage Bot
on:
pull_request_review_comment:
types: [created, edited]
pull_request_review:
types: [submitted]
workflow_dispatch:
inputs:
pr_number:
description: PR number to triage manually
required: true
type: number
permissions:
contents: read
pull-requests: write
concurrency:
group: triage-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: false
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Triage unresolved review threads
uses: actions/github-script@v7
with:
github-token: ${{ secrets.TRIAGE_PAT }}
script: |
const { owner, repo } = context.repo;
const pr_number = context.payload.pull_request?.number
?? Number(context.payload.inputs?.pr_number);
if (!pr_number) { core.setFailed('no PR number'); return; }
const data = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id isResolved viewerCanResolve
comments(first: 5) {
nodes { id body author { login } }
}
}
}
}
}
}`, { owner, repo, number: pr_number });
const threads = data.repository.pullRequest.reviewThreads.nodes
.filter(t => !t.isResolved);
core.info(`PR #${pr_number}: ${threads.length} unresolved thread(s)`);
const summary = [];
for (const t of threads) {
const first = t.comments.nodes[0] ?? {};
const body = first.body ?? '';
const author = first.author?.login ?? 'unknown';
const snippet = body.slice(0, 80).replace(/\s+/g, ' ');
let decision = 'needs-human';
let reason = 'no rule matched — keeping blocked for manual review';
if (body.includes('[triage:auto-resolve]')) {
decision = 'dismiss';
reason = 'explicit [triage:auto-resolve] marker present';
} else if (/^\s*(nit:|nitpick:|praise:)/i.test(body)) {
decision = 'dismiss';
reason = 'classified as nit/nitpick/praise — non-blocking';
} else if (author === 'Copilot' && body.length < 200) {
decision = 'dismiss';
reason = 'short Copilot comment, treated as informational';
}
core.info(`thread ${t.id.slice(-8)} from @${author}: decision=${decision} viewerCanResolve=${t.viewerCanResolve}`);
summary.push(
`- thread ${t.id.slice(-8)} from @${author}: **${decision}** — ${reason}\n > ${snippet}`);
if (decision === 'dismiss') {
try {
const res = await github.graphql(`
mutation($id: ID!) {
resolveReviewThread(input: { threadId: $id }) {
thread { id isResolved }
}
}`, { id: t.id });
core.info(`resolved ${t.id.slice(-8)} -> isResolved=${res.resolveReviewThread.thread.isResolved}`);
} catch (err) {
core.warning(`resolve failed for ${t.id.slice(-8)}: ${err.message}`);
}
}
}
if (summary.length > 0) {
await github.rest.issues.createComment({
owner, repo, issue_number: pr_number,
body: `## Triage Bot report\n\n${summary.join('\n')}\n\n_Threads marked \`needs-human\` stay unresolved and block auto-merge._`,
});
}