From ede48f8d7ed601afcf55bc26745726a467b47b7d Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 13:34:31 -0700 Subject: [PATCH 1/7] ci: unpin durable functions emulator image tag --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfae5e813f..ccc2279c90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -197,7 +197,6 @@ jobs: run: pytest -vv ${{ matrix.tests_config.params }} env: FORCE_RUN_DOCKER_TEST: ${{ matrix.tests_config.name == 'durable-functions' && '1' || '' }} - DURABLE_EXECUTIONS_EMULATOR_IMAGE_TAG: ${{ matrix.tests_config.name == 'durable-functions' && 'v1.1.1' || '' }} smoke-and-functional-tests: name: ${{ matrix.tests_config.name }} / ${{ matrix.tests_config.os }} / ${{ matrix.python }} From 6eff6d38fc20b5626daaa15df53a0c2da20007c7 Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 15:37:06 -0700 Subject: [PATCH 2/7] ci: add PR size labeler workflow skeleton --- .github/workflows/pr-size-labeler.yml | 60 +++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/pr-size-labeler.yml diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml new file mode 100644 index 0000000000..2b5d47babc --- /dev/null +++ b/.github/workflows/pr-size-labeler.yml @@ -0,0 +1,60 @@ +name: "PR Size Labeler" + +on: + pull_request_target: + types: [opened, synchronize, reopened, labeled, unlabeled] + +# Cancel in-flight runs on rapid pushes; the latest push is what matters. +concurrency: + group: pr-size-labeler-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + size-label: + name: Size label + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - name: Compute size, label, and gate + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // ---- Buckets (sorted ascending by lower bound). ---- + const BUCKETS = [ + { label: 'size/XS', min: 0, max: 9 }, + { label: 'size/S', min: 10, max: 29 }, + { label: 'size/M', min: 30, max: 99 }, + { label: 'size/L', min: 100, max: 499 }, + { label: 'size/XL', min: 500, max: 999 }, + { label: 'size/XXL', min: 1000, max: Infinity }, + ]; + const SIZE_LABELS = BUCKETS.map(b => b.label); + const OVERRIDE_LABEL = 'size/override'; + const STICKY_MARKER = ''; + + // ---- Exclusion globs. Files matching ANY glob are excluded + // from the size count. Edit this list to tune. ---- + const EXCLUDE_GLOBS = [ + '**/*.lock', + '**/package-lock.json', + '**/poetry.lock', + '**/yarn.lock', + '**/Pipfile.lock', + '**/uv.lock', + '**/requirements*.txt', + 'schema/**', + '**/__snapshots__/**', + '**/*.snap', + '**/*.svg', + '**/*.png', + '**/*.jpg', + '**/*.jpeg', + '**/*.gif', + '**/*.ico', + '**/*.pdf', + ]; + + core.info(`Configured ${BUCKETS.length} buckets, ${EXCLUDE_GLOBS.length} exclusion globs.`); From 67222c2edef3ce9aac8e1b7ad40456f591f2c675 Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 15:42:00 -0700 Subject: [PATCH 3/7] ci: read PR files and compute size bucket --- .github/workflows/pr-size-labeler.yml | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index 2b5d47babc..c205513af7 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -58,3 +58,47 @@ jobs: ]; core.info(`Configured ${BUCKETS.length} buckets, ${EXCLUDE_GLOBS.length} exclusion globs.`); + + const minimatch = require('minimatch'); + const { owner, repo } = context.repo; + const pull_number = context.payload.pull_request.number; + + // True if `filename` matches ANY exclusion glob. + function shouldExclude(filename) { + return EXCLUDE_GLOBS.some(g => minimatch(filename, g, { dot: true })); + } + + // Pick the first bucket whose [min, max] contains `n`. + function pickBucket(n) { + return BUCKETS.find(b => n >= b.min && n <= b.max); + } + + // ---- Fetch all changed files (paginated, capped at 3000 by GitHub). ---- + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number, + per_page: 100, + }); + + const truncated = files.length === 3000; + if (truncated) { + core.warning('Pull-request files response hit GitHub\'s 3000-file cap; counting partial sum. The PR is XXL regardless.'); + } + + let total = 0; + let excludedFiles = 0; + for (const f of files) { + if (shouldExclude(f.filename)) { + excludedFiles += 1; + continue; + } + total += (f.additions || 0) + (f.deletions || 0); + } + + const bucket = pickBucket(total); + core.info(`PR #${pull_number}: ${total} changed lines (excluded ${excludedFiles}/${files.length} files) -> ${bucket.label}`); + await core.summary + .addHeading('PR size') + .addRaw(`**${bucket.label}** — ${total} changed lines (excluded ${excludedFiles}/${files.length} files)${truncated ? ' _(file list was truncated)_' : ''}`) + .write(); From 061dc1170737ad70a6147b155a46c9a85ca3d3f8 Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 15:45:36 -0700 Subject: [PATCH 4/7] ci: apply size label and remove stale size labels --- .github/workflows/pr-size-labeler.yml | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index c205513af7..aa38176170 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -102,3 +102,33 @@ jobs: .addHeading('PR size') .addRaw(`**${bucket.label}** — ${total} changed lines (excluded ${excludedFiles}/${files.length} files)${truncated ? ' _(file list was truncated)_' : ''}`) .write(); + + // ---- Apply the chosen size label; remove stale size labels. ---- + const currentLabels = (context.payload.pull_request.labels || []).map(l => l.name); + const stale = currentLabels.filter(n => SIZE_LABELS.includes(n) && n !== bucket.label); + const alreadyApplied = currentLabels.includes(bucket.label); + + if (!alreadyApplied) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pull_number, + labels: [bucket.label], + }); + core.info(`Applied label ${bucket.label}`); + } + + for (const name of stale) { + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pull_number, + name, + }); + core.info(`Removed stale label ${name}`); + } catch (err) { + // 404 = label was already removed by another run. Ignore. + if (err.status !== 404) throw err; + } + } From 1b48933a35b1599ca6bfb68916db60129ecf6b4f Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 15:47:29 -0700 Subject: [PATCH 5/7] ci: add XXL gate with sticky comment and override label --- .github/workflows/pr-size-labeler.yml | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index aa38176170..64f555f683 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -132,3 +132,55 @@ jobs: if (err.status !== 404) throw err; } } + + // ---- Sticky comment helper. Identifies our comment by the + // HTML marker on its first line. ---- + async function upsertStickyComment(body) { + const fullBody = `${STICKY_MARKER}\n${body}`; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pull_number, + per_page: 100, + }); + const existing = comments.find(c => c.body && c.body.startsWith(STICKY_MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body: fullBody, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pull_number, + body: fullBody, + }); + } + } + + // ---- XXL gate. ---- + if (bucket.label === 'size/XXL') { + const overrideActive = currentLabels.includes(OVERRIDE_LABEL); + const overrideParagraph = overrideActive + ? `A maintainer applied \`${OVERRIDE_LABEL}\`; this PR is allowed despite its size. Remove the label to re-arm the gate.` + : `A maintainer can apply the \`${OVERRIDE_LABEL}\` label to bypass this check.`; + const body = [ + `**PR size: \`XXL\` (${total} changed lines)**`, + ``, + `This PR exceeds the 1,000-line threshold. Smaller PRs get reviewed faster and are easier to revert.`, + ``, + overrideParagraph, + ``, + `_Excluded from the count: lockfiles, schema, snapshots, images. See \`.github/workflows/pr-size-labeler.yml\` to adjust._`, + ].join('\n'); + await upsertStickyComment(body); + + if (!overrideActive) { + core.setFailed( + `PR has ${total} changed lines (>= 1000). Apply the \`${OVERRIDE_LABEL}\` label to bypass.`, + ); + } + } From dffd5dee15218210c94f4d53ce2d492cc022e1c7 Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 15:50:55 -0700 Subject: [PATCH 6/7] ci: tolerate transient API failures without dropping the XXL gate --- .github/workflows/pr-size-labeler.yml | 140 +++++++++++++------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index 64f555f683..548292236c 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -35,8 +35,6 @@ jobs: const OVERRIDE_LABEL = 'size/override'; const STICKY_MARKER = ''; - // ---- Exclusion globs. Files matching ANY glob are excluded - // from the size count. Edit this list to tune. ---- const EXCLUDE_GLOBS = [ '**/*.lock', '**/package-lock.json', @@ -57,84 +55,17 @@ jobs: '**/*.pdf', ]; - core.info(`Configured ${BUCKETS.length} buckets, ${EXCLUDE_GLOBS.length} exclusion globs.`); - const minimatch = require('minimatch'); const { owner, repo } = context.repo; const pull_number = context.payload.pull_request.number; - // True if `filename` matches ANY exclusion glob. function shouldExclude(filename) { return EXCLUDE_GLOBS.some(g => minimatch(filename, g, { dot: true })); } - - // Pick the first bucket whose [min, max] contains `n`. function pickBucket(n) { return BUCKETS.find(b => n >= b.min && n <= b.max); } - // ---- Fetch all changed files (paginated, capped at 3000 by GitHub). ---- - const files = await github.paginate(github.rest.pulls.listFiles, { - owner, - repo, - pull_number, - per_page: 100, - }); - - const truncated = files.length === 3000; - if (truncated) { - core.warning('Pull-request files response hit GitHub\'s 3000-file cap; counting partial sum. The PR is XXL regardless.'); - } - - let total = 0; - let excludedFiles = 0; - for (const f of files) { - if (shouldExclude(f.filename)) { - excludedFiles += 1; - continue; - } - total += (f.additions || 0) + (f.deletions || 0); - } - - const bucket = pickBucket(total); - core.info(`PR #${pull_number}: ${total} changed lines (excluded ${excludedFiles}/${files.length} files) -> ${bucket.label}`); - await core.summary - .addHeading('PR size') - .addRaw(`**${bucket.label}** — ${total} changed lines (excluded ${excludedFiles}/${files.length} files)${truncated ? ' _(file list was truncated)_' : ''}`) - .write(); - - // ---- Apply the chosen size label; remove stale size labels. ---- - const currentLabels = (context.payload.pull_request.labels || []).map(l => l.name); - const stale = currentLabels.filter(n => SIZE_LABELS.includes(n) && n !== bucket.label); - const alreadyApplied = currentLabels.includes(bucket.label); - - if (!alreadyApplied) { - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: pull_number, - labels: [bucket.label], - }); - core.info(`Applied label ${bucket.label}`); - } - - for (const name of stale) { - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pull_number, - name, - }); - core.info(`Removed stale label ${name}`); - } catch (err) { - // 404 = label was already removed by another run. Ignore. - if (err.status !== 404) throw err; - } - } - - // ---- Sticky comment helper. Identifies our comment by the - // HTML marker on its first line. ---- async function upsertStickyComment(body) { const fullBody = `${STICKY_MARKER}\n${body}`; const comments = await github.paginate(github.rest.issues.listComments, { @@ -161,7 +92,76 @@ jobs: } } - // ---- XXL gate. ---- + let bucket; + let total = 0; + let currentLabels = []; + + try { + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number, + per_page: 100, + }); + const truncated = files.length === 3000; + if (truncated) { + core.warning('Pull-request files response hit GitHub\'s 3000-file cap; counting partial sum. The PR is XXL regardless.'); + } + + let excludedFiles = 0; + for (const f of files) { + if (shouldExclude(f.filename)) { + excludedFiles += 1; + continue; + } + total += (f.additions || 0) + (f.deletions || 0); + } + + bucket = pickBucket(total); + core.info(`PR #${pull_number}: ${total} changed lines (excluded ${excludedFiles}/${files.length} files) -> ${bucket.label}`); + await core.summary + .addHeading('PR size') + .addRaw(`**${bucket.label}** — ${total} changed lines (excluded ${excludedFiles}/${files.length} files)${truncated ? ' _(file list was truncated)_' : ''}`) + .write(); + + currentLabels = (context.payload.pull_request.labels || []).map(l => l.name); + const stale = currentLabels.filter(n => SIZE_LABELS.includes(n) && n !== bucket.label); + const alreadyApplied = currentLabels.includes(bucket.label); + + if (!alreadyApplied) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pull_number, + labels: [bucket.label], + }); + core.info(`Applied label ${bucket.label}`); + } + for (const name of stale) { + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pull_number, + name, + }); + core.info(`Removed stale label ${name}`); + } catch (err) { + if (err.status !== 404) throw err; + } + } + } catch (err) { + // Sizing infra should not block PRs when GitHub's API is flaky. + // The intentional XXL-without-override failure happens AFTER this + // catch and is therefore unaffected. + core.warning(`PR size labeler: non-fatal error during sizing/labeling: ${err.message}`); + await core.summary + .addRaw(`PR size labeler hit a non-fatal error: \`${err.message}\``) + .write(); + return; // Skip the gate when we can't trust `bucket` / `currentLabels`. + } + + // ---- XXL gate (outside try/catch). ---- if (bucket.label === 'size/XXL') { const overrideActive = currentLabels.includes(OVERRIDE_LABEL); const overrideParagraph = overrideActive From 294f08e2e4320bfd65d9b76df650bbc83c38f85a Mon Sep 17 00:00:00 2001 From: Harold Sun Date: Mon, 1 Jun 2026 17:09:25 -0700 Subject: [PATCH 7/7] ci: document one-time label setup for PR size labeler --- .github/workflows/pr-size-labeler.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/pr-size-labeler.yml b/.github/workflows/pr-size-labeler.yml index 548292236c..25563e6871 100644 --- a/.github/workflows/pr-size-labeler.yml +++ b/.github/workflows/pr-size-labeler.yml @@ -1,3 +1,20 @@ +# PR Size Labeler +# +# Labels every PR with one of size/XS, size/S, size/M, size/L, size/XL, size/XXL +# based on additions+deletions across non-excluded files, and fails the check on +# size/XXL unless a maintainer applies size/override. +# +# ONE-TIME SETUP (a maintainer must run these once after this workflow merges): +# gh label create size/XS --color ededed --description "PR size: 0-9 changed lines" +# gh label create size/S --color c2e0c6 --description "PR size: 10-29 changed lines" +# gh label create size/M --color fef2c0 --description "PR size: 30-99 changed lines" +# gh label create size/L --color fbca04 --description "PR size: 100-499 changed lines" +# gh label create size/XL --color e99695 --description "PR size: 500-999 changed lines" +# gh label create size/XXL --color b60205 --description "PR size: 1000+ changed lines" +# gh label create size/override --color 0e8a16 --description "Maintainer override for size/XXL gate" +# +# Spec: docs/superpowers/specs/2026-06-01-pr-size-labeler-design.md + name: "PR Size Labeler" on: @@ -35,6 +52,8 @@ jobs: const OVERRIDE_LABEL = 'size/override'; const STICKY_MARKER = ''; + // ---- Exclusion globs. Files matching ANY glob are excluded + // from the size count. Edit this list to tune. ---- const EXCLUDE_GLOBS = [ '**/*.lock', '**/package-lock.json', @@ -147,6 +166,7 @@ jobs: }); core.info(`Removed stale label ${name}`); } catch (err) { + // 404 = label was already removed by another run. Ignore. if (err.status !== 404) throw err; } }