One repo, every workflow
How Bombshell maintains GitHub Actions across many repositories from a single source of truth, making hardening against recent npm supply-chain attacks a one-PR job
A recent wave of supply-chain vulnerabilities has put renewed scrutiny on CI/CD workflows. As maintainers who hold the keys to packages with about 11M combined weekly downloads, we take the threat of potential compromise seriously.
Into the Abyss
On May 11, 2026, TanStack was hit by a supply-chain attack that published 84 malicious versions across 42 packages in about six minutes. The chain that made it possible is documented in their postmortem; the part that matters here is the final exploit—attackers extracted a live OIDC token from a publish job’s memory and used it to push directly to npm.
Bombshell’s publishing pipeline was already doing several things right. Our workflows already no-op on forks—every job is gated with if: github.repository_owner == 'bombshell-dev'. We use OIDC trusted publishing, enforce 2FA everywhere we can, and don’t use pull_request_target in GitHub Actions. We have branch protections in place with an exclusion for our dedicated bot account, which also handles publishing.
Before getting into what we needed to change, it’s worth explaining our fairly unique approach to GitHub Actions.
One Repo to Rule them All
One of the first things we configured when Bombshell graduated from a personal project to a proper organization was a centralized automation repository. The initial inspiration was similar work we started in the Astro community, but Bombshell’s approach has evolved quite a bit since then.
The concept is simple—instead of duplicating workflow YAML across every repository, we define reusable workflows once in automation and call them from each consumer repo. The repo actions themselves end up with an extremely light footprint:
name: CIon: push: branches: [main] pull_request:
permissions: {}
jobs: scripts: if: github.repository_owner == 'bombshell-dev' uses: bombshell-dev/automation/.github/workflows/run.yml@3a8b4a38fe464b0b51d14962ae416a169517fba9 # main as of 2026-05-12 with: commands: '["build", "types", "test", "deps"]'That’s a full CI configuration! The actual logic that installs Node, sets up pnpm, and runs each command in a matrix lives in automation. Reusable workflows are the mechanism that makes this scale across repos—each of our repo workflows calls out to an automation workflow via uses:.
In practice, we’ve found that this architecture makes maintaining GitHub Actions, and especially propagating updates across an entire org, significantly less painful.
The Reckoning
If you’re looking for a full security checklist, Zach Leatherman’s and the e18e publishing guide are both excellent. We’ll cover just what needed to change for Bombshell.
After evaluating our actions, we needed four updates. Roughly in order of impact: pin SHAs, default-deny permissions, explicit secret pass-through, and splitting build from publish. The first two are mechanical, the third is good hygiene, and the last is the most critical mitigation.
Pin floating refs to a SHA. Out of pure laziness, our workflows used @main references. But branch refs are mutable, so changes to automation could easily break CI in downstream repos. We replaced every @main with a 40-character commit SHA:
# beforeuses: bombshell-dev/automation/.github/workflows/publish.yml@main
# afteruses: bombshell-dev/automation/.github/workflows/publish.yml@3a8b4a38fe464b0b51d14962ae416a169517fba9 # main as of 2026-05-12Since pinning is only useful when you keep references updated, we configured Dependabot to open weekly bump PRs. Changes from the automation repo are now accepted at the consumer’s convenience.
version: 2updates: - package-ecosystem: github-actions directory: / schedule: interval: weeklyDefault-deny workflow permissions. Without setting explicit permissions: at the top of a workflow file, jobs inherit the org or repo default. We added permissions: {} to the root of every workflow file to ensure explicit failures for new jobs instead of silent inheritance. Job-level permissions grant what each job actually needs.
name: Publishon: push: branches: [main]
permissions: {} # default deny — jobs opt in explicitly
jobs: publish: permissions: id-token: write contents: write pull-requests: write uses: bombshell-dev/automation/.github/workflows/publish.yml@<sha>Replace secrets: inherit with explicit pass-through. secrets: inherit does exactly what it seems—your entire secret store is inherited by the reusable workflow. While that made changing required secrets easy, sharing as little as possible is wise. Explicit pass-through turns secret access into a contract that is transparent on both sides.
# beforeuses: bombshell-dev/automation/.github/workflows/publish.yml@mainsecrets: inherit
# afteruses: bombshell-dev/automation/.github/workflows/publish.yml@<sha>secrets: BOT_APP_ID: ${{ secrets.BOT_APP_ID }} BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}Split build and publish. The riskiest workflow in any setup is the one that uses a write token to publish. This is exactly where the TanStack exploit happened. Our original publish job ran pnpm install → pnpm run build → changesets publish in a single job, meaning the OIDC token was live during dependency installation and the build step. We were potentially vulnerable to token extraction from a compromised dependency.
split them:
jobs: build: permissions: contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - run: pnpm install - run: pnpm run build - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: build-output path: packages/*/dist retention-days: 1
publish: needs: build permissions: contents: write pull-requests: write id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install dependencies (no scripts) run: pnpm install --ignore-scripts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: build-output - uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0 with: publish: pnpm exec changeset publishNow the publish job never runs project code. It installs with --ignore-scripts (no lifecycle hooks), downloads the prebuilt artifact, and hands off to Changesets. The OIDC token only exists in a job that has no opportunity to run a poisoned postinstall.
You can see one of the resulting release PRs here—@bombshell-bot picks up the changeset, versions the packages, and publishes.
The Reward
The thing that made all of this manageable is our centralized repo. When the TanStack post dropped, we were able to apply changes once in automation and open one hardening PR per consumer repo to bump the SHA pins and add permissions: {}. No per-repo git history spelunking needed.
When security fixes, Node version bumps, and tooling updates all flow through one place, the per-repo surface stays really tiny. It’s pretty nice.
Keep your friends close…
Supply-chain risk isn’t limited to compromised dependencies. As the xz-utils backdoor demonstrated, patient social engineering and reputation are additional vectors to consider.
Back in February, Socket reported that an autonomous agent landed PRs in 95 major projects—Nx, ESLint, Vitest, React, Astro, and yes, even Clack. There was no disclosure that the account was agentic, so while clack#460 was a technically competent fix, the pattern of establishing trust for future payoff was all too familiar.
We immediately started thinking about how to detect this systematically and surface additional signal in our projects. We’ve since added a detect-agent workflow to our automation repo, powered by @MatteoGabriele’s AgentScan. The workflow analyzes the PR author’s public event history and applies an automated label to PRs from suspect accounts. There’s also a voluntary AI disclosure in our PR templates.
While we’re not closing anything automatically, the label gives reviewers a signal to better understand the risk profile of the contributions they’re evaluating. Keep your friends close and your potentially autonomous reputation-farming agents closer.
Fork Around and Find Out
Feel free to fork our repo—it’s MIT-licensed.
If it saves you some time, consider supporting Bombshell on OpenCollective. The best way to secure our supply chain is to pay maintainers enough to focus on their projects long-term.