Skip to content

fix: Make bundle size workflow work on fork PRs#4619

Open
TrevorBurnham wants to merge 2 commits into
cloudscape-design:mainfrom
TrevorBurnham:ci/bundle-size-fork-safe
Open

fix: Make bundle size workflow work on fork PRs#4619
TrevorBurnham wants to merge 2 commits into
cloudscape-design:mainfrom
TrevorBurnham:ci/bundle-size-fork-safe

Conversation

@TrevorBurnham

@TrevorBurnham TrevorBurnham commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This PR fixes the Measure bundle size workflow, which fails on every pull request opened from a fork (example run).

For pull_request events triggered from a fork, GitHub forcibly downgrades the GITHUB_TOKEN to read-only regardless of the permissions: the workflow requests — a security measure that prevents untrusted fork code from obtaining a write-capable token. The workflow requested statuses: write and called repos.createCommitStatus(...) to post the bundle-size result, so on fork PRs the call failed with:

HttpError: Resource not accessible by integration
POST /repos/cloudscape-design/components/statuses/<sha>
x-accepted-github-permissions: statuses=write

Because contributors routinely open PRs from forks, the workflow showed a red ❌ on those PRs and never reported a size delta.

Fix: privilege separation across two workflows

The fix here is to split the work in two:

  • bundle-size.yml (Measure bundle size) — runs the untrusted PR build (npm i, quick-build, esbuild bundle). It now requests only contents: read, does no API writes, and uploads the measurement (output.json, output-basebranch.json, metadata.json) as the size-report artifact.
  • bundle-size-report.yml (Report bundle size) — new workflow triggered via workflow_run when the measurement completes. Because workflow_run executes in the base repository's context, its GITHUB_TOKEN keeps statuses: write even for fork PRs. It downloads the artifact and posts the commit status.

status-report.js gains a single reportBundleSize() entry point used by the report workflow; the per-state helpers it replaced were removed.

Behavioural notes

  • On a failed build, only metadata.json is uploaded (written before the build, uploaded with if: always()), which is enough for the report workflow to post an error status — preserving the previous failure-reporting behaviour.
  • The interim pending status was dropped; the in-progress state is already visible from the Actions check itself.
  • The commit status now appears slightly later (after the report workflow starts) and remains a commit status rather than a required check unless wired into branch protection.
  • workflow_run workflows always run from the definition on the default branch, so the report half can only be fully exercised once this lands on main. Recommend a quick validation on a follow-up main commit after merge.

Security note: the status target is taken from a trusted source

The report workflow runs privileged, so anything it consumes from the measurement job must be treated as attacker-controlled — that job executes untrusted PR code (npm i postinstall scripts, then the build) and can write arbitrary content into the artifact.

So the SHA is read from the trusted github.event.workflow_run.head_sha event field instead of any artifact. This is GitHub-generated, not attacker-writable, and resolves to the PR head commit — which is also strictly more correct than the workflow's previous context.sha, an ephemeral merge commit. With the SHA sourced from the event, no metadata round-trip through the artifact is needed; the artifact now carries only the numeric measurements, whose values flow solely into number formatting.

Additional hardening:

  • The build job (running untrusted PR code) holds no write token.
  • The report job never checks out or executes PR code; its reporting script comes from the base repo's default-branch checkout, not the artifact, and it runs no npm i/build.
  • No ${{ }} expression is interpolated into any run/script body — dynamic values (head SHA, conclusion, target URL) are passed via env: and read through process.env, closing the expression-injection hole.
  • The artifact download is pinned to github.event.workflow_run.id.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@TrevorBurnham TrevorBurnham requested a review from a team as a code owner June 12, 2026 15:25
@TrevorBurnham TrevorBurnham requested review from taheramr and removed request for a team June 12, 2026 15:25
The bundle size workflow failed on every fork PR. GitHub downgrades the
GITHUB_TOKEN to read-only for pull_request events from forks, so the
statuses:write permission the workflow requested was denied, and posting
the commit status failed with "Resource not accessible by integration".

Split into two workflows with privilege separation:

- "Measure bundle size" runs the untrusted PR build with contents:read
  only and uploads the measured sizes as an artifact.
- "Report bundle size" runs on workflow_run (in the base repo context, so
  it keeps statuses:write even for fork PRs) and posts the commit status.
  It never checks out or executes PR code.

The commit SHA the status is attached to is taken from the trusted
workflow_run.head_sha event field, not from the artifact, so untrusted
fork code cannot redirect the status onto an arbitrary commit. head_sha
is the PR head (not the merge commit the workflow previously used).
@TrevorBurnham TrevorBurnham force-pushed the ci/bundle-size-fork-safe branch from 961a893 to 2b935c8 Compare June 12, 2026 15:41
@amanabiy amanabiy self-requested a review July 10, 2026 09:05
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.51%. Comparing base (3668b0b) to head (2b935c8).
⚠️ Report is 974 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4619      +/-   ##
==========================================
+ Coverage   96.45%   97.51%   +1.06%     
==========================================
  Files         801      948     +147     
  Lines       22872    30334    +7462     
  Branches     7490    11059    +3569     
==========================================
+ Hits        22061    29580    +7519     
+ Misses        804      707      -97     
- Partials        7       47      +40     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' ||
github.event.workflow_run.event == 'push'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't push to main. Why do we need the github.event.workflow_run.event == 'push' Can we remove it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. You're right that it's unnecessary — I've removed it in 2e6fd07.

Two notes on the reasoning:

  • Technically main does get push events (every merged PR fires one), and the measure workflow was triggering on them. But on a push there's no pull_request.base.sha, so the base and PR checkouts resolved to the same commit and the report was always a zero-delta "no change" — while still paying for a full npm i + build.
  • So rather than just drop the push arm here, I dropped the push: trigger from bundle-size.yml too. Otherwise every merge to main would still run the measurement and upload an artifact that nothing reports on.

Reporting is now PR-only.

The push path measured base==head (no PR base SHA), so it only ever
produced a zero-delta status on main commits while still running the
full npm i + build. Remove the push trigger from the measure workflow
and the corresponding arm from the report workflow's gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants