Skip to content

ci: isolate publish from installs and drop NPM_TOKEN for OIDC - #596

Draft
guyofeck wants to merge 1 commit into
mainfrom
ci/trusted-publishing
Draft

ci: isolate publish from installs and drop NPM_TOKEN for OIDC#596
guyofeck wants to merge 1 commit into
mainfrom
ci/trusted-publishing

Conversation

@guyofeck

Copy link
Copy Markdown
Contributor

What

Makes both publish workflows satisfy the two controls @dimar set out in this thread, so GitHub Actions can be re-enabled for base44/cli at the org level.

packages-firewall/embargo does not cover the publish flow, so both publish workflows unpin registry.npmjs.org before npm publish. Two problems with that as it stood:

  1. The same job carried secrets.NPM_TOKEN — exactly the credential a poisoned transitive dependency would exfiltrate.
  2. "Nothing is installed after the unpin" held only by step ordering. True today, but by comment, not by construction.

The split is the control

Each publish workflow becomes two jobs:

Job Gateway Installs Credential
build pinned throughout bun install --frozen-lockfile none
publish unpins before publish nothing none (OIDC)

build does every install behind the embargo gateway, so the cooldown applies to the whole tree, then uploads the built package as an artifact. publish downloads that artifact and runs only npm publish — no dependency is ever resolved while the gateway is bypassed, so the unpin cannot pull an un-embargoed package. The property is now structural: the publish job has no package-manager install step at all.

Also in here

  • Trusted publishing (OIDC) replaces token auth. Drops NODE_AUTH_TOKEN/secrets.NPM_TOKEN from preview-publish.yml. Note manual-publish.yml never had a credential wired at all — setup-node writes an .npmrc expecting ${NODE_AUTH_TOKEN} and nothing set it — so it could not have published regardless of the org block. OIDC is what makes it work; no token was added back.
  • Removed npm install -g npm@latest from both. Trusted publishing needs npm >= 11.5.1, and every Node 24.x release bundles npm >= 11.6.2, so .node-version already clears the floor. The upgrade also fetched npm through the gateway, which refuses a release until it clears the cooldown — a self-inflicted flake.
  • Least-privilege per-job permissions replace the workflow-level blocks: build gets contents: read; publish gets id-token: write plus only what its release steps need. Drops manual-publish's unused packages: write and pull-requests: read.

Root cause of the embargo publish failure

Documented in the workflow comments so the unpin gets removed once fixed rather than living forever. From package-embargo/nginx.conf:

  • npm publish sends PUT /<package>, which matches neither the ^~ /-/ passthrough block nor ~ \.tgz$ — so it falls into location /proxy_metadata(), a read path with caching, whose own comment scopes it to /lodash, /@babel/core.
  • nginx.conf sets no client_max_body_size, so nginx's 1 MB default applies. npm publish base64-encodes the tarball into the packument, making the body ~1.37x the tarball — over the limit for base44, giving a 413 before Lua runs.
  • lua/nginx.lua:152-164 reads the entire body into worker memory (f:read("*all")), so raising the limit alone would buffer every publish tarball in RAM.

The routing, missing client_max_body_size, and full-body buffering are verified in the config. The 413 itself is inference — worth confirming against the actual response.

Verification

  • .github/scripts/check_wix_proxy_steps.py16 of 16 jobs across 13 workflows (was 14; the split adds 2). Both new jobs have checkout at step 1, proxy at step 2.
  • Asserted no job both unpins/publishes and installs, and no npm token remains in either workflow.
  • Cannot be run: Actions is disabled org-wide for base44, so nothing here has executed. Review accordingly.

Blocked on / needs decision

  1. Registry-side setup, before either workflow can publish. A trusted publisher on npmjs.com keyed per repo and per workflow filename — so manual-publish.yml and preview-publish.yml each need their own entry. Needs npm org admin. Without it both workflows fail to authenticate.
  2. @dimar — requirement 1 (min-release-age) is not addressed, deliberately. Every install here is bun install, and Bun has no equivalent directive; honoring it literally means migrating off bun.lock across all six installing workflows, against this repo's Bun-for-everything rule. My read is that the requirement was premised on publish jobs bypassing the gateway wholesale — which is not what ci: enforce wix gateway proxy in all workflows #592 built, and is now structurally not the case. Does gatewayed bun install plus a publish job that installs nothing satisfy the control? If not, this PR needs a package-manager migration and will look quite different.
  3. Whether dry_run=true is worth anything as a smoke test: npm publish --dry-run never authenticates, so it cannot exercise the OIDC path. First real proof will be a preview publish.

🤖 Generated with Claude Code

Embargo/packages-firewall does not cover the publish flow, so both publish
workflows unpin registry.npmjs.org before `npm publish`. Two problems with
that as it stood: the same job also carried secrets.NPM_TOKEN — exactly the
credential a poisoned transitive dependency would exfiltrate — and "nothing is
installed after the unpin" held only by step ordering, i.e. by comment.

Split each publish workflow into two jobs so the property is structural:

  build   - fully gatewayed. Every dependency install happens here, behind the
            embargo gateway, so the cooldown applies to the whole tree. Never
            unpins, holds no publish credential. Uploads the built package.
  publish - installs nothing. Downloads build's artifact and runs `npm publish`.
            No dependency is resolved while the gateway is bypassed, so the
            unpin cannot pull an un-embargoed package.

Also:

- Authenticate via npm trusted publishing (OIDC). Drops
  NODE_AUTH_TOKEN/secrets.NPM_TOKEN from preview-publish; manual-publish never
  had a credential wired at all, so it could not have published regardless.
- Remove `npm install -g npm@latest` from both. Trusted publishing needs
  npm >= 11.5.1 and every Node 24.x release bundles npm >= 11.6.2, so
  .node-version already clears the floor. The upgrade also fetched through the
  gateway, which refuses an npm release until it clears the cooldown.
- Least-privilege per-job permissions, replacing the workflow-level blocks:
  build gets contents: read; publish gets id-token: write plus only what its
  release steps need. Drops manual-publish's unused packages: write and
  pull-requests: read.
- Document why the unpin exists, why it is safe, and that it is temporary:
  `npm publish` sends PUT /<package>, which matches neither the gateway's
  `^~ /-/` passthrough block nor `~ \.tgz$`, so it lands in `location /`
  (proxy_metadata, a read path with caching); the gateway also sets no
  client_max_body_size, so nginx's 1 MB default rejects a packument carrying
  the base64 tarball.

Verified: .github/scripts/check_wix_proxy_steps.py passes 16 of 16 jobs across
13 workflows, and no job both unpins/publishes and installs.

Requires registry-side setup before either workflow can publish: a trusted
publisher on npmjs.com per repo AND per workflow filename, so manual-publish.yml
and preview-publish.yml each need their own entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@guyofeck

Copy link
Copy Markdown
Contributor Author

Superseded by #597.

@dimar's verdict on the build/publish split in this thread: "it's valid for sure, but too complicated for a long-term solution." He prescribed an interim policy instead — enforced lockfiles, a package manager honoring a minimal-age directive, and embargo only in non-publish tasks — which #597 implements.

The trusted-publishing (OIDC) work from here carries over to #597 unchanged; the split and the unpin documentation do not. Keeping this open only as the record of the split approach and the embargo root-cause analysis.

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.

1 participant