Skip to content

Commit 18f9964

Browse files
authored
Merge branch 'main' into main
2 parents 0741065 + ded07c7 commit 18f9964

2 files changed

Lines changed: 212 additions & 78 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
name: Explore PR Triage Commenter
2+
3+
# Posts a sticky comment on PRs that touch topic or collection pages,
4+
# surfacing the facts maintainers normally look up by hand:
5+
# - topics: repo count for the topic
6+
# - collections: per-item stars, last push, owner type, plus a flag if
7+
# the PR author looks like one of the item owners (self-submission)
8+
#
9+
# Edit-in-place: subsequent runs (synchronize, reopen) update the same
10+
# comment instead of posting a new one. Marker: <!-- explore-triage-comment -->
11+
12+
on:
13+
pull_request_target:
14+
types: [opened, synchronize, reopened]
15+
paths:
16+
- 'topics/**'
17+
- 'collections/**'
18+
19+
concurrency:
20+
group: explore-triage-commenter-${{ github.event.pull_request.number }}
21+
cancel-in-progress: true
22+
23+
permissions:
24+
contents: read
25+
pull-requests: write
26+
27+
jobs:
28+
triage:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/github-script@v9
32+
env:
33+
MARKER: '<!-- explore-triage-comment -->'
34+
with:
35+
script: |
36+
const marker = process.env.MARKER;
37+
const pr = context.payload.pull_request;
38+
const prNumber = pr.number;
39+
const prAuthor = pr.user.login.toLowerCase();
40+
const headSha = pr.head.sha;
41+
const baseOwner = context.repo.owner;
42+
const baseRepo = context.repo.repo;
43+
44+
// List files in the PR (paginated).
45+
const files = await github.paginate(github.rest.pulls.listFiles, {
46+
owner: baseOwner,
47+
repo: baseRepo,
48+
pull_number: prNumber,
49+
per_page: 100,
50+
});
51+
52+
// Detect topic and collection slugs touched.
53+
// Skip removed files; only validate slug shape we'd ever expect on disk.
54+
const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,80}[a-z0-9])?$/i;
55+
const topics = new Set();
56+
const collections = new Set();
57+
for (const f of files) {
58+
if (f.status === 'removed') continue;
59+
const m = f.filename.match(/^(topics|collections)\/([^\/]+)\//);
60+
if (!m) continue;
61+
const slug = m[2];
62+
if (!SLUG.test(slug)) continue;
63+
if (m[1] === 'topics') topics.add(slug);
64+
else collections.add(slug);
65+
}
66+
67+
if (topics.size === 0 && collections.size === 0) {
68+
core.info('No topic or collection changes detected; nothing to do.');
69+
return;
70+
}
71+
72+
const sections = [];
73+
74+
// ---- Topic section ----
75+
if (topics.size > 0) {
76+
const lines = ['### Topics', ''];
77+
for (const slug of topics) {
78+
let count = null;
79+
try {
80+
const res = await github.rest.search.repos({
81+
q: `topic:${slug}`,
82+
per_page: 1,
83+
});
84+
count = res.data.total_count;
85+
} catch (err) {
86+
core.warning(`Search failed for topic '${slug}': ${err.message}`);
87+
}
88+
const url = `https://github.com/topics/${encodeURIComponent(slug)}`;
89+
if (count == null) {
90+
lines.push(`- **${slug}** — [topic page](${url}) _(repo count lookup failed)_`);
91+
} else {
92+
lines.push(`- **${slug}** — ${count.toLocaleString()} repositories — [topic page](${url})`);
93+
}
94+
}
95+
sections.push(lines.join('\n'));
96+
}
97+
98+
// ---- Collection section ----
99+
if (collections.size > 0) {
100+
for (const slug of collections) {
101+
const lines = [`### Collection \`${slug}\``, ''];
102+
103+
// Read collection's index.md at the PR head SHA.
104+
// PR commits from forks are mirrored into the base repo's network,
105+
// so we can fetch from the base repo with the head SHA — simpler
106+
// and avoids any cross-repo token concerns.
107+
let content;
108+
try {
109+
const res = await github.rest.repos.getContent({
110+
owner: baseOwner,
111+
repo: baseRepo,
112+
path: `collections/${slug}/index.md`,
113+
ref: headSha,
114+
});
115+
content = Buffer.from(res.data.content, 'base64').toString('utf8');
116+
} catch (err) {
117+
lines.push(`_Could not read \`collections/${slug}/index.md\` at PR head (\`${err.status || 'error'}\`)._`);
118+
sections.push(lines.join('\n'));
119+
continue;
120+
}
121+
122+
const items = parseCollectionItems(content);
123+
if (items.length === 0) {
124+
lines.push('_No `items:` list found in frontmatter._');
125+
sections.push(lines.join('\n'));
126+
continue;
127+
}
128+
129+
lines.push('| Item | Stars | Last push | Owner type | Notes |');
130+
lines.push('| --- | ---: | --- | --- | --- |');
131+
132+
for (const item of items) {
133+
if (!/^[\w.-]+\/[\w.-]+$/.test(item)) {
134+
const safeItem = item.replace(/`/g, "'").replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
135+
lines.push(`| \`${safeItem}\` | – | – | – | invalid format |`);
136+
continue;
137+
}
138+
const [owner, repo] = item.split('/');
139+
try {
140+
const r = await github.rest.repos.get({ owner, repo });
141+
const stars = r.data.stargazers_count.toLocaleString();
142+
const pushed = r.data.pushed_at ? r.data.pushed_at.slice(0, 10) : '–';
143+
const ownerType = r.data.owner.type;
144+
const notes = [];
145+
if (owner.toLowerCase() === prAuthor) notes.push('⚠️ possible self-submission');
146+
if (r.data.archived) notes.push('archived');
147+
if (r.data.disabled) notes.push('disabled');
148+
lines.push(`| [\`${item}\`](https://github.com/${item}) | ${stars} | ${pushed} | ${ownerType} | ${notes.join(', ') || '–'} |`);
149+
} catch (err) {
150+
const note = err.status === 404 ? 'not found' : `error (${err.status || '?'})`;
151+
lines.push(`| \`${item}\` | – | – | – | ${note} |`);
152+
}
153+
}
154+
lines.push('');
155+
sections.push(lines.join('\n'));
156+
}
157+
}
158+
159+
const body = [
160+
marker,
161+
'<!-- Maintained by .github/workflows/explore-triage-commenter.yml. Edits will be overwritten. -->',
162+
'',
163+
'## Maintainer triage',
164+
'',
165+
...sections,
166+
].join('\n');
167+
168+
// Edit-in-place via marker.
169+
const comments = await github.paginate(github.rest.issues.listComments, {
170+
owner: baseOwner,
171+
repo: baseRepo,
172+
issue_number: prNumber,
173+
per_page: 100,
174+
});
175+
const existing = comments.find(c => c.body && c.body.startsWith(marker));
176+
177+
if (existing) {
178+
await github.rest.issues.updateComment({
179+
owner: baseOwner,
180+
repo: baseRepo,
181+
comment_id: existing.id,
182+
body,
183+
});
184+
core.info(`Updated comment ${existing.id}`);
185+
} else {
186+
await github.rest.issues.createComment({
187+
owner: baseOwner,
188+
repo: baseRepo,
189+
issue_number: prNumber,
190+
body,
191+
});
192+
core.info('Created new comment');
193+
}
194+
195+
function parseCollectionItems(text) {
196+
// Frontmatter between leading --- lines.
197+
const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
198+
if (!fmMatch) return [];
199+
const lines = fmMatch[1].split('\n');
200+
const items = [];
201+
let inItems = false;
202+
for (const line of lines) {
203+
if (/^items:\s*$/.test(line)) { inItems = true; continue; }
204+
// Next top-level key ends the items block.
205+
if (inItems && /^[a-zA-Z_]\w*\s*:/.test(line)) break;
206+
if (inItems) {
207+
const m = line.match(/^\s*-\s*([^\s#]+)/);
208+
if (m) items.push(m[1]);
209+
}
210+
}
211+
return items;
212+
}

.github/workflows/topic-commenter.yml

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)