Skip to content

ci(npm-publish): rename to npm-publish.yml + env npm-publish, OIDC trusted publishing + dry-run gate + auto GitHub release (v1.x) - #1428

Merged
John-David Dalton (jdalton) merged 2 commits into
v1.xfrom
jdalton/surf-726-v1x-publish-workflows
Jul 29, 2026
Merged

ci(npm-publish): rename to npm-publish.yml + env npm-publish, OIDC trusted publishing + dry-run gate + auto GitHub release (v1.x)#1428
John-David Dalton (jdalton) merged 2 commits into
v1.xfrom
jdalton/surf-726-v1x-publish-workflows

Conversation

@jdalton

@jdalton John-David Dalton (jdalton) commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Important

Update (adds commit 1bfae126c): the workflow is renamed provenance.ymlnpm-publish.yml and its job now binds environment: npm-publish.

npm trusted publishing is per-package, not per-branch. main now publishes socket / @socketsecurity/cli / @socketsecurity/cli-with-sentry from npm-publish.yml + the npm-publish environment, and their TP entries pin that filename + environment. v1.x must publish those same packages from the same filename + environment or the OIDC token exchange 404s — so this PR conforms NAME + ENV (previously Environment: none). v1.x's own 3-variant build/publish flow is otherwise unchanged; OIDC (id-token: write, no NPM_TOKEN) is unchanged.

What this does

The v1.x publish workflow (.github/workflows/npm-publish.yml) is a standalone, inlined workflow that builds and publishes the three CLI packages to npm. This PR finishes the SURF-726 goal of making tag and release creation automatic in the release process, and removes the last long-lived npm secret from the release path.

Three changes:

  1. npm trusted publishing (OIDC) instead of a stored NPM_TOKEN. All three npm publish steps drop NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}. The job already has id-token: write, so npm mints a short-lived registry token from the OIDC token at publish time and attaches provenance. No npm secret lives in the repo anymore.
  2. A dry-run input that defaults to the safe mode. It defaults to true, so a stray dispatch builds everything but does not touch the registry. A real release requires a deliberate dispatch with dry-run: false.
  3. An automatic GitHub Release. After a successful publish and the existing v<version> tag creation, the workflow cuts the immutable GitHub Release for that tag, idempotently.

Links: SURF-726. Modeled on socket-sdk-js #639 (inlined standalone publish + idempotent auto-tag with a dry-run input), adapted to socket-cli v1.x's real build (Socket Firewall shims, the Maven manifest jar, and the three package variants), which was already inlined by #1322.

How the publish path works

The workflow runs on manual dispatch only. In order, the single build job:

  1. Checks out with persist-credentials: false (no git token in .git/config).
  2. Installs a pinned, checksum-verified pnpm and Node 25.9.0.
  3. Downloads Socket Firewall and creates shims so pnpm install is scanned.
  4. Installs dependencies and builds the Maven manifest extension jar.
  5. Builds and publishes each of the three packages in turn: socket, @socketsecurity/cli (legacy), and @socketsecurity/cli-with-sentry.

Each publish uses npm publish --provenance --access public --tag <dist-tag>. The three variants differ only in the build env vars (INLINED_SOCKET_CLI_LEGACY_BUILD, INLINED_SOCKET_CLI_SENTRY_BUILD) that rename the package and adjust its bins before publish. None of that changed here; only the auth mechanism, the dry-run gate, and the release step are new.

OIDC / trusted-publisher setup (what needs configuring on npm)

npm trusted publishing means npm trusts a specific GitHub workflow to publish a package, with no shared secret. Each package must have a trusted-publisher entry on npm pointing at:

  • Repository: SocketDev/socket-cli
  • Workflow file: .github/workflows/npm-publish.yml
  • Branch/ref: v1.x
  • Environment: npm-publish

Packages that need the entry (this workflow publishes all three):

  • socket
  • @socketsecurity/cli
  • @socketsecurity/cli-with-sentry

Until each entry exists on npm, that package's publish step will fail at the OIDC token exchange. This is why the token was removed here and configured on npm separately.

Dry-run gate

The dry-run input is a boolean, default true. Every publish step carries if: ${{ inputs.dry-run == false }}, so on a dry run they are skipped and only the build runs. Because the tag and release steps are gated on steps.publish_socket.outcome == 'success', a skipped publish also skips the tag and the release. A real release is a deliberate dispatch with dry-run: false.

A concurrency group keyed on the dist-tag serializes concurrent dispatches so two runs can't race on the same npm publish (one would 409); an in-flight publish is never cancelled.

Token scoping and the order rule

The tag and release steps talk to GitHub through gh (gh api / gh release), with GH_TOKEN set only in each step's env block. The token is never written to .git/config, so it can't leak into an earlier pnpm install's postinstall scripts. Checkout keeps persist-credentials: false for the same reason.

Order rule: the v<version> tag and the immutable GitHub Release are the final markers of a release. They are created only after the socket publish is confirmed live (publish_socket.outcome == 'success'), and the release step runs after the tag step. Tag creation is idempotent — an existing tag at the same commit is a no-op, and an existing tag at a different commit hard-fails with operator-recovery instructions (release immutability makes moving a tag unsafe). The release step is likewise idempotent: it no-ops if a release for the tag already exists.

Validation

  • actionlint on .github/workflows/npm-publish.yml: zero findings (exit 0). This PR also fixes a pre-existing actionlint error on v1.x — the debug input had an options: list that is only valid for choice-typed inputs.
  • zizmor (v1.25.2): no findings (the two suppressed items are the pre-existing, intentional github-env ignores on the pnpm/Socket Firewall download scripts).
  • No secret is written to .git/config (checkout uses persist-credentials: false) or exposed to pnpm install (the gh token lives only in the tag/release step env).

