Skip to content

ci: hardening per TanStack supply-chain postmortem + action updates#750

Merged
jthrilly merged 6 commits into
nextfrom
ci-supply-chain-hardening
May 12, 2026
Merged

ci: hardening per TanStack supply-chain postmortem + action updates#750
jthrilly merged 6 commits into
nextfrom
ci-supply-chain-hardening

Conversation

@jthrilly

Copy link
Copy Markdown
Member

Summary

Response to the TanStack npm supply-chain compromise (2026-05-11). Six commits, all CI/CD-only — no application code touched.

What changed

  • SHA-pinned every third-party Action to an immutable commit. A future namespace takeover on chromaui, thollander, neondatabase, oven-sh, or complexdatacollective can no longer publish a malicious update to a moving tag we trust. Also replaces chromaui/action@latest (the highest-risk reference in the repo before this PR) with a specific commit on v16.10.0.
  • Hardened chromatic.yml's issue_comment trigger. Added an author_association gate so only OWNER/MEMBER/COLLABORATOR comments can fire /chromatic; removed a dead ref: input from the Setup step that would have re-introduced an untrusted-code path if someone later "fixed" it; isolated the Chromatic pnpm-store cache via cache-key-prefix so a Chromatic-triggered run cannot poison the cache that build.yml restores on push to main.
  • Migrated thollander/actions-comment-pull-request v2 → v3 in netlify-deploy-preview.yml, including its breaking parameter renames (comment_tagcomment-tag) and mode: delete semantic change (now delete-on-completion to preserve v2's end-of-job-delete behaviour).
  • Removed tj-actions/branch-names entirely — that namespace was the subject of the March 2024 changed-files compromise. We only used the action's current_branch output, which is ${{ github.head_ref }} natively on pull_request events.
  • Bumped all first-party / Docker-official Actions to their latest major versions. These are intentionally not SHA-pinned — verified-publisher namespaces (GitHub, Docker) are out of scope for SHA pinning per the project scope decision.
  • Scoped id-token: write in e2e.yml and cleanup-e2e-reports.yml from workflow-level to job-level on only the jobs that consume OIDC. The e2e job, which runs untrusted PR code via ./scripts/run-e2e.sh, no longer inherits the permission. Defense-in-depth — the OIDC token here is audience-bound to GitHub Pages, so the blast radius was already limited to Pages deployment.

Explicitly NOT in this PR

  • No @tanstack/* dependencies were touched — the repo only uses @tanstack/react-table, which is in the confirmed-clean @tanstack/table family per the postmortem.
  • No SHA-pinning of first-party actions/* / docker/* actions.
  • No fix for the pre-existing /chromatic feature bug where the workflow runs against main rather than the PR head — fixing it requires a separate product decision about whether the comment-trigger UX is worth the residual risk, and this PR removes the dead ref: input that would otherwise silently re-introduce the vulnerability if anyone "fixes" the bug.

Plan & audit trail

docs/superpowers/plans/2026-05-12-ci-hardening.md (local-only; the docs/superpowers/ path is gitignored).

Test plan

  • build.yml — passes automatically on this PR push.
  • netlify-deploy-preview.yml — automatic on PR open. Verify:
    • Neon branch created successfully (PR comment appears with the Neon link).
    • Branch preview URL resolves.
    • The deploy-preview PR comment posts via the new thollander v3 path (exercises comment-tag: rename).
    • To exercise the failure / cleanup paths, temporarily break a setup script in a follow-up push: confirm failure comment appears, then on subsequent success the failure comment is deleted (exercises mode: delete-on-completion).
  • /chromatic from a maintainer — comment /chromatic on this PR from a maintainer account. Confirm the workflow fires, Chromatic builds, the link comment appears, and the commit status is posted.
  • /chromatic from a non-maintainer — high-value but optional. Have a non-collaborator account comment /chromatic (or open a second test PR from a fork). Confirm the workflow does not fire — Actions tab shows no run.
  • e2e.yml — automatic on PR open. Verify the run completes through deploy-report and posts the PR comment with the new actions/github-script@v9.
  • docker-publish.yml — triggers only on release. Either wait for the next release or push a test tag in a scratch repo to exercise the Docker v4/v7 + checkout v6 bumps.

GitHub-UI checks not automatable

Open Settings → Actions → General and confirm:

  • "Fork pull request workflows from outside collaborators": set to Require approval for first-time contributors or stricter.
  • "Send write tokens to workflows from fork pull requests": unchecked.
  • "Send secrets and variables to workflows from fork pull requests": unchecked.

If any of these are permissive, the chromatic hardening here is necessary but insufficient.

Follow-ups (not blocking this PR)

  • File an issue against complexdatacollective/github-actions to bump the inner actions used by the setup-pnpm composite (actions/checkout@v4@v6, actions/setup-node@v4@v6, pnpm/action-setup@v4@v6, actions/cache@v4@v5).

jthrilly added 6 commits May 12, 2026 11:12
Mitigates supply-chain risk per the TanStack npm compromise postmortem
(2026-05-11). A namespace takeover on complexdatacollective, chromaui,
or oven-sh cannot publish a malicious update to a moving tag we trust.

Also replaces chromaui/action@latest (the highest-risk reference in
the repo) with a specific commit on v16.10.0.

netlify-deploy-preview.yml is intentionally untouched here; pinning
its third-party actions is bundled with a breaking-change migration
in a separate commit so the file is never in a broken state.
- actions/github-script v7 -> v9 (chromatic)
- actions/checkout v4 -> v6 (docker-publish)
- docker/setup-buildx-action v3 -> v4 (docker-publish)
- docker/login-action v3 -> v4 (docker-publish)
- docker/setup-qemu-action v3 -> v4 (docker-publish)
- docker/build-push-action v6 -> v7 (docker-publish)

All first-party / Docker-official actions, kept at major-version refs
rather than pinned to SHAs: these namespaces are verified-publisher
and out of scope for the third-party supply-chain hardening in the
companion commits. Docker v4/v7 and actions/checkout@v6 require Node
24 / Actions Runner v2.327.1+, both present on ubuntu-latest. Verified
this repo does not use DOCKER_BUILD_NO_SUMMARY or
DOCKER_BUILD_EXPORT_RETENTION_DAYS (removed in build-push v7).
Mitigates supply-chain risk per the TanStack npm compromise postmortem
(2026-05-11). A namespace takeover on neondatabase, complexdatacollective,
or thollander cannot publish a malicious update to a moving tag we trust.

Bundled changes (one commit so the file is never in a broken state):

- Pin neondatabase/create-branch-action to 6.3.1 SHA
- Pin complexdatacollective/github-actions/setup-pnpm to v1 SHA
- Pin thollander/actions-comment-pull-request to v3.0.1 SHA, which is
  also a major-version bump. v3 renames inputs from snake_case to
  kebab-case (comment_tag -> comment-tag) and changes mode: delete
  semantics. The 'Remove failure comment on success' step uses
  mode: delete-on-completion to preserve the v2 end-of-job-delete
  behavior the workflow originally relied on.
- Remove tj-actions/branch-names@v9 entirely. The tj-actions namespace
  was compromised in the March 2024 changed-files incident, and the
  only output we used (current_branch) is github.head_ref on
  pull_request events.
Closes a privileged-trigger surface flagged by the TanStack npm
compromise postmortem (2026-05-11). Two orthogonal mitigations so
no single regression reopens it:

- author_association gate: only OWNER/MEMBER/COLLABORATOR comments
  can fire the /chromatic trigger. Fork-PR authors commenting on
  their own PRs no longer qualify.
- Removed the dead Setup-step 'ref:' input (the YAML 'with:' line,
  not the 'core.setOutput(ref, ...)' value that lives in the
  github-script step further up -- that value is consumed downstream
  by 'Post links to PR' so it stays). setup-pnpm@v1 silently dropped
  the 'ref:' input (no such input declared), so the workflow already
  checked out the default branch -- but a future 'fix' would silently
  introduce the untrusted-code path. Leaving no booby trap. Also
  isolated the Chromatic pnpm-store cache via cache-key-prefix so
  this workflow cannot poison the cache that build.yml restores on
  push to main.

Residual risk: a maintainer can still be socially engineered into
commenting /chromatic on a malicious PR. Treat /chromatic like an
approval -- only invoke after reviewing the diff.
Second batch of first-party action bumps on this branch (the first
batch covered chromatic + docker-publish). All actions kept at major-
version refs rather than SHA-pinned, consistent with the project scope
decision treating verified-publisher first-party namespaces as out of
scope for the third-party supply-chain hardening.

- actions/checkout v4 -> v6 (e2e x2, cleanup x1)
- actions/upload-artifact v4 -> v7 (e2e)
- actions/download-artifact v4 -> v8 (e2e)
- actions/upload-pages-artifact v3 -> v5 (e2e, cleanup)
- actions/deploy-pages v4 -> v5 (e2e, cleanup)
- actions/github-script v7 -> v9 (e2e)

Breaking-change deltas verified safe for the actual usage:
- upload-artifact v7's opt-in direct uploads (archive: false) aren't
  used here; default zip-archive path is unchanged.
- download-artifact v8's hash-mismatch-errors-by-default is safe for
  this single-job upload/download flow.
- upload-pages-artifact v4+'s hidden-file exclusion: artifacts are
  playwright reports + index.html, no dotfiles.
- deploy-pages v5 and v6 of every other action require Node 24 /
  Runner v2.327.1+, present on ubuntu-latest.
Defense-in-depth follow-up to the supply-chain hardening commits on
this branch. The TanStack npm compromise postmortem (2026-05-11)
demonstrated that id-token: write inherited by a job running untrusted
code allows the attacker to mint an OIDC token from process memory.

In e2e.yml the e2e job runs untrusted PR code via scripts/run-e2e.sh
and inherited id-token: write from the workflow level despite never
needing it -- only deploy-report consumes OIDC (for actions/deploy-
pages). Moving id-token: write to deploy-report's job-level
permissions removes the inheritance to e2e.

In cleanup-e2e-reports.yml the workflow has a single job so the move
is structural rather than functional, but explicit job-level
permissions are self-documenting and consistent with the e2e.yml
treatment.

The OIDC token in both files is audience-bound to the github-pages
environment, so the residual blast radius is limited to Pages
deployment (no path to npm / ghcr / secrets). This commit is
defense-in-depth, not closing an active vulnerability.
@github-actions

Copy link
Copy Markdown

@jthrilly
jthrilly marked this pull request as ready for review May 12, 2026 09:58
Copilot AI review requested due to automatic review settings May 12, 2026 09:58
@github-actions

Copy link
Copy Markdown

✅ E2E tests — success

📊 View report

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

CI/CD hardening in response to the TanStack npm supply-chain compromise postmortem, focused on reducing workflow supply-chain and token-exposure risk without touching application code.

Changes:

  • SHA-pin third-party GitHub Actions (e.g., Chromatic, Neon, Thollander, Bun setup) and remove tj-actions/branch-names usage in favor of native github.head_ref.
  • Harden /chromatic comment-triggered runs by gating on author_association and isolating pnpm-store caching via a dedicated cache key prefix.
  • Reduce token exposure by scoping elevated pages / id-token / pull-requests permissions to only the jobs that require them, and bump first-party actions to newer majors.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
.github/workflows/netlify-deploy-preview.yml Removes tj-actions/branch-names, SHA-pins third-party actions, and migrates actions-comment-pull-request v2→v3 inputs/semantics.
.github/workflows/netlify-cleanup-preview.yml SHA-pins oven-sh/setup-bun for cleanup workflow determinism/supply-chain safety.
.github/workflows/e2e.yml Scopes elevated permissions to the Pages deploy job and bumps first-party actions to newer major versions.
.github/workflows/docker-publish.yml Bumps Docker official actions and actions/checkout to newer majors for the release publishing flow.
.github/workflows/cleanup-e2e-reports.yml Moves Pages/OIDC permissions to job-level and bumps Pages-related actions/checkout majors.
.github/workflows/chromatic.yml Adds maintainer-only /chromatic gating, SHA-pins Chromatic + setup action, isolates cache keyspace, and bumps github-script.
.github/workflows/build.yml SHA-pins the shared setup-pnpm composite action.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jthrilly
jthrilly merged commit 7c26899 into next May 12, 2026
17 of 18 checks passed
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