chore: Scheduled updates (Firmware, Hardware, Translations) #7754
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(', ')}.`); | |
| } | |
| } |