A green check is not a release boundary
A pull request needs enough freedom to build and test code. It should not inherit the authority to publish a package. GitHub's July 28 supply-chain update explains why: recent attacks have chained an initial workflow compromise to credential theft, cache poisoning and malicious releases across multiple projects.
The dangerous design is not simply "CI has a secret." It is a trust path. Unreviewed code runs in one job, leaves output or state behind, and a later job with more authority treats that output as safe. If the privileged job has a reusable registry token, repository write access or a shared mutable cache, the pull request may gain a route to release authority without ever editing the publish workflow.
As checked on 26 September 2026, GitHub's secure-use reference warns that privileged pull_request_target and workflow_run jobs can expose write access, secrets and shared cache state when they check out or consume untrusted code. A green test result only says the configured checks passed. It does not prove the test job was isolated from the release path.
The practical acceptance condition is stronger: code from a pull request can fail tests, produce artifacts and request review, but it cannot mint a publishing credential or influence what a privileged job releases without a separate verification step.
Split untrusted tests from privileged publishing
Start by drawing two lanes.
The first lane handles pull requests. It checks out the proposed commit, installs dependencies, runs lint, tests and a build, and uploads evidence. Give its GITHUB_TOKEN read-only contents permission unless a specific step needs more. Do not expose production environments or registry credentials. Treat every file, artifact name, cache entry and command produced by this lane as attacker-controlled input.
The second lane publishes. Trigger it from a reviewed tag, a protected branch, or a manually approved environment. Check out the exact reviewed commit again rather than reusing a workspace from the pull-request job. Build the release from a locked dependency graph, or verify the digest of an artifact before accepting it. The publish lane should know exactly which commit it is releasing and why that commit became eligible.
This separation matters even when the repository is private. A compromised developer account, dependency or third-party action can still reach a workflow. GitHub notes that a single compromised action can access the secrets available to its job and may use the job token to write to the repository. Private access narrows who can open a pull request. It does not make untrusted execution privileged by default.
Avoid checking out pull-request code inside pull_request_target. That trigger runs in the context of the base repository. If you need it for labels or comments, keep the job metadata-only. GitHub now applies safer checkout defaults in commonly exploited cases, but an explicit opt-out can reopen the path. Review trigger, checkout ref and permissions together.
Remove the reusable publishing secret
A long-lived npm token turns any successful theft into continuing publish access until somebody notices and revokes it. Moving the token into a better secret store protects it at rest, but the workflow still has to reveal it to the publishing process at runtime.
npm's trusted publishing documentation describes a narrower model. The registry trusts one named repository and workflow, then exchanges a signed OpenID Connect identity for a short-lived, workflow-specific publishing credential. There is no reusable npm write token for the job to print, copy or save.
That is a meaningful reduction in blast radius, not a complete defence. The workflow with id-token: write can still ask for an identity token. Its trigger, checked-out commit and environment protections therefore remain part of the security boundary. Configure id-token: write only on the publishing job, not at workflow or organisation level.
Keep installation credentials separate too. If the build needs private packages, use a read-only token for installation. Publishing should use the short-lived identity. A stolen read token may expose private source, which is serious, but it should not also permit a malicious release.
After trusted publishing works, disable traditional token-based publishing and revoke old automation tokens. Otherwise the safer path exists beside the original bypass.
Add a second release gate
Short-lived credentials answer who is asking to publish. They do not answer whether this release should go live.
npm supports staged publishing, where CI can stage a version and a maintainer approves it with two-factor authentication before public release. A GitHub environment with required reviewers provides another gate before the publishing job receives its authority. Either approach breaks the assumption that one compromised workflow execution is enough.
Choose the gate according to the release model. A small internal package may use a protected tag and required environment review. A public package with many downstream users may justify staged publishing and an explicit package diff. The important property is independence: the actor or step approving release should not merely repeat a decision already made by the untrusted lane.
Review the package contents, not just the source diff. Generated bundles can include files that are absent from the pull request view. Run npm pack --dry-run, inspect the file list, and compare the package version and provenance to the reviewed commit. A correct source tree can still produce the wrong artifact through a changed build script or ignored file rule.
Audit the path, not just the YAML
A workflow review should follow authority from trigger to registry.
List every event that can start the workflow. Record which commit is checked out. Expand reusable workflows and third-party actions. Pin third-party actions to full commit SHAs, which GitHub identifies as the immutable option. Check job-level permissions, environment reviewers, cache access, artifact producers and network destinations.
Then test the negative cases. Open a pull request from a fork and confirm it receives no publishing credential. Change an artifact name and confirm the release job rejects it. Attempt to run the publish job from an unprotected branch. Remove an approval. A control that only works in the expected path has not yet proved its boundary.
Keep the evidence small enough to review: the release commit, artifact digest, workflow run, approving identity and registry result. This is more useful during an incident than a screenshot of a green pipeline.
What this does not change
Separating workflows does not make dependencies trustworthy. A malicious dependency can execute during the privileged build unless install scripts, lockfiles and package sources are controlled. GitHub's platform changes reduce common attack paths, but they do not inspect your application logic or decide whether a release is safe.
OIDC also does not remove every secret. Private dependency installation, signing systems or deployment platforms may still require credentials. Give each one its own scope and lifetime instead of collecting them in a single release job.
The useful result is narrower: pull-request code can prove that it builds, but cannot publish by itself. The release job starts from an independently eligible commit, receives only short-lived authority, and leaves evidence that ties the registry artifact back to that commit. That turns publication from a side effect of CI into a separate security decision.
Written by Dandelion Labs