Note

High Risk
Changes the release path to OIDC publishing and automatic immutable GitHub releases; misconfiguration or a dispatch with dry-run false can block or incorrectly gate production npm/GitHub artifacts.

Overview
Hardens the v1.x manual npm publish workflow so accidental runs build only, publishes use OIDC trusted publishing instead of a repo secret, and a successful socket publish can auto-tag and cut a GitHub Release.

A new dry-run input defaults to true, so dispatch still runs the full build but skips all three npm publish steps unless dry-run: false. Tag and release steps stay gated on publish_socket succeeding, so dry runs never touch the registry or GitHub release markers. concurrency serializes runs per dist-tag without cancelling in-flight publishes.

NODE_AUTH_TOKEN / NPM_TOKEN is removed from every publish step; publishing relies on existing id-token: write and npm trusted-publisher config for socket, @socketsecurity/cli, and @socketsecurity/cli-with-sentry. Legacy and Sentry publishes get explicit step names; behavior is unchanged aside from auth and the dry-run gate.

After a successful primary publish, the tag step now exports tag to step outputs (including same-SHA no-op) for a new idempotent gh release create step (--verify-tag, --generate-notes). Minor workflow fixes: invalid debug input options, and shellcheck notes on checksum tr for Windows paths.

Reviewed by Cursor Bugbot for commit 651ad81. Configure here.

cursor[bot]

This comment was marked as resolved.

@jdalton John-David Dalton (jdalton) changed the title ci(provenance): OIDC trusted publishing + dry-run gate + auto GitHub release (v1.x) ci(npm-publish): rename to npm-publish.yml + env npm-publish, OIDC trusted publishing + dry-run gate + auto GitHub release (v1.x) Jul 27, 2026
@jdalton
John-David Dalton (jdalton) enabled auto-merge (squash) July 27, 2026 17:33
…usted publishing + dry-run gate + auto GitHub release (v1.x)

collapses the publish workflow rename, OIDC trusted publishing, dry-run
gate, and automatic GitHub release cutting for the v1.x line into one
commit. bugbot's high-severity finding was confirmed a false positive
(steps.<id>.outcome is the pre-continue-on-error result).
@jdalton
John-David Dalton (jdalton) force-pushed the jdalton/surf-726-v1x-publish-workflows branch from 1bfae12 to 76065c7 Compare July 27, 2026 17:40
…lish fail

Two ways this workflow could ship a bad release.

A dispatch from any ref could overwrite socket@latest. npm trusted
publishing authorizes on repository + workflow filename + GitHub
environment — it does not pin a branch. The npm-publish environment has
no deployment-branch policy either (checked 2026-07-28: deployment_
branch_policy is null, zero protection rules), and dist-tag defaults to
latest. So a workflow_dispatch from an old release branch, or from any
branch an attacker can push, mints a valid token and publishes over
latest. The guard now refuses a latest publish from anything but the
default branch; other refs must name a non-latest tag, which is how the
v1.x line should publish anyway. An environment deployment-branch policy
is still worth adding, but this guard does not depend on one.

A 1-of-3 release could look green. All three publishes carried
continue-on-error: true while the tag and GitHub Release were gated only
on the socket publish. If socket landed and either sibling failed, the
job stayed green and cut a v<version> tag plus an IMMUTABLE Release
describing artifacts that were never published — recovery is a version
burn. A failed publish now fails the job, so no tag and no release.

Verified by parsing the workflow rather than reading it: the guard is
step 0 of the build job and no step carries continue-on-error. The first
attempt at this edit inserted a second `steps:` key, which YAML resolves
by keeping the last one — the guard was silently dropped and the file
still looked correct by eye. Parse-checking caught it.
@jdalton

Copy link
Copy Markdown
Collaborator Author

Pushed 2297ce885 fixing two ways this workflow could ship a bad release.

A dispatch from any ref could overwrite socket@latest. npm trusted publishing authorizes on repository + workflow filename + GitHub environment — it does not pin a branch. I checked the npm-publish environment and it has no deployment-branch policy either (deployment_branch_policy: null, zero protection rules), and dist-tag defaults to latest. So a workflow_dispatch from an old release branch — or any branch an attacker can push — mints a valid publish token and publishes over latest.

The guard now refuses a latest publish from anything but the default branch. Other refs must name a non-latest tag, which is how the v1.x line should be publishing anyway. Adding a deployment-branch policy on the environment is still worth doing, but this guard doesn't depend on one.

A 1-of-3 release could look green. All three publishes carried continue-on-error: true, while the tag and GitHub Release were gated only on the socket publish. If socket landed and either sibling failed, the job stayed green and cut a v<version> tag plus an immutable Release describing artifacts that were never published — recovery is a version burn. A failed publish now fails the job, so no tag and no release.

How I verified, and the mistake worth knowing about

I checked by parsing the workflow, not by reading it: the guard is step 0 of the build job and no step carries continue-on-error.

That mattered. My first attempt inserted a second steps: key into the job. YAML resolves duplicate keys by keeping the last one, so the guard was silently dropped — and the diff still looked correct by eye. The parse check caught it; a visual review would not have.

@jdalton
John-David Dalton (jdalton) merged commit 813b5ee into v1.x Jul 29, 2026
12 checks passed
@jdalton
John-David Dalton (jdalton) deleted the jdalton/surf-726-v1x-publish-workflows branch July 29, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants