diff --git a/.github/workflows/pr-size.yml b/.github/workflows/pr-size.yml new file mode 100644 index 0000000..1982c05 --- /dev/null +++ b/.github/workflows/pr-size.yml @@ -0,0 +1,103 @@ +name: PR Size Check and Label + +# Uses pull_request_target so it works on fork PRs (write access to labels). +# Safe because this workflow only reads PR metadata — it never checks out untrusted code. +on: + pull_request_target: + branches: [main] + +jobs: + label-size: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Calculate PR size and apply label + uses: actions/github-script@v8 + with: + script: | + const prNumber = context.payload.pull_request.number; + const { owner, repo } = context.repo; + + // Fetch file-level stats so we can exclude generated/lock files + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: prNumber, + per_page: 100 + }); + + const EXCLUDED = [ + /package-lock\.json$/, + /yarn\.lock$/, + /pnpm-lock\.yaml$/, + /\.snap$/ + ]; + + let additions = 0; + let deletions = 0; + for (const file of files) { + if (EXCLUDED.some(re => re.test(file.filename))) continue; + additions += file.additions; + deletions += file.deletions; + } + const totalChanges = additions + deletions; + + // Remove existing size labels (catch 404 in case of race) + const labels = await github.rest.issues.listLabelsOnIssue({ + owner, repo, issue_number: prNumber + }); + + for (const label of labels.data) { + if (label.name.startsWith('size/')) { + try { + await github.rest.issues.removeLabel({ + owner, repo, issue_number: prNumber, name: label.name + }); + } catch (e) { + if (e.status !== 404) throw e; + } + } + } + + // Determine size label + let sizeLabel; + if (totalChanges <= 20) sizeLabel = 'size/xs'; + else if (totalChanges <= 100) sizeLabel = 'size/s'; + else if (totalChanges <= 500) sizeLabel = 'size/m'; + else if (totalChanges <= 1000) sizeLabel = 'size/l'; + else sizeLabel = 'size/xl'; + + // Ensure the label exists before applying it + const LABEL_COLORS = { + 'size/xs': '3CBF00', + 'size/s': '5D9801', + 'size/m': 'FBCA04', + 'size/l': 'E67409', + 'size/xl': 'D93F0B' + }; + + try { + await github.rest.issues.getLabel({ owner, repo, name: sizeLabel }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner, repo, + name: sizeLabel, + color: LABEL_COLORS[sizeLabel], + description: `PR size: ${sizeLabel.split('/')[1].toUpperCase()}` + }); + } else { + throw e; + } + } + + await github.rest.issues.addLabels({ + owner, repo, issue_number: prNumber, labels: [sizeLabel] + }); + + core.info(`PR has ${totalChanges} changed lines (excluding lockfiles/snapshots) → ${sizeLabel}`); + + if (sizeLabel === 'size/xl') { + core.warning(`PR is large (${totalChanges} lines). Consider splitting into smaller PRs.`); + }