Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/PR_LABELS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Automatic PR labels

The `Label pull requests` workflow categorizes new and updated pull requests,
including contributions from forks. It becomes active when merged into the
default branch. Existing PRs are labeled on their next update or title edit.

- `area: ...` labels match changed paths in `.github/labeler.yml`. Multiple
areas can apply to one PR.
- `type: ...` labels match Conventional Commit prefixes in the PR title:
`fix`, `feat`, `docs`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`,
`style`, and `revert`. An optional scope is supported.
- For example, `fix(device): restore DPI settings` gets `type: bug`, and
`feat(ui)!: replace profile editor` gets `type: enhancement` and
`type: breaking`. Titles without a recognized prefix get no type label;
this workflow does not enforce a title format.

Automation creates missing labels and removes its own labels when they no
longer match. Reserve the configured `area:` and `type:` labels for automation;
other labels, including the standard `bug` and `enhancement` labels, are left
for manual triage. Existing label colors and descriptions are preserved.

The workflow uses `pull_request_target` with API access only. Do not add a PR
checkout or execute contributor-provided code in this workflow. `issues: write`
allows creation of repository labels, and `pull-requests: write` allows labeling
PRs. Both actions are pinned to commit SHAs.
21 changes: 21 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"area: native":
- changed-files:
- any-glob-to-any-file: ["src/**"]
"area: devices":
- changed-files:
- any-glob-to-any-file: ["src/hid.rs", "src/drivers/**", "native-hid/**"]
"area: overlay":
- changed-files:
- any-glob-to-any-file: ["src/overlay/**"]
"area: packaging":
- changed-files:
- any-glob-to-any-file: ["install-*.sh", "assets/**"]
"area: documentation":
- changed-files:
- any-glob-to-any-file: ["**/*.md", "**/*.mdx", "docs/**"]
"area: dependencies":
- changed-files:
- any-glob-to-any-file: ["**/package.json", "**/package-lock.json", "**/Cargo.toml", "**/Cargo.lock"]
"area: ci":
- changed-files:
- any-glob-to-any-file: [".github/**", "build/**", "scripts/**", "**/build.rs", "vite.config.*", "tsconfig*.json"]
93 changes: 93 additions & 0 deletions .github/workflows/label-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Label pull requests

on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]

permissions:
contents: read
pull-requests: write
issues: write

concurrency:
group: label-pr-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
label:
runs-on: ubuntu-latest
steps:
# Read metadata through the API; never check out or execute PR code.
- name: Ensure labels exist and categorize the current title
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const repo = context.repo;
const issue_number = context.payload.pull_request.number;
const areas = {
"native": "Native bridge and platform services",
"devices": "HID access and device drivers",
"overlay": "Desktop overlay",
"packaging": "Installation and application assets",
"documentation": "Documentation",
"dependencies": "Dependency manifests and lockfiles",
"ci": "Automation and build configuration",
};
const types = {
fix: 'bug', feat: 'enhancement', docs: 'documentation',
refactor: 'refactor', perf: 'performance', test: 'tests',
build: 'build', ci: 'ci', chore: 'maintenance',
style: 'style', revert: 'revert',
};
const definitions = [
...Object.entries(areas).map(([area, description]) => ({
name: `area: ${area}`, color: '1d76db', description,
})),
...Object.values(types).map(type => ({
name: `type: ${type}`, color: '5319e7',
description: `PR title category: ${type}`,
})),
{ name: 'type: breaking', color: 'd93f0b', description: 'PR title declares a breaking change with !' },
];
const existing = new Set((await github.paginate(
github.rest.issues.listLabelsForRepo, { ...repo, per_page: 100 }
)).map(label => label.name));
for (const definition of definitions) {
if (existing.has(definition.name)) continue;
try {
await github.rest.issues.createLabel({ ...repo, ...definition });
} catch (error) {
// Another PR's workflow may have created the same label.
if (error.status !== 422 || !error.response?.data?.errors?.some(
item => item.code === 'already_exists'
)) throw error;
}
}
// Fetch the latest title so queued events do not restore stale categories.
const { data: pr } = await github.rest.pulls.get({
...repo, pull_number: issue_number,
});
const match = pr.title.trim().match(
/^(fix|feat|docs|refactor|perf|test|build|ci|chore|style|revert)(?:\([^)\r\n]+\))?(!)?:\s+\S/i
);
const desired = new Set();
if (match) {
desired.add(`type: ${types[match[1].toLowerCase()]}`);
if (match[2]) desired.add('type: breaking');
}
const managed = new Set([
...Object.values(types).map(type => `type: ${type}`), 'type: breaking',
]);
for (const label of pr.labels) {
if (managed.has(label.name) && !desired.has(label.name)) {
await github.rest.issues.removeLabel({ ...repo, issue_number, name: label.name });
}
}
if (desired.size) {
await github.rest.issues.addLabels({ ...repo, issue_number, labels: [...desired] });
}

- name: Categorize changed files
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7
with:
sync-labels: true
Loading