Skip to content

Commit 7e6ff1e

Browse files
Merge branch 'main' into add-topics-langchain-core
2 parents e694177 + 42590df commit 7e6ff1e

38 files changed

Lines changed: 827 additions & 131 deletions

File tree

.github/workflows/explore-triage-commenter-writer.yml

Lines changed: 442 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 126 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
name: Explore PR Triage Commenter
22

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 -->
3+
# Computes maintainer triage data for Explore PRs in an unprivileged
4+
# pull_request workflow. A separate workflow_run workflow writes the sticky
5+
# comment after re-fetching PR state; no privileged job checks out PR code.
116

127
on:
13-
pull_request_target:
8+
pull_request:
149
types: [opened, synchronize, reopened]
1510
paths:
1611
- 'topics/**'
@@ -22,41 +17,53 @@ concurrency:
2217

2318
permissions:
2419
contents: read
25-
pull-requests: write
20+
pull-requests: read
2621

2722
jobs:
28-
triage:
23+
build-comment-data:
2924
runs-on: ubuntu-latest
3025
steps:
31-
- uses: actions/github-script@v9
26+
- name: Build triage comment data
27+
uses: actions/github-script@v9
3228
env:
33-
MARKER: '<!-- explore-triage-comment -->'
29+
OUTPUT_PATH: ${{ runner.temp }}/explore-triage-comment.json
3430
with:
3531
script: |
36-
const marker = process.env.MARKER;
32+
const fs = require('fs');
33+
3734
const pr = context.payload.pull_request;
35+
const baseOwner = context.repo.owner;
36+
const baseRepo = context.repo.repo;
3837
const prNumber = pr.number;
3938
const prAuthor = pr.user.login.toLowerCase();
4039
const headSha = pr.head.sha;
41-
const baseOwner = context.repo.owner;
42-
const baseRepo = context.repo.repo;
4340
44-
// List files in the PR (paginated).
41+
const payload = {
42+
schema: 'explore-triage-comment/v1',
43+
owner: baseOwner,
44+
repo: baseRepo,
45+
prNumber,
46+
headSha,
47+
baseRepoFullName: pr.base.repo.full_name,
48+
headRepoFullName: pr.head.repo && pr.head.repo.full_name,
49+
hasChanges: false,
50+
topics: [],
51+
collections: [],
52+
};
53+
4554
const files = await github.paginate(github.rest.pulls.listFiles, {
4655
owner: baseOwner,
4756
repo: baseRepo,
4857
pull_number: prNumber,
4958
per_page: 100,
5059
});
5160
52-
// Detect topic and collection slugs touched.
53-
// Skip removed files; only validate slug shape we'd ever expect on disk.
5461
const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,80}[a-z0-9])?$/i;
5562
const topics = new Set();
5663
const collections = new Set();
5764
for (const f of files) {
5865
if (f.status === 'removed') continue;
59-
const m = f.filename.match(/^(topics|collections)\/([^\/]+)\//);
66+
const m = f.filename.match(/^(topics|collections)\/([^/]+)\//);
6067
if (!m) continue;
6168
const slug = m[2];
6269
if (!SLUG.test(slug)) continue;
@@ -66,142 +73,121 @@ jobs:
6673
6774
if (topics.size === 0 && collections.size === 0) {
6875
core.info('No topic or collection changes detected; nothing to do.');
76+
writePayload(payload);
6977
return;
7078
}
7179
72-
const sections = [];
80+
payload.hasChanges = true;
81+
82+
for (const slug of [...topics].sort()) {
83+
const topic = { slug, count: null };
84+
try {
85+
const res = await github.rest.search.repos({
86+
q: `topic:${slug}`,
87+
per_page: 1,
88+
});
89+
topic.count = res.data.total_count;
90+
} catch (err) {
91+
core.warning(`Search failed for topic '${slug}': ${err.message}`);
92+
}
93+
payload.topics.push(topic);
94+
}
95+
96+
for (const slug of [...collections].sort()) {
97+
const collection = {
98+
slug,
99+
readStatus: 'ok',
100+
errorStatus: null,
101+
items: [],
102+
};
103+
104+
let content;
105+
try {
106+
content = await readCollectionIndex(slug);
107+
} catch (err) {
108+
collection.readStatus = err.status === 404 ? 'not-found' : 'error';
109+
collection.errorStatus = String(err.status || 'error');
110+
payload.collections.push(collection);
111+
continue;
112+
}
113+
114+
const items = parseCollectionItems(content);
115+
for (const item of items) {
116+
if (!/^[\w.-]+\/[\w.-]+$/.test(item)) {
117+
collection.items.push({ name: item, valid: false });
118+
continue;
119+
}
73120
74-
// ---- Topic section ----
75-
if (topics.size > 0) {
76-
const lines = ['### Topics', ''];
77-
for (const slug of topics) {
78-
let count = null;
121+
const [owner, repo] = item.split('/');
79122
try {
80-
const res = await github.rest.search.repos({
81-
q: `topic:${slug}`,
82-
per_page: 1,
123+
const r = await github.rest.repos.get({ owner, repo });
124+
const notes = [];
125+
if (owner.toLowerCase() === prAuthor) notes.push('possible-self-submission');
126+
if (r.data.archived) notes.push('archived');
127+
if (r.data.disabled) notes.push('disabled');
128+
collection.items.push({
129+
name: item,
130+
valid: true,
131+
lookupStatus: 'ok',
132+
stars: r.data.stargazers_count,
133+
pushed: r.data.pushed_at ? r.data.pushed_at.slice(0, 10) : null,
134+
ownerType: r.data.owner.type,
135+
notes,
83136
});
84-
count = res.data.total_count;
85137
} 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})`);
138+
collection.items.push({
139+
name: item,
140+
valid: true,
141+
lookupStatus: err.status === 404 ? 'not-found' : 'error',
142+
errorStatus: String(err.status || 'error'),
143+
});
93144
}
94145
}
95-
sections.push(lines.join('\n'));
146+
147+
payload.collections.push(collection);
96148
}
97149
98-
// ---- Collection section ----
99-
if (collections.size > 0) {
100-
for (const slug of collections) {
101-
const lines = [`### Collection \`${slug}\``, ''];
150+
writePayload(payload);
151+
152+
async function readCollectionIndex(slug) {
153+
const attempts = [];
154+
if (pr.head.repo) {
155+
attempts.push({
156+
owner: pr.head.repo.owner.login,
157+
repo: pr.head.repo.name,
158+
ref: headSha,
159+
});
160+
}
161+
attempts.push({ owner: baseOwner, repo: baseRepo, ref: headSha });
102162
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;
163+
let lastError;
164+
for (const attempt of attempts) {
108165
try {
109166
const res = await github.rest.repos.getContent({
110-
owner: baseOwner,
111-
repo: baseRepo,
167+
...attempt,
112168
path: `collections/${slug}/index.md`,
113-
ref: headSha,
114169
});
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} |`);
170+
if (Array.isArray(res.data) || res.data.type !== 'file' || !res.data.content) {
171+
const err = new Error('Collection index is not a file');
172+
err.status = 'invalid';
173+
throw err;
152174
}
175+
return Buffer.from(res.data.content, 'base64').toString('utf8');
176+
} catch (err) {
177+
lastError = err;
153178
}
154-
lines.push('');
155-
sections.push(lines.join('\n'));
156179
}
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');
180+
throw lastError;
193181
}
194182
195183
function parseCollectionItems(text) {
196-
// Frontmatter between leading --- lines.
197184
const fmMatch = text.match(/^---\n([\s\S]*?)\n---/);
198185
if (!fmMatch) return [];
199186
const lines = fmMatch[1].split('\n');
200187
const items = [];
201188
let inItems = false;
202189
for (const line of lines) {
203190
if (/^items:\s*$/.test(line)) { inItems = true; continue; }
204-
// Next top-level key ends the items block.
205191
if (inItems && /^[a-zA-Z_]\w*\s*:/.test(line)) break;
206192
if (inItems) {
207193
const m = line.match(/^\s*-\s*([^\s#]+)/);
@@ -210,3 +196,16 @@ jobs:
210196
}
211197
return items;
212198
}
199+
200+
function writePayload(data) {
201+
fs.writeFileSync(process.env.OUTPUT_PATH, JSON.stringify(data, null, 2));
202+
core.info(`Wrote ${process.env.OUTPUT_PATH}`);
203+
}
204+
205+
- name: Upload triage comment data
206+
uses: actions/upload-artifact@v4
207+
with:
208+
name: explore-triage-comment
209+
path: ${{ runner.temp }}/explore-triage-comment.json
210+
if-no-files-found: error
211+
retention-days: 1

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ gem "json", "2.21.2"
77
gem "language_server-protocol", "3.17.0.6"
88
gem "nokogiri", "~> 1.19.4"
99
gem "rake", "13.4.2"
10-
gem "rubocop", "1.88.2"
10+
gem "rubocop", "1.89.0"
1111

1212
group :test do
1313
gem "fastimage"

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ GEM
286286
faraday (>= 1, < 3)
287287
sawyer (~> 0.9)
288288
parallel (2.1.0)
289-
parser (3.3.11.1)
289+
parser (3.3.12.0)
290290
ast (~> 2.4.1)
291291
racc
292292
pathutil (0.16.2)
@@ -308,7 +308,7 @@ GEM
308308
io-console (~> 0.5)
309309
rexml (3.4.2)
310310
rouge (3.30.0)
311-
rubocop (1.88.2)
311+
rubocop (1.89.0)
312312
json (~> 2.3)
313313
language_server-protocol (~> 3.17.0.2)
314314
lint_roller (~> 1.1.0)
@@ -379,7 +379,7 @@ DEPENDENCIES
379379
octokit
380380
pry
381381
rake (= 13.4.2)
382-
rubocop (= 1.88.2)
382+
rubocop (= 1.89.0)
383383
rubocop-performance
384384
safe_yaml
385385
webrick

collections/ai-agents/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ items:
66
- microsoft/semantic-kernel
77
- modelcontextprotocol/servers
88
- gfernandf/agent-skills
9+
- tamish-max/embercore
910
display_name: AI Agents
1011
created_by: gfernandf
1112
---
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
items:
3+
- ajay-dhangar/algo
4+
- codeharborhub/dsa
5+
- TheAlgorithms/Python
6+
- TheAlgorithms/Java
7+
- trekhleb/javascript-algorithms
8+
- jwasham/coding-interview-university
9+
display_name: Algorithms & Data Structures
10+
created_by: ajay-dhangar
11+
---
12+
13+
A curated collection of open-source algorithms, data structures, and implementation guides to help developers master computational problem-solving and technical interviews.

0 commit comments

Comments
 (0)