Files
Meshtastic-Android/.github/workflows/pull-request-target.yml
2026-07-05 09:35:54 -05:00

129 lines
5.4 KiB
YAML

name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, synchronize, reopened, labeled, unlabeled]
# Do not execute arbitrary code on this workflow.
# See warnings at https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-24.04-arm
steps:
- name: Auto-label PR
uses: actions/github-script@v9
with:
script: |
const branch = context.payload.pull_request.head.ref;
const title = context.payload.pull_request.title || '';
const labels = new Set();
// Match branch prefix (e.g. feat/..., fix/..., chore-something)
// Also parse conventional-commit-style PR titles as a fallback: type(scope): subject
const branchPrefix = branch.split(/[\/\-_]/)[0].toLowerCase();
const titleMatch = title.match(/^(\w+)(?:\(([^)]+)\))?:/);
const titlePrefix = titleMatch?.[1]?.toLowerCase();
const titleScope = titleMatch?.[2]?.toLowerCase();
// Map prefixes to changelog-aligned labels
const prefixMap = {
'feat': 'enhancement',
'feature': 'enhancement',
'fix': 'bugfix',
'bug': 'bugfix',
'bugfix': 'bugfix',
'hotfix': 'bugfix',
'refactor': 'refactor',
'chore': 'chore',
'build': 'build',
'ci': 'ci',
'test': 'testing',
'tests': 'testing',
'docs': 'documentation',
'doc': 'documentation',
'perf': 'enhancement',
'style': 'refactor',
'repo': 'repo',
'release': 'release',
};
// Map conventional-commit scopes to labels not derivable from the type alone
const scopeMap = {
'desktop': 'desktop',
};
// Label from branch prefix
if (prefixMap[branchPrefix]) labels.add(prefixMap[branchPrefix]);
// Label from PR title prefix (if different from branch)
if (titlePrefix && prefixMap[titlePrefix] && !labels.has(prefixMap[titlePrefix])) {
labels.add(prefixMap[titlePrefix]);
}
// Label from PR title scope, e.g. "feat(desktop): ..."
if (titleScope && scopeMap[titleScope]) labels.add(scopeMap[titleScope]);
// Path-based labels from changed files
const pathLabels = [
{ prefix: '.github/', label: 'repo' },
{ prefix: 'build-logic/', label: 'build' },
{ prefix: 'desktopApp/', label: 'desktop' },
].filter(({ label }) => !labels.has(label));
if (pathLabels.length > 0) {
try {
const files = await github.paginate(
github.rest.pulls.listFiles,
{ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.payload.pull_request.number, per_page: 100 },
(res) => res.data.map(f => f.filename)
);
for (const { prefix, label } of pathLabels) {
if (files.some(f => f.startsWith(prefix))) labels.add(label);
}
} catch (e) {
core.warning(`Could not list PR files (rate limited?): ${e.message}`);
}
}
if (labels.size > 0) {
const labelArray = [...labels];
core.info(`Applying labels: ${labelArray.join(', ')}`);
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: labelArray,
});
} catch (e) {
core.warning(`Could not apply labels (rate limited?): ${e.message}`);
}
} else {
core.info('No labels matched for this PR.');
}
// Enforce that the PR ends up with a required label. Done here (not in a
// separate pull_request workflow) so the check sees labels this job just
// applied: label events fired by GITHUB_TOKEN don't re-trigger workflows,
// so a separate checker would read an empty label set at `opened` and stay
// red forever. Bot PRs are labeled by their own flows and are skipped.
const author = context.payload.pull_request.user.login;
const headRef = context.payload.pull_request.head.ref;
const skipAuthors = ['renovate[bot]', 'github-actions[bot]', 'dependabot[bot]'];
const skipRefs = ['scheduled-updates', 'l10n_main'];
if (!skipAuthors.includes(author) && !skipRefs.includes(headRef)) {
const requiredLabels = ['bugfix', 'enhancement', 'automation', 'dependencies', 'repo', 'release', 'refactor', 'desktop', 'chore', 'ci', 'build', 'testing', 'documentation'];
const effectiveLabels = new Set([
...context.payload.pull_request.labels.map(l => l.name),
...labels,
]);
if (!requiredLabels.some(l => effectiveLabels.has(l))) {
core.setFailed(`PR must have at least one of the following labels before it can be merged: ${requiredLabels.join(', ')}.`);
}
}