mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-08 12:26:23 -04:00
129 lines
5.4 KiB
YAML
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(', ')}.`);
|
|
}
|
|
}
|