ci(triage): label pull requests by path and title; add app, os and priority label families

Extends the issue triage workflow (the predecessor it replaces: its area and os
rules, which targeted label names that did not exist on the repository, so they
never fired) to pull_request_target. Pull requests now get area labels from the
files they change (including the desktop, web and mobile paths the apps will
land under), a type label from the conventional-commit title prefix, and
contributor:external for authors outside the team. It adds labels only, and only
ones that already exist; nothing is enforced and nothing is removed.

Issue rules gain desktop, web, mobile, accessibility, freebsd and ohos matches;
the sandbox hint now maps to area:sandbox. CONTRIBUTING.md documents the label
families.

Repository labels changed alongside this commit (applied with gh label edit /
create, history preserved on rename): tui, mcp, tools, plugins, providers,
packaging, runtime-api, remote-control, ci, sandbox, subagents, localization ->
area:*; windows, linux, macos -> os:*; created area:core, area:state,
area:desktop, area:web, area:mobile, os:freebsd, os:ohos, lang:zh,
priority:p0/p1/p2, accessibility, contributor:external. No workflow, template or
script referenced the renamed names; stale.yml's security label is untouched.

Verified: actionlint on triage.yml clean; the script body executed in node
against nine mocked events (PR by path, docs-only, tools-in-engine, engine core,
over-broad PR, future desktop/web paths, bot, issue with desktop+Windows, issue
with accessibility+mobile) and produced the expected labels. Not verified: a live
GitHub run (pull_request_target runs from the base branch only after this lands).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Sonnet 5.5 committed 2026-11-05 16:33:39 -08:00
1 parent e2d10895c6
commit dbb4facf55
2 files changed
+103 -29

No files matched your search

+83 -29
View File
@@ -1,11 +1,14 @@
name: Issue triage
name: Issue and PR triage
on:
issues:
types: [opened, reopened]
pull_request_target:
types: [opened, reopened, synchronize]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
@@ -13,44 +16,95 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Auto-label by title and body
- name: Auto-label by title, body and changed paths
uses: actions/github-script@v9
with:
script: |
const issue = context.payload.issue;
const pr = context.payload.pull_request;
const issue = context.payload.issue || pr;
const title = (issue.title || '').toLowerCase();
const body = (issue.body || '').toLowerCase();
const text = `${title}\n${body}`;
const labels = new Set();
// Type — matched against the TITLE only.
//
// These ran against title+body and mislabelled long, well-specified
// issues. Any body containing an acceptance criterion like "fails
// when a pack drifts" matched the bug rule, and a "How to add a
// locale" heading matched the question rule, so planned work landed
// tagged `bug, question`. A title states the type; a body just
// discusses it. `error`/`fail` are dropped entirely — they describe
// what almost every issue talks about, not what any issue *is*.
if (/\b(bug|crash|panic|broken|stack ?trace|regression)\b/.test(title)) labels.add('bug');
if (/\b(feat(?:ure)?|request|enhancement|wishlist|proposal|please add|would be nice|support for)\b/.test(title)) labels.add('enhancement');
if (/\b(docs?|readme|documentation|typo|grammar|wording|spelling)\b/.test(title)) labels.add('documentation');
if (/\b(question|how (?:do|to)|why does|what does|is it possible)\b/.test(title)) labels.add('question');
if (pr) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const pathRules = [
[/^crates\/(tui|tui-kit|palette)\//, 'area:tui'],
[/^crates\/(tools\/|engine\/src\/tools\/)/, 'area:tools'],
[/^crates\/(engine|core|runtime|agent|agent-contract|protocol)\/(?!src\/tools\/)/, 'area:core'],
[/^crates\/mcp\//, 'area:mcp'],
[/^crates\/(state|snapshot|memory)\//, 'area:state'],
[/^crates\/(sandbox|execpolicy)\//, 'area:sandbox'],
[/^crates\/(plugins|skills|skill-install|hooks)\//, 'area:plugins'],
[/^(crates\/(app-server|app-server-protocol)|npm\/runtime-sdk)\//, 'area:runtime-api'],
[/^crates\/models\//, 'area:providers'],
[/^(crates\/localization|docs\/zh_hans)\//, 'area:localization'],
[/^(npm\/(codewhale|deepseek-tui)|scripts\/release)\//, 'area:install'],
[/^(\.github|scripts\/ci)\//, 'area:ci'],
[/^apps\/desktop\//, 'area:desktop'],
[/^(apps\/web|web)\//, 'area:web'],
[/^apps\/mobile\//, 'area:mobile'],
];
const areas = new Set();
let docsOnly = files.length > 0;
for (const f of files) {
if (!/^(docs\/|[^/]+\.md$)/.test(f.filename)) docsOnly = false;
for (const [re, name] of pathRules) if (re.test(f.filename)) areas.add(name);
}
if (areas.size <= 4) areas.forEach(name => labels.add(name));
if (docsOnly) labels.add('documentation');
if (/^feat(\(|!|:)/.test(title)) labels.add('enhancement');
if (/^fix(\(|!|:)/.test(title)) labels.add('bug');
if (/^docs(\(|!|:)/.test(title)) labels.add('documentation');
if (/^(chore|refactor|style|build)(\(|!|:)/.test(title)) labels.add('cleanup');
if (/^perf(\(|!|:)/.test(title)) labels.add('performance');
if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(pr.author_association) && pr.user.type !== 'Bot') {
labels.add('contributor:external');
}
} else {
// Locale — title contains CJK (Chinese, Japanese, Korean) characters
if (/[぀-ヿ㐀-鿿가-힯]/.test(issue.title || '')) labels.add('lang:zh');
// Type — matched against the TITLE only.
//
// These ran against title+body and mislabelled long, well-specified
// issues. Any body containing an acceptance criterion like "fails
// when a pack drifts" matched the bug rule, and a "How to add a
// locale" heading matched the question rule, so planned work landed
// tagged `bug, question`. A title states the type; a body just
// discusses it. `error`/`fail` are dropped entirely — they describe
// what almost every issue talks about, not what any issue *is*.
if (/\b(bug|crash|panic|broken|stack ?trace|regression)\b/.test(title)) labels.add('bug');
if (/\b(feat(?:ure)?|request|enhancement|wishlist|proposal|please add|would be nice|support for)\b/.test(title)) labels.add('enhancement');
if (/\b(docs?|readme|documentation|typo|grammar|wording|spelling)\b/.test(title)) labels.add('documentation');
if (/\b(question|how (?:do|to)|why does|what does|is it possible)\b/.test(title)) labels.add('question');
// Areas (path-driven hints)
if (/crates\/tui|\btui\b|ratatui|composer|sidebar/.test(text)) labels.add('area:tui');
if (/crates\/core|engine|turn ?loop|agent ?loop/.test(text)) labels.add('area:core');
if (/crates\/mcp|\bmcp\b/.test(text)) labels.add('area:mcp');
if (/crates\/state|sqlite|sessions?|persistence/.test(text)) labels.add('area:state');
if (/crates\/execpolicy|approval|sandbox|seatbelt|landlock/.test(text)) labels.add('area:execpolicy');
if (/crates\/tools|tool[ _]call|tool[ _]registry/.test(text)) labels.add('area:tools');
if (/install|cargo install|npm install|scoop|homebrew|prebuilt|binary/.test(text)) labels.add('area:install');
if (/windows/.test(text)) labels.add('os:windows');
if (/macos|darwin|apple silicon/.test(text)) labels.add('os:macos');
if (/\blinux\b|ubuntu|debian|fedora|arch ?linux/.test(text)) labels.add('os:linux');
// Locale — title contains CJK (Chinese, Japanese, Korean) characters
if (/[぀-ヿ㐀-鿿가-힯]/.test(issue.title || '')) labels.add('lang:zh');
// Areas (path-driven hints)
if (/crates\/tui|\btui\b|ratatui|composer|sidebar/.test(text)) labels.add('area:tui');
if (/crates\/core|engine|turn ?loop|agent ?loop/.test(text)) labels.add('area:core');
if (/crates\/mcp|\bmcp\b/.test(text)) labels.add('area:mcp');
if (/crates\/state|sqlite|sessions?|persistence/.test(text)) labels.add('area:state');
if (/crates\/execpolicy|approval|sandbox|seatbelt|landlock/.test(text)) labels.add('area:sandbox');
if (/crates\/tools|tool[ _]call|tool[ _]registry/.test(text)) labels.add('area:tools');
if (/install|cargo install|npm install|scoop|homebrew|prebuilt|binary/.test(text)) labels.add('area:install');
if (/\bdesktop app\b|electron/.test(text)) labels.add('area:desktop');
if (/app\.codewhale\.net|\bweb app\b|codewhale web/.test(text)) labels.add('area:web');
if (/\bmobile\b|\bios\b|\bandroid\b/.test(text)) labels.add('area:mobile');
if (/screen ?reader|accessib|a11y|voiceover|\bnvda\b|keyboard navigation/.test(text)) labels.add('accessibility');
if (/windows/.test(text)) labels.add('os:windows');
if (/freebsd/.test(text)) labels.add('os:freebsd');
if (/\bohos\b|harmonyos/.test(text)) labels.add('os:ohos');
if (/macos|darwin|apple silicon/.test(text)) labels.add('os:macos');
if (/\blinux\b|ubuntu|debian|fedora|arch ?linux/.test(text)) labels.add('os:linux');
}
if (labels.size === 0) return;
+20
View File
@@ -382,6 +382,26 @@ because `main` moved while it was in review, say so and a maintainer resolves
it on your branch. If your branch is in a fork we cannot push to, we ask on the
PR first; harvesting is the last resort.
## Labels
Issues and pull requests are labelled by `.github/workflows/triage.yml`, which
only ever adds labels that already exist. Pull requests get their labels from
the paths they change and their conventional-commit title prefix; issues get
them from the title and form answers.
| Family | Meaning |
| --- | --- |
| `area:*` | The subsystem or client: `tui`, `core`, `tools`, `mcp`, `state`, `sandbox`, `plugins`, `providers`, `runtime-api`, `install`, `localization`, `ci`, `desktop`, `web`, `mobile`, `remote-control`, `subagents` |
| `os:*` | Platform-specific behaviour: `windows`, `macos`, `linux`, `freebsd`, `ohos` |
| `priority:p0` / `p1` / `p2` | Set by a maintainer; `release-blocker` still marks work that must land before a release |
| `bug`, `enhancement`, `documentation`, `question`, `cleanup`, `performance`, `security` | What kind of change it is |
| `accessibility` | Screen readers, keyboard use, contrast, motion |
| `contributor:external` | Opened by someone outside the maintainer team |
| `lang:zh` | Written in Chinese |
| `needs-triage`, `needs-info`, `agent-ready`, `agent-in-progress`, `needs-human`, `keep-open`, `pinned`, `stale` | Workflow state; other workflows read these names |
Release targeting uses milestones, not version labels.
## Contribution Gate
Codewhale uses a maintainer-managed contribution gate for the community front