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(', ')}.`);
            }
          }
