## 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
110 lines
3.6 KiB
YAML
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}
|