Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/bundle-size-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Report bundle size

# Runs after "Measure bundle size" completes. Because workflow_run executes in
# the context of the base repository, its GITHUB_TOKEN keeps statuses:write
# even when the triggering run came from a fork PR. This workflow posts a commit
# status using the trusted workflow_run head SHA and (on success) the measured
# sizes from the artifact — it never checks out or executes the PR's code.
on:
workflow_run:
workflows: ['Measure bundle size']
types:
- completed

permissions:
statuses: write
# Required to download the artifact from the triggering workflow run.
actions: read

jobs:
report:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'push'
steps:
# Trusted code only: the reporting script comes from the base repo's
# default branch, not from the artifact produced by the PR build.
- name: Checkout the base repo
uses: actions/checkout@v4

# Untrusted DATA only, pinned to the run that triggered this workflow.
# Skipped on a failed run (no artifact is produced); the report workflow
# posts an error status from the run conclusion instead.
- name: Download the size report artifact
if: github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4
with:
name: size-report
path: bundle-size-data
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Post the commit status
uses: actions/github-script@v7
env:
# head_sha comes from the trusted workflow_run event, not from the
# untrusted artifact, so a fork PR cannot redirect the status onto an
# arbitrary commit. It is the PR head (not the merge commit), which is
# what we want to annotate.
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
TARGET_URL: ${{ github.event.workflow_run.html_url }}
with:
script: |
const { reportBundleSize } = await import(process.cwd() + '/.github/workflows/bundle-size/status-report.js');
await reportBundleSize({
github,
context,
sha: process.env.HEAD_SHA,
dataDir: process.cwd() + '/bundle-size-data',
conclusion: process.env.CONCLUSION,
targetUrl: process.env.TARGET_URL,
});
40 changes: 14 additions & 26 deletions .github/workflows/bundle-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ on:
branches:
- main

# This workflow runs untrusted PR code (npm install + build) and therefore
# requests no write permissions. Fork PRs would have their token downgraded to
# read-only anyway. Posting the commit status is delegated to the separate
# "Report bundle size" workflow (bundle-size-report.yml), which runs with
# elevated permissions but never executes PR code.
permissions:
statuses: write
contents: read

jobs:
measure:
Expand All @@ -27,13 +32,6 @@ jobs:
cd bundle-size
npm i --force

- uses: actions/github-script@v7
name: Create a pending status on the commit
with:
script: |
const { statusReportStart } = await import(process.cwd() + '/bundle-size/status-report.js');
await statusReportStart({ context, github });

# ------- Base branch size measurement ------------------------------

- name: Checkout the base branch
Expand Down Expand Up @@ -73,25 +71,15 @@ jobs:

cat output.json

# Only the numeric measurements are uploaded. The report workflow derives
# the commit SHA from the trusted workflow_run event, not from this
# artifact, and handles a missing/failed run via the run conclusion — so
# no artifact is needed on the failure path.
- name: Upload size report artifact
uses: actions/upload-artifact@v4
with:
name: size-report
path: bundle-size/output.json

- name: Update the commit status with calculated results
uses: actions/github-script@v7
with:
script: |
const { statusReportSuccess } = await import(process.cwd() + '/bundle-size/status-report.js');
await statusReportSuccess({ context, github });

# ------- Error reporting -------------------------------------------

- name: Report failure
if: failure()
uses: actions/github-script@v7
with:
script: |
const { statusReportFailure } = await import(process.cwd() + '/bundle-size/status-report.js');
await statusReportFailure({ context, github });
path: |
bundle-size/output.json
bundle-size/output-basebranch.json
if-no-files-found: error
38 changes: 20 additions & 18 deletions .github/workflows/bundle-size/status-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,40 @@ function formatDiff(prSize, headSize) {
)} KB)`;
}

function getStatusMetadata(context) {
function getStatusMetadata(context, sha, targetUrl) {
return {
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
sha,
context: 'Bundle size',
state: 'pending',
target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
target_url: targetUrl,
};
}

export async function statusReportStart({ github, context }) {
await github.rest.repos.createCommitStatus({ ...getStatusMetadata(context), state: 'pending' });
}
// Entry point for the "Report bundle size" workflow. Posts a commit status on
// `sha` (the trusted workflow_run head SHA). On a non-success run it reports an
// error without touching the artifact. On success it reads the measurement
// data, whose values only ever flow into numeric formatting, never execution.
export async function reportBundleSize({ github, context, sha, dataDir, conclusion, targetUrl }) {
const metadata = getStatusMetadata(context, sha, targetUrl);

export async function statusReportFailure({ github, context }) {
await github.rest.repos.createCommitStatus({
...getStatusMetadata(context),
state: 'error',
description: 'The workflow encountered an error.',
});
}
if (conclusion !== 'success') {
await github.rest.repos.createCommitStatus({
...metadata,
state: 'error',
description: 'The workflow encountered an error.',
});
return;
}

export async function statusReportSuccess({ github, context }) {
const basebranch = readJson('./bundle-size/output-basebranch.json');
const pr = readJson('./bundle-size/output.json');
const basebranch = readJson(`${dataDir}/output-basebranch.json`);
const pr = readJson(`${dataDir}/output.json`);

console.log('Base branch:', basebranch);
console.log('This PR:', pr);

await github.rest.repos.createCommitStatus({
...getStatusMetadata(context),
...metadata,
state: 'success',
description: [
`CSS: ${formatDiff(pr.cssCompressedSize, basebranch.cssCompressedSize)}`,
Expand Down
Loading