Every Trusted Publisher Expands the Release-Authority Union
npm now accepts multiple OIDC publishers while GitHub exposes reusable-workflow identity. Audit release authority as a union of paths.
GitHub's 3 September update lets an npm package have multiple trusted publishing configurations. That removes an awkward constraint: stable, prerelease, staging, and migration workflows no longer have to squeeze through one OIDC configuration or retain a long-lived publish token.
The important detail is the authorization model. Each configuration is independent and additive. A publish or stage is allowed when its OIDC token matches any one configuration; configurations do not restrict one another, and their evaluation order is not guaranteed.
Two other same-day changes make the operational consequence clearer. GitHub Actions can now expose the source identity of the workflow defining a job, including its repository, file, ref, and commit SHA. CodeQL 2.26.4 now detects mutable references to reusable workflows. Read together, these releases turn package publishing from a single-workflow setting into a graph of release paths that can be enumerated, observed, and narrowed.
The repeated angle this avoids
Recent posts here have already covered publish-time malware controls, dependency cooldowns, explicit registry contracts, credential revocation, and the difference between mitigation and remediation. The generic X needs Y version would be "npm publishing needs more guardrails." That would add little.
The sharper thesis is mathematical and operational: multiple publishing configurations use OR between paths, while the fields inside one configuration act like AND criteria. That distinction changes how maintainers should migrate, review, log, and revoke release authority.
The new information surplus comes from joining three layers that the announcements describe separately:
- npm decides whether an incoming identity is an allowed publisher.
- GitHub Actions decides which workflow definition actually supplies a job.
- Staged publishing decides whether an authorized submission becomes available immediately.
Those are different controls. Treating one as proof of the others creates avoidable supply-chain gaps.
Model the package as a union of sufficient paths
Suppose a package has three trusted publishers:
- the main repository's
release.ymlusing theproductionenvironment; - the same repository's
prerelease.ymlusing thepreviewenvironment; - a migration repository's
publish.ymlusing theproductionenvironment.
The effective OIDC release authority is:
allowed_oidc_publish = stable_path OR prerelease_path OR migration_path
Within the stable path, the check is closer to:
stable_path = owner AND repository AND workflow_filename AND environment AND allowed_action
This is why adding a "more restricted" second configuration does not tighten the first one. If the first path omits an environment or permits direct publishing, the second path cannot compensate for it. GitHub's announcement is explicit that configurations never restrict one another.
The npm trusted publishing documentation says a package can have up to ten trusted publishers. For GitHub Actions, each configuration identifies an organization or user, repository, workflow filename, optional environment, and allowed actions. Staging is allowed by default; direct npm publish is an opt-in for each configuration.
That is useful flexibility, but configuration count is now a security metric. Ten entries are not ten layers of defence. They are as many as ten independently sufficient doors.
Do not forget the token side of the union
OIDC removes the need to store a long-lived write token in a workflow, but enabling a trusted publisher does not automatically prove every old token path has disappeared. npm's documentation says trusted publishing is accepted in addition to traditional authentication methods, and the CLI can fall back to traditional tokens.
During migration, the effective authority may therefore be:
allowed_publish = any_trusted_publisher OR any_remaining_publish_credential
That temporary overlap is valuable because it lets a maintainer prove the new route before cutting off the old one. It becomes dangerous when "temporary" has no owner or removal date.
npm recommends verifying trusted publishers first, then changing package publishing access to require two-factor authentication and disallow tokens, and finally revoking obsolete automation tokens. The sequence matters:
- add the OIDC configuration;
- exercise it with a real staged version;
- verify the package, source path, and approval record;
- restrict traditional token access;
- revoke old automation tokens;
- remove any migration-only publisher configuration.
Skipping step two risks a release outage. Skipping steps four through six leaves an invisible bypass.
Separate authorization, execution identity, and availability
A useful review starts by naming what each control can actually prove.
| Control | What it proves | What it does not prove |
|---|---|---|
| npm trusted publisher | An incoming OIDC identity matched one configured release path | That another configured path is equally strict |
| GitHub environment | The job used the named environment and its configured protections | That every publisher configuration requires that environment |
job.workflow_* context |
Which repository, file, ref, and SHA defined the current job | That npm authorized those values or that the called ref was safely selected |
| SHA-pinned reusable workflow | The called workflow definition cannot move without a caller change | That the caller's trigger, inputs, or permissions are safe |
| Staged publishing | An authorized submission waits for approval before availability | That the submitted package is correct or that the submitter path was least-privileged |
| Malware scan | Registry scanning completed before staged approval is enabled | That the release is free from logic errors or compromised-but-non-malicious behaviour |
GitHub says the new job.workflow_ref, job.workflow_sha, job.workflow_repository, and job.workflow_file_path values identify the workflow that defines the current job. They match the existing github.workflow_ref and github.workflow_sha for a directly defined job, but diverge when a reusable workflow defines the job.
That makes the new context ideal for a release receipt. It is observability, not authorization by itself. Printing job.workflow_sha after publishing does not constrain which SHA was allowed to run; the workflow reference and surrounding policy still need to do that.
Reusable workflows create a caller-and-definition pair
A release job built from a reusable workflow has at least two identities worth recording:
- the caller run that supplied the event, repository context, inputs, and permissions;
- the reusable workflow definition that supplied the release logic.
GitHub's reusable workflow documentation allows a cross-repository workflow reference to use a commit SHA, release tag, or branch. It calls a commit SHA the safest option for stability and security. CodeQL's new detection for mutable reusable-workflow references turns that advice into a finding teams can route through code scanning.
For same-repository calls, $/ or ./ resolves the called workflow from the same commit as the caller. For cross-repository calls, a branch such as @main can move without a change in the consuming repository. That is convenient for centrally managed workflows, but it also means release logic can change outside the package repository's review trail.
Use this decision rule:
- Same repository, same commit: prefer
$/where supported or./; review the caller and reusable workflow in one commit. - Cross-repository, high-authority job: pin a full commit SHA; use an update process to advance it.
- Cross-repository, moving tag or branch: accept only when central maintainers, change control, monitoring, and rollback make the transitive trust explicit.
A shared workflow can reduce duplicated release code. It also concentrates authority. One compromised moving reference may affect many packages, even when each package has a tidy local release.yml.
Keep a release-authority ledger
A repository inventory is not enough because npm configuration lives outside Git. Keep a small ledger per package that joins registry settings to workflow source and lifecycle intent.
package: '@example/checkout-client'
traditional_publish_tokens: disallowed
publisher_paths:
- purpose: stable
provider: github-actions
repository: example/checkout-client
workflow: release.yml
environment: production
allowed_action: stage-only
reusable_workflow: example/release-workflows/.github/workflows/npm.yml
reusable_ref_policy: full-sha
owner: web-platform
remove_after: null
- purpose: migration
provider: github-actions
repository: example/checkout-migration
workflow: publish.yml
environment: production
allowed_action: stage-only
reusable_workflow: null
reusable_ref_policy: not-applicable
owner: web-platform
remove_after: '2026-09-18'
This is an operating record, not a file npm reads. Its value is reconciliation. A reviewer should compare the ledger with all three live surfaces:
- npm's package publishing settings;
- workflow files and their reusable-workflow references;
- GitHub environment protections and branch or tag rules.
The remove_after field prevents migration and emergency paths from becoming permanent by neglect. The allowed_action field exposes which routes can bypass staging. The reusable-workflow fields reveal authority that is easy to miss when reviewing only the top-level workflow filename configured on npm.
Add a machine-readable release receipt
The new job context can make each run explain which definition executed. Capture both caller and defining-workflow data before the publish step, then retain it with the workflow logs or release evidence.
- name: Record release identity
run: |
{
echo "caller_ref=${{ github.workflow_ref }}"
echo "caller_sha=${{ github.workflow_sha }}"
echo "job_ref=${{ job.workflow_ref }}"
echo "job_sha=${{ job.workflow_sha }}"
echo "job_repository=${{ job.workflow_repository }}"
echo "job_file=${{ job.workflow_file_path }}"
echo "environment=production"
echo "release_mode=stage"
} | tee release-identity.txt
Do not place secrets or a full dumped context in the receipt. The six explicit fields are enough to answer two incident questions quickly: which caller initiated the release, and which workflow definition supplied the job?
The receipt should be generated before submission and uploaded even when later steps fail. Otherwise, a failed or rejected stage may be the run with the least evidence.
Stage-only should be the ordinary route
GitHub's npm announcement also tightens the staged path: a staged package cannot be approved until malware scanning finishes, and maintainers can see version history showing whether a version was staged, approved, or rejected. npm's docs say new trusted-publisher configurations allow staging by default, while direct publishing remains optional.
That creates a useful separation:
- OIDC answers: may this workflow submit this package?
- malware scanning answers: did the registry's scan finish without blocking approval?
- human approval answers: should this exact staged version become available?
For most small teams, make stage-only the default for every path. Permit direct publishing only where release latency has a documented business requirement and another control supplies equivalent confidence. "The old workflow used npm publish" is not a requirement.
Failure modes worth testing
Multiple publishers solve real migration and release-lane problems, but they also create predictable mistakes.
A narrower entry is mistaken for a global restriction
A team adds a production-environment path and assumes all publishes now require that environment. An older path without the environment still authorizes a release. Test every configuration independently; do not infer package-wide policy from the newest one.
A migration door remains open
A second repository is authorized during a platform move, then forgotten. Its workflow, maintainers, branch rules, and dependencies remain sufficient to publish. Give temporary paths an expiry date and alert on overdue removal.
Direct publish is enabled everywhere
Stable and emergency releases may justify different behaviour, but direct publishing is switched on across all entries for convenience. Keep direct authority exceptional and visible in the ledger.
A reusable workflow moves underneath the caller
The package repository pins its own release trigger, but calls a central workflow at @main. A central change alters release logic without a local diff. Pin high-authority cross-repository workflows to a SHA and treat CodeQL's mutable-reference alert as a release-control issue, not style noise.
Runtime identity is logged but never checked
A receipt records the called SHA after the fact, yet no review process compares it with the approved SHA or policy. Evidence without an expected value helps forensics, not prevention. Reconcile receipts against the ledger or an allowlist.
Renaming a workflow becomes an emergency
npm's configuration matches a workflow filename, and its documentation says matching is exact and case-sensitive. Rename the file only as a controlled publisher migration: add and test the new path, then remove the old one.
A practical review sequence
For each package maintained by your team, use this order:
- List every trusted publisher from npm and count the independently sufficient paths.
- Record repository, workflow filename, environment, provider, and allowed action for each path.
- List every remaining token or manual route that can publish.
- Mark paths as permanent, prerelease, emergency, or temporary migration.
- Require an owner and expiry date for temporary paths.
- Check that ordinary paths are stage-only and environment-scoped.
- Inspect every reusable workflow reference; pin high-authority cross-repository calls to full SHAs.
- Enable or review CodeQL scanning for mutable reusable-workflow references.
- Add caller and job-definition identity to the release receipt.
- Run one staged release, verify scan and approval history, then remove obsolete configurations and credentials.
The final acceptance rule should fit in one sentence:
Every currently sufficient release path is intentional, owned, observable, and no broader than its purpose.
Multiple trusted publishers are a meaningful improvement because maintainers no longer need a token workaround for legitimate parallel release lanes. The safe way to adopt them is not to ask whether each new entry looks secure. Draw the whole authorization union, record the workflow definition that actually ran, and shrink temporary routes as soon as their job is done.