Files
zed/.github/workflows/pr-size-check.yml
T
John D. Swanson d344d7f1c5 Split size check into compute + label workflows for fork PR support (#51972)
## Context

Fork PRs receive a read-only `GITHUB_TOKEN`, causing 403 errors on all
label and comment writes
([example](https://github.com/zed-industries/zed/pull/51878)). This
splits the single workflow into two:

- **pr-size-check.yml** (`pull_request`): computes size and guided tour
detection, uploads a JSON artifact — read-only, works for forks
- **pr-size-label.yml** (`workflow_run`): downloads the artifact,
applies labels and comments — runs on the base repo with full write
access

### Security

- Artifact treated as untrusted data: fields are cast and validated
(`Number()`, `String()` + prefix check, `Boolean()`) before use
- No artifact content is executed or interpolated into shell
- Missing artifact handled gracefully (steps skip via output flag)

### Also included

- Structural guided tour detection: extracts "How to Review" section,
strips HTML comment placeholders, checks for actual author content
(fixes false positive in #51957)
- Softer confirmation: "appears to include guidance"

Tested locally end-to-end against 4 real PRs
(XL/small/medium/false-positive).

## How to Review

1. `pr-size-check.yml` — the compute half. Compare against the previous
version: all write operations removed, artifact upload added at the end.
2. `pr-size-label.yml` — new file. Download artifact, validate, apply
labels/comments. Same label logic as before, just in a `workflow_run`
context.

## 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
2026-03-19 16:39:28 -04:00

110 lines
3.6 KiB
YAML

# PR Size Check — Compute
#
# Calculates PR size and saves the result as an artifact. A companion
# workflow (pr-size-label.yml) picks up the artifact via workflow_run
# and applies labels + comments with write permissions.
#
# This two-workflow split is required because fork PRs receive a
# read-only GITHUB_TOKEN. The compute step needs no write access;
# the label/comment step runs via workflow_run on the base repo with
# full write permissions.
#
# Security note: This workflow only reads PR file data via the JS API
# and writes a JSON artifact. No untrusted input is interpolated into
# shell commands.
name: PR Size Check
on:
pull_request:
types: [opened, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
compute-size:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Calculate PR size
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const fs = require('fs');
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
per_page: 300,
});
// Sum additions + deletions, excluding generated/lock files
const IGNORED_PATTERNS = [
/\.lock$/,
/^Cargo\.lock$/,
/pnpm-lock\.yaml$/,
/\.generated\./,
/\/fixtures\//,
/\/snapshots\//,
];
let totalChanges = 0;
for (const file of files) {
const ignored = IGNORED_PATTERNS.some(p => p.test(file.filename));
if (!ignored) {
totalChanges += file.additions + file.deletions;
}
}
// Assign size bracket
const SIZE_BRACKETS = [
['Size S', 0, 100, '0e8a16'],
['Size M', 100, 400, 'fbca04'],
['Size L', 400, 800, 'e99695'],
['Size XL', 800, Infinity, 'b60205'],
];
let sizeLabel = 'Size S';
let labelColor = '0e8a16';
for (const [label, min, max, color] of SIZE_BRACKETS) {
if (totalChanges >= min && totalChanges < max) {
sizeLabel = label;
labelColor = color;
break;
}
}
// Check if the author wrote content in the "How to Review" section.
const rawBody = context.payload.pull_request.body || '';
const howToReview = rawBody.match(/## How to Review\s*\n([\s\S]*?)(?=\n## |$)/i);
const hasReviewGuidance = howToReview
? howToReview[1].replace(/<!--[\s\S]*?-->/g, '').trim().length > 0
: false;
const result = {
pr_number: context.issue.number,
total_changes: totalChanges,
size_label: sizeLabel,
label_color: labelColor,
has_review_guidance: hasReviewGuidance,
};
console.log(`PR #${result.pr_number}: ${totalChanges} LOC, ${sizeLabel}`);
fs.mkdirSync('pr-size', { recursive: true });
fs.writeFileSync('pr-size/result.json', JSON.stringify(result));
- name: Upload size result
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-size-result
path: pr-size/
retention-days: 1
defaults:
run:
shell: bash -euxo pipefail {0}