mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 02:26:02 -10:00
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:
1 parent
e2d10895c6
commit
dbb4facf55
2 files changed
+103
-29
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user