-
Notifications
You must be signed in to change notification settings - Fork 14.2k
ci: replace disabled topic-commenter with explore-triage-commenter #5151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+212
−78
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
eb82a30
ci: add Explore PR Triage Commenter workflow
kenyonj dd6bb15
ci: remove disabled topic-commenter.yml
kenyonj eb1b6c7
Add concurrency group to prevent duplicate sticky comments
Copilot c6f2ac5
Sanitize item in invalid format branch to prevent Markdown injection
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| name: Explore PR Triage Commenter | ||
|
|
||
| # Posts a sticky comment on PRs that touch topic or collection pages, | ||
| # surfacing the facts maintainers normally look up by hand: | ||
| # - topics: repo count for the topic | ||
| # - collections: per-item stars, last push, owner type, plus a flag if | ||
| # the PR author looks like one of the item owners (self-submission) | ||
| # | ||
| # Edit-in-place: subsequent runs (synchronize, reopen) update the same | ||
| # comment instead of posting a new one. Marker: <!-- explore-triage-comment --> | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, synchronize, reopened] | ||
| paths: | ||
| - 'topics/**' | ||
| - 'collections/**' | ||
|
|
||
| concurrency: | ||
| group: explore-triage-commenter-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
kenyonj marked this conversation as resolved.
|
||
|
|
||
| jobs: | ||
| triage: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/github-script@v9 | ||
| env: | ||
| MARKER: '<!-- explore-triage-comment -->' | ||
| with: | ||
| script: | | ||
| const marker = process.env.MARKER; | ||
| const pr = context.payload.pull_request; | ||
| const prNumber = pr.number; | ||
| const prAuthor = pr.user.login.toLowerCase(); | ||
| const headSha = pr.head.sha; | ||
| const baseOwner = context.repo.owner; | ||
| const baseRepo = context.repo.repo; | ||
|
|
||
| // List files in the PR (paginated). | ||
| const files = await github.paginate(github.rest.pulls.listFiles, { | ||
| owner: baseOwner, | ||
| repo: baseRepo, | ||
| pull_number: prNumber, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| // Detect topic and collection slugs touched. | ||
| // Skip removed files; only validate slug shape we'd ever expect on disk. | ||
| const SLUG = /^[a-z0-9](?:[a-z0-9-]{0,80}[a-z0-9])?$/i; | ||
| const topics = new Set(); | ||
| const collections = new Set(); | ||
| for (const f of files) { | ||
| if (f.status === 'removed') continue; | ||
| const m = f.filename.match(/^(topics|collections)\/([^\/]+)\//); | ||
| if (!m) continue; | ||
| const slug = m[2]; | ||
| if (!SLUG.test(slug)) continue; | ||
| if (m[1] === 'topics') topics.add(slug); | ||
| else collections.add(slug); | ||
| } | ||
|
|
||
| if (topics.size === 0 && collections.size === 0) { | ||
| core.info('No topic or collection changes detected; nothing to do.'); | ||
| return; | ||
| } | ||
|
|
||
| const sections = []; | ||
|
|
||
| // ---- Topic section ---- | ||
| if (topics.size > 0) { | ||
| const lines = ['### Topics', '']; | ||
| for (const slug of topics) { | ||
| let count = null; | ||
| try { | ||
| const res = await github.rest.search.repos({ | ||
| q: `topic:${slug}`, | ||
| per_page: 1, | ||
| }); | ||
| count = res.data.total_count; | ||
| } catch (err) { | ||
| core.warning(`Search failed for topic '${slug}': ${err.message}`); | ||
| } | ||
| const url = `https://github.com/topics/${encodeURIComponent(slug)}`; | ||
| if (count == null) { | ||
| lines.push(`- **${slug}** — [topic page](${url}) _(repo count lookup failed)_`); | ||
| } else { | ||
| lines.push(`- **${slug}** — ${count.toLocaleString()} repositories — [topic page](${url})`); | ||
| } | ||
| } | ||
| sections.push(lines.join('\n')); | ||
| } | ||
|
|
||
| // ---- Collection section ---- | ||
| if (collections.size > 0) { | ||
| for (const slug of collections) { | ||
| const lines = [`### Collection \`${slug}\``, '']; | ||
|
|
||
| // Read collection's index.md at the PR head SHA. | ||
| // PR commits from forks are mirrored into the base repo's network, | ||
| // so we can fetch from the base repo with the head SHA — simpler | ||
| // and avoids any cross-repo token concerns. | ||
| let content; | ||
| try { | ||
| const res = await github.rest.repos.getContent({ | ||
| owner: baseOwner, | ||
| repo: baseRepo, | ||
| path: `collections/${slug}/index.md`, | ||
| ref: headSha, | ||
| }); | ||
| content = Buffer.from(res.data.content, 'base64').toString('utf8'); | ||
| } catch (err) { | ||
| lines.push(`_Could not read \`collections/${slug}/index.md\` at PR head (\`${err.status || 'error'}\`)._`); | ||
| sections.push(lines.join('\n')); | ||
| continue; | ||
| } | ||
|
|
||
| const items = parseCollectionItems(content); | ||
| if (items.length === 0) { | ||
| lines.push('_No `items:` list found in frontmatter._'); | ||
| sections.push(lines.join('\n')); | ||
| continue; | ||
| } | ||
|
|
||
| lines.push('| Item | Stars | Last push | Owner type | Notes |'); | ||
| lines.push('| --- | ---: | --- | --- | --- |'); | ||
|
|
||
| for (const item of items) { | ||
| if (!/^[\w.-]+\/[\w.-]+$/.test(item)) { | ||
| const safeItem = item.replace(/`/g, "'").replace(/\\/g, '\\\\').replace(/\|/g, '\\|'); | ||
| lines.push(`| \`${safeItem}\` | – | – | – | invalid format |`); | ||
| continue; | ||
|
kenyonj marked this conversation as resolved.
|
||
| } | ||
| const [owner, repo] = item.split('/'); | ||
| try { | ||
| const r = await github.rest.repos.get({ owner, repo }); | ||
| const stars = r.data.stargazers_count.toLocaleString(); | ||
| const pushed = r.data.pushed_at ? r.data.pushed_at.slice(0, 10) : '–'; | ||
| const ownerType = r.data.owner.type; | ||
| const notes = []; | ||
| if (owner.toLowerCase() === prAuthor) notes.push('⚠️ possible self-submission'); | ||
| if (r.data.archived) notes.push('archived'); | ||
| if (r.data.disabled) notes.push('disabled'); | ||
| lines.push(`| [\`${item}\`](https://github.com/${item}) | ${stars} | ${pushed} | ${ownerType} | ${notes.join(', ') || '–'} |`); | ||
| } catch (err) { | ||
| const note = err.status === 404 ? 'not found' : `error (${err.status || '?'})`; | ||
| lines.push(`| \`${item}\` | – | – | – | ${note} |`); | ||
| } | ||
| } | ||
| lines.push(''); | ||
| sections.push(lines.join('\n')); | ||
| } | ||
| } | ||
|
|
||
| const body = [ | ||
| marker, | ||
| '<!-- Maintained by .github/workflows/explore-triage-commenter.yml. Edits will be overwritten. -->', | ||
| '', | ||
| '## Maintainer triage', | ||
| '', | ||
| ...sections, | ||
| ].join('\n'); | ||
|
|
||
| // Edit-in-place via marker. | ||
| const comments = await github.paginate(github.rest.issues.listComments, { | ||
| owner: baseOwner, | ||
| repo: baseRepo, | ||
| issue_number: prNumber, | ||
| per_page: 100, | ||
| }); | ||
| const existing = comments.find(c => c.body && c.body.startsWith(marker)); | ||
|
|
||
| if (existing) { | ||
| await github.rest.issues.updateComment({ | ||
| owner: baseOwner, | ||
| repo: baseRepo, | ||
| comment_id: existing.id, | ||
| body, | ||
| }); | ||
| core.info(`Updated comment ${existing.id}`); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner: baseOwner, | ||
| repo: baseRepo, | ||
| issue_number: prNumber, | ||
| body, | ||
| }); | ||
| core.info('Created new comment'); | ||
| } | ||
|
|
||
| function parseCollectionItems(text) { | ||
| // Frontmatter between leading --- lines. | ||
| const fmMatch = text.match(/^---\n([\s\S]*?)\n---/); | ||
| if (!fmMatch) return []; | ||
| const lines = fmMatch[1].split('\n'); | ||
| const items = []; | ||
| let inItems = false; | ||
| for (const line of lines) { | ||
| if (/^items:\s*$/.test(line)) { inItems = true; continue; } | ||
| // Next top-level key ends the items block. | ||
| if (inItems && /^[a-zA-Z_]\w*\s*:/.test(line)) break; | ||
| if (inItems) { | ||
| const m = line.match(/^\s*-\s*([^\s#]+)/); | ||
| if (m) items.push(m[1]); | ||
| } | ||
| } | ||
| return items; | ||
| } | ||
This file was deleted.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.