From a7c4907af99619af7f1c2f2d5bed84688b30a388 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Wed, 1 Jul 2026 14:19:46 -0700 Subject: [PATCH 1/4] chore(ci): no more pull_request_target --- .github/workflows/build.yml | 70 ++++++++++++++++++++++ .github/workflows/bundle-compare.yml | 77 ------------------------ .github/workflows/chromatic.yml | 9 +-- .github/workflows/leave-comment.yml | 89 ++++++++++++++++++++++++++++ .github/workflows/lighthouse.yml | 40 ++++++------- 5 files changed, 179 insertions(+), 106 deletions(-) delete mode 100644 .github/workflows/bundle-compare.yml create mode 100644 .github/workflows/leave-comment.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89d8d81a9ca0c..aadc572b76f6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,3 +85,73 @@ jobs: # We want to ensure that static exports for all locales do not occur on `pull_request` events # TODO: The output of this is too large, and it crashes the GitHub Runner NEXT_PUBLIC_STATIC_EXPORT_LOCALE: false # ${{ github.event_name == 'push' }} + + compare-bundle-size: + name: Compare Bundle Size + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'pull_request' + + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Git Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Download Stats (HEAD) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: webpack-stats + path: head-stats + + - name: Get Run ID from BASE + id: base-run + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + GH_TOKEN: ${{ github.token }} + run: | + ID=$(gh run list -c "$BASE_SHA" -w build.yml -s success -L 1 --json databaseId --jq ".[].databaseId") + echo "run_id=$ID" >> "$GITHUB_OUTPUT" + + - name: Download Stats (BASE) + id: base-stats + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: webpack-stats + path: base-stats + run-id: ${{ steps.base-run.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Compare Bundle Size + id: compare-bundle-size + if: steps.base-stats.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + HEAD_STATS_PATH: ./head-stats/webpack-stats.json + BASE_STATS_PATH: ./base-stats/webpack-stats.json + with: + script: | + const { compare } = await import('${{github.workspace}}/apps/site/scripts/compare-size/index.mjs') + await compare({core}) + + - name: Prepare Comment + if: steps.base-stats.outcome == 'success' + env: + COMMENT: ${{ steps.compare-bundle-size.outputs.comment }} + run: | + mkdir -p pr-comment + printf '%s' "$COMMENT" > pr-comment/comment.md + printf '%s' 'compare_bundle_size' > pr-comment/tag.txt + + - name: Upload Comment + if: steps.base-stats.outcome == 'success' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-comment + path: pr-comment/ diff --git a/.github/workflows/bundle-compare.yml b/.github/workflows/bundle-compare.yml deleted file mode 100644 index d728c022cf30a..0000000000000 --- a/.github/workflows/bundle-compare.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Compare Bundle Size - -on: - workflow_run: - workflows: ['Build'] - types: [completed] - -permissions: - contents: read - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event.workflow_run.id }} - cancel-in-progress: true - -jobs: - compare: - name: Compare Bundle Stats - runs-on: ubuntu-latest - permissions: - # Required to comment on pull requests - pull-requests: write - if: github.event.workflow_run.event == 'pull_request' - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Git Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Download Stats (HEAD) - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: webpack-stats - path: head-stats - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Get Run ID from BASE - id: base-run - env: - WORKFLOW_ID: ${{ github.event.workflow_run.workflow_id }} - GH_TOKEN: ${{ github.token }} - run: | - ID=$(gh run list -c $GITHUB_SHA -w $WORKFLOW_ID -L 1 --json databaseId --jq ".[].databaseId") - echo "run_id=$ID" >> $GITHUB_OUTPUT - - - name: Download Stats (BASE) - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: webpack-stats - path: base-stats - run-id: ${{ steps.base-run.outputs.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Compare Bundle Size - id: compare-bundle-size - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - HEAD_STATS_PATH: ./head-stats/webpack-stats.json - BASE_STATS_PATH: ./base-stats/webpack-stats.json - with: - script: | - const { compare } = await import('${{github.workspace}}/apps/site/scripts/compare-size/index.mjs') - await compare({core}) - - - name: Add Comment to PR - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 - with: - comment-tag: 'compare_bundle_size' - message: ${{ steps.compare-bundle-size.outputs.comment }} - pr-number: ${{ github.event.workflow_run.pull_requests[0].number }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 4092c4897d981..6283c73aceef4 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -14,14 +14,12 @@ on: paths: - packages/ui-components/** - .github/workflows/chromatic.yml - pull_request_target: + pull_request: branches: - main paths: - packages/ui-components/** - .github/workflows/chromatic.yml - types: - - labeled workflow_dispatch: defaults: @@ -42,9 +40,8 @@ jobs: # We only need to run Storybook Builds and Storybook Visual Regression Tests within Pull Requests that actually # introduce changes to the Storybook. Hence, we skip running these on Crowdin PRs and Dependabot PRs if: | - github.event_name != 'pull_request_target' || + github.event_name != 'pull_request' || ( - github.event.label.name == 'github_actions:pull-request' && github.actor != 'dependabot[bot]' && github.event.pull_request.head.ref != 'chore/crowdin' ) @@ -70,6 +67,6 @@ jobs: with: workingDir: packages/ui-components buildScriptName: storybook:build - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + projectToken: ${{ vars.CHROMATIC_PROJECT_TOKEN }} exitOnceUploaded: true onlyChanged: true diff --git a/.github/workflows/leave-comment.yml b/.github/workflows/leave-comment.yml new file mode 100644 index 0000000000000..0b054af4d6983 --- /dev/null +++ b/.github/workflows/leave-comment.yml @@ -0,0 +1,89 @@ +name: Leave Comment + +on: + workflow_run: + # Any Workflow that uploads a `pr-comment` artifact should be listed here + workflows: ['Build', 'Lighthouse'] + types: [completed] + +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.id }} + cancel-in-progress: true + +jobs: + leave-comment: + name: Leave Comment + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Download Comment Artifact + # The Workflow may not have produced a comment (e.g. the comparison was skipped), so this is + # allowed to fail and every subsequent step is gated on it having succeeded. + id: download + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-comment + path: pr-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve Pull Request Number + id: pr + if: steps.download.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const run = context.payload.workflow_run; + + // 1. For same-repo Pull Requests the run is already linked to its PR(s). + if (run.pull_requests && run.pull_requests.length) { + core.setOutput('number', run.pull_requests[0].number); + return; + } + + // 2. For forks that list is empty, so find the open Pull Request whose HEAD SHA matches + // the commit that triggered the run. + const pulls = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + const match = pulls.find(pull => pull.head.sha === run.head_sha); + + if (!match) { + core.info(`No open pull request found for HEAD ${run.head_sha}`); + return; + } + + core.setOutput('number', match.number); + + - name: Read Comment Tag + id: meta + if: steps.download.outcome == 'success' + run: | + tag="$(tr -cd 'A-Za-z0-9_-' < pr-comment/tag.txt)" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Add Comment to PR + # The comment body is untrusted markdown, so it is passed as a file (data) rather than + # interpolated into an expression or shell command. + if: steps.download.outcome == 'success' && steps.pr.outputs.number != '' + uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + with: + file-path: pr-comment/comment.md + comment-tag: ${{ steps.meta.outputs.tag }} + pr-number: ${{ steps.pr.outputs.number }} diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index e7b0ad2b713b0..2eeca1e51227a 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -1,5 +1,7 @@ # Security Notes -# This workflow uses `pull_request_target`, so will run against all PRs automatically (without approval), be careful with allowing any user-provided code to be run here +# This Workflow runs in the untrusted `pull_request` context and therefore must not rely on any +# repository secrets. It does not comment on the Pull Request itself; instead it uploads a +# `pr-comment` artifact which the trusted `Leave Comment` Workflow posts once this Workflow completes. # Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) # for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. # REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! @@ -9,7 +11,7 @@ name: Lighthouse on: - pull_request_target: + pull_request: branches: - main types: @@ -36,9 +38,6 @@ jobs: github.event.label.name == 'github_actions:pull-request' name: Lighthouse Report runs-on: ubuntu-latest - permissions: - # Required by `thollander/actions-comment-pull-request` - pull-requests: write steps: - name: Harden Runner @@ -55,21 +54,11 @@ jobs: check_interval: 10 # check every 10 seconds - name: Git Checkout + # Only needed for the Lighthouse formatting script; no credentials are persisted. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - # Provides the Pull Request commit SHA or the GitHub merge group ref - ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.ref }} persist-credentials: false - - name: Add Comment to PR - # Signal that a lighthouse run is about to start - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 - with: - message: | - Running Lighthouse audit... - # Used later to edit the existing comment - comment-tag: 'lighthouse_audit' - - name: Audit Preview URL with Lighthouse # Conduct the lighthouse audit id: lighthouse_audit @@ -105,11 +94,16 @@ jobs: const { formatLighthouseResults } = await import('${{github.workspace}}/apps/site/scripts/lighthouse/index.mjs') await formatLighthouseResults({core}) - - name: Add Comment to PR - # Replace the previous message with our formatted lighthouse results - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + - name: Prepare Comment + env: + COMMENT: ${{ steps.format_lighthouse_score.outputs.comment }} + run: | + mkdir -p pr-comment + printf '%s' "$COMMENT" > pr-comment/comment.md + printf '%s' 'lighthouse_audit' > pr-comment/tag.txt + + - name: Upload Comment + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - # Reference the previously created comment - comment-tag: 'lighthouse_audit' - message: | - ${{ steps.format_lighthouse_score.outputs.comment }} + name: pr-comment + path: pr-comment/ From c075acf77d02ed573317421090d55ba49d84e0e5 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 2 Jul 2026 21:22:23 -0700 Subject: [PATCH 2/4] fixup! --- .github/workflows/chromatic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 6283c73aceef4..c69aaea298b91 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -63,7 +63,7 @@ jobs: - name: Start Visual Regression Tests (Chromatic) # This assigns the Environment Deployment for Storybook id: chromatic-deploy - uses: chromaui/action@8ad69a40dea06755a3c6db290f300a39e011433b # v17.1.0 + uses: chromaui/action@94713c544284a14195de3b50ef24301579f1877e # v18.0.1 with: workingDir: packages/ui-components buildScriptName: storybook:build From 27963747180b3461a85ba118ab44037f0e980de0 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Sat, 4 Jul 2026 13:05:24 -0700 Subject: [PATCH 3/4] fixup! --- .github/workflows/build.yml | 8 +-- apps/site/scripts/compare-size/index.mjs | 91 ++++++++++++++---------- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 875d4782800fb..f463ad5618e09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,14 +65,14 @@ jobs: # this should be a last resort in case by any chances the build memory gets too high # but in general this should never happen NODE_OPTIONS: '--max_old_space_size=4096' - # See https://github.com/vercel/next.js/pull/81318 + # See https://github.com/vercel/next.js/pull/90949 TURBOPACK_STATS: ${{ matrix.os == 'ubuntu-latest' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: matrix.os == 'ubuntu-latest' with: name: webpack-stats - path: apps/site/.next/server/webpack-stats.json + path: apps/site.next/diagnostics/route-bundle-stats.json - name: Build Next.js (Static Export) # We want to generate a static build, as it is a requirement of our website. @@ -133,8 +133,8 @@ jobs: if: steps.base-stats.outcome == 'success' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - HEAD_STATS_PATH: ./head-stats/webpack-stats.json - BASE_STATS_PATH: ./base-stats/webpack-stats.json + HEAD_STATS_PATH: ./head-stats/route-bundle-stats.json + BASE_STATS_PATH: ./base-stats/route-bundle-stats.json with: script: | const { compare } = await import('${{github.workspace}}/apps/site/scripts/compare-size/index.mjs') diff --git a/apps/site/scripts/compare-size/index.mjs b/apps/site/scripts/compare-size/index.mjs index 1b6675e1f7dbd..b8663bde4ca20 100644 --- a/apps/site/scripts/compare-size/index.mjs +++ b/apps/site/scripts/compare-size/index.mjs @@ -26,30 +26,41 @@ const formatPercent = (oldValue, newValue) => { }; /** - * Categorizes asset changes + * Categorizes route bundle size changes. */ -const categorizeChanges = (oldAssets, newAssets) => { - const oldMap = new Map(oldAssets.map(a => [a.name, a])); - const newMap = new Map(newAssets.map(a => [a.name, a])); +const categorizeChanges = (oldRoutes, newRoutes) => { + const oldMap = new Map(oldRoutes.map(r => [r.route, r])); + const newMap = new Map(newRoutes.map(r => [r.route, r])); const changes = { added: [], removed: [], modified: [] }; - for (const [name, oldAsset] of oldMap) { - const newAsset = newMap.get(name); - if (!newAsset) { - changes.removed.push({ name, size: oldAsset.size }); - } else if (oldAsset.size !== newAsset.size) { + for (const [route, oldRoute] of oldMap) { + const newRoute = newMap.get(route); + if (!newRoute) { + changes.removed.push({ + name: route, + size: oldRoute.firstLoadUncompressedJsBytes, + }); + } else if ( + oldRoute.firstLoadUncompressedJsBytes !== + newRoute.firstLoadUncompressedJsBytes + ) { changes.modified.push({ - name, - oldSize: oldAsset.size, - newSize: newAsset.size, - delta: newAsset.size - oldAsset.size, + name: route, + oldSize: oldRoute.firstLoadUncompressedJsBytes, + newSize: newRoute.firstLoadUncompressedJsBytes, + delta: + newRoute.firstLoadUncompressedJsBytes - + oldRoute.firstLoadUncompressedJsBytes, }); } } - for (const [name, newAsset] of newMap) { - if (!oldMap.has(name)) { - changes.added.push({ name, size: newAsset.size }); + for (const [route, newRoute] of newMap) { + if (!oldMap.has(route)) { + changes.added.push({ + name: route, + size: newRoute.firstLoadUncompressedJsBytes, + }); } } @@ -72,19 +83,25 @@ const tableSection = (title, items, columns, icon) => { }; /** - * Compares old and new assets and returns a markdown report + * Compares old and new route bundle stats and returns a markdown report */ -function reportDiff({ assets: oldAssets }, { assets: newAssets }) { - const changes = categorizeChanges(oldAssets, newAssets); - - const oldTotal = oldAssets.reduce((sum, a) => sum + a.size, 0); - const newTotal = newAssets.reduce((sum, a) => sum + a.size, 0); +function reportDiff(oldRoutes, newRoutes) { + const changes = categorizeChanges(oldRoutes, newRoutes); + + const oldTotal = oldRoutes.reduce( + (sum, r) => sum + r.firstLoadUncompressedJsBytes, + 0 + ); + const newTotal = newRoutes.reduce( + (sum, r) => sum + r.firstLoadUncompressedJsBytes, + 0 + ); const totalDelta = newTotal - oldTotal; // Summary table let report = `## 📦 Build Size Comparison\n\n### Summary\n\n| Metric | Value |\n|--------|-------|\n`; - report += `| Old Total Size | ${formatBytes(oldTotal)} |\n`; - report += `| New Total Size | ${formatBytes(newTotal)} |\n`; + report += `| Old Total First Load JS | ${formatBytes(oldTotal)} |\n`; + report += `| New Total First Load JS | ${formatBytes(newTotal)} |\n`; report += `| Delta | ${formatBytes(totalDelta)} (${formatPercent( oldTotal, newTotal @@ -98,34 +115,34 @@ function reportDiff({ assets: oldAssets }, { assets: newAssets }) { ) { report += `### Changes\n\n`; - // Asset tables + // Route tables report += tableSection( - 'Added Assets', + 'Added Routes', changes.added, [ - { label: 'Name', format: a => `\`${a.name}\`` }, - { label: 'Size', format: a => formatBytes(a.size) }, + { label: 'Route', format: a => `\`${a.name}\`` }, + { label: 'First Load JS', format: a => formatBytes(a.size) }, ], '➕' ); report += tableSection( - 'Removed Assets', + 'Removed Routes', changes.removed, [ - { label: 'Name', format: a => `\`${a.name}\`` }, - { label: 'Size', format: a => formatBytes(a.size) }, + { label: 'Route', format: a => `\`${a.name}\`` }, + { label: 'First Load JS', format: a => formatBytes(a.size) }, ], '➖' ); report += tableSection( - 'Modified Assets', + 'Modified Routes', changes.modified, [ - { label: 'Name', format: a => `\`${a.name}\`` }, - { label: 'Old Size', format: a => formatBytes(a.oldSize) }, - { label: 'New Size', format: a => formatBytes(a.newSize) }, + { label: 'Route', format: a => `\`${a.name}\`` }, + { label: 'Old First Load JS', format: a => formatBytes(a.oldSize) }, + { label: 'New First Load JS', format: a => formatBytes(a.newSize) }, { label: 'Delta', format: a => @@ -142,11 +159,11 @@ function reportDiff({ assets: oldAssets }, { assets: newAssets }) { } export async function compare({ core }) { - const [oldAssets, newAssets] = await Promise.all([ + const [oldRoutes, newRoutes] = await Promise.all([ readFile(process.env.BASE_STATS_PATH).then(f => JSON.parse(f)), readFile(process.env.HEAD_STATS_PATH).then(f => JSON.parse(f)), ]); - const comment = reportDiff(oldAssets, newAssets); + const comment = reportDiff(oldRoutes, newRoutes); core.setOutput('comment', comment); } From efab82ab34f2c2042ed36f0deede33eebf58551b Mon Sep 17 00:00:00 2001 From: avivkeller Date: Sat, 4 Jul 2026 13:06:09 -0700 Subject: [PATCH 4/4] fixup! --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f463ad5618e09..5f637a6b7712b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: if: matrix.os == 'ubuntu-latest' with: name: webpack-stats - path: apps/site.next/diagnostics/route-bundle-stats.json + path: apps/site/.next/diagnostics/route-bundle-stats.json - name: Build Next.js (Static Export) # We want to generate a static build, as it is a requirement of our website.