v3/docs/adr/ADR-378-npm-trusted-publishing-cicd.md
CLAUDE.md → "Publishing to npm" (existing manual runbook), 2026-07-14 helpers-signing-key leak incident (same section), Cognitum Platform deployment table (existing WIF precedent for meta-llm's CD).github/workflows/ has 24 workflows; none named *publish* or *release*). This is net-new automation, not a migration.All three published packages — @claude-flow/cli (v3/@claude-flow/cli/), claude-flow
(repo root), ruflo (ruflo/) — are version-locked (currently 3.32.24 across all three)
and published by hand, from an operator's local machine, per the runbook in root
CLAUDE.md:
npm version <x.y.z> --no-git-tag-version in each of the three package directories, in ordernpm publish per package (default latest tag)npm dist-tag add <pkg>@<version> alpha and ... v3alpha per package (9 dist-tag calls total)latest === alpha === v3alpha across all threegit tag v<version> main && git push origin v<version> && gh release create ...@claude-flow/cli's prepublishOnly runs scripts/prepare-publish.mjs, which:
@claude-flow/swarm then the CLI itself via tscREADME.md into the packageplugins/ruflo-metaharness plugin into the tarballgenerate-catalog-manifest.mjs, sign-helpers.mjs, verify-helpers.mjs in sequencesign-helpers.mjs needs a private Ed25519 key to sign .claude/helpers/helpers.manifest.json.
That key lives in GCP Secret Manager, project ruv-dev, secret ruflo-helpers-signing-key
— a separate trust boundary from npm auth itself, and one this repo has already been burned
by: on 2026-07-14, gcloud secrets versions access latest --secret=ruflo-helpers-signing-key
was run in a way that printed the PEM to stdout, which landed in a Claude Code tool-call
transcript. GCP secret v1 was destroyed and v2 rotated in (commit 0052b1b06 / PR #2673).
CLAUDE.md now mandates the secret only ever be redirected straight to a file, never allowed
to reach tool output.
The user attempted to create an npm access token on npmjs.com for CI/CD use and hit npm's own warning against it:
"There are security risks with this option. For automation or CI/CD uses, please use Trusted Publishing instead."
npm's Trusted Publishing (general availability with npm CLI ≥ 11.5.1, OIDC-based, modeled
on PyPI's Trusted Publishers) lets a CI job authenticate to the npm registry with a
short-lived token minted by exchanging the CI provider's OIDC identity token — no NPM_TOKEN
secret stored anywhere, nothing to leak, nothing to rotate. Currently supported: GitHub Actions
(GA); GitLab CI (beta).
This is the same shape of fix this repo already applied elsewhere: meta-llm's Cloud Run
deploy uses Workload Identity Federation (GCP_WIF_PROVIDER + GCP_DEPLOY_SA, no static
service-account JSON key) instead of a long-lived credential. Trusted Publishing is the npm
-registry equivalent of that same pattern.
Adopt npm Trusted Publishing via GitHub Actions OIDC for all three packages, and fetch the helpers-signing secret in CI via GCP Workload Identity Federation rather than a static service-account key — so no long-lived secret of either kind (npm or GCP) is stored in GitHub Actions secrets for this workflow.
For each of @claude-flow/cli, claude-flow, ruflo: package Settings → Trusted
Publisher → add GitHub Actions publisher scoped to:
ruvnet/ruflo.github/workflows/npm-publish.ymlnpm-publish (see below — required reviewers preserve the "a human approves
this" property the manual process has today)Create a dedicated WIF pool + provider trusting GitHub's OIDC issuer, scoped to this repo,
and an IAM binding granting only roles/secretmanager.secretAccessor on
projects/ruv-dev/secrets/ruflo-helpers-signing-key — not project-wide, not org-wide. This
is a new, narrower principal; do not reuse meta-llm's GCP_DEPLOY_SA binding (different
blast radius, different purpose).
.github/workflows/npm-publish.ymlworkflow_dispatch only (inputs: version, bump type). Not on tag-push,
not on merge-to-main — the semver bump is a judgment call per CLAUDE.md's PATCH/MINOR/MAJOR
discipline, so a human still decides when and what to publish; this workflow only
automates the mechanics once that decision is made.id-token: write (OIDC for both npm and GCP WIF), contents: write
(tag + release).npm-publish, with required reviewers configured in repo settings —
this is the workflow-level equivalent of "an operator is at the keyboard."ubuntu-latest):
actions/checkout@v4actions/setup-node@v4 — pin node-version: 20, registry-url: https://registry.npmjs.org;
confirm npm ≥ 11.5.1 (npm install -g npm@latest if the bundled version is older —
Trusted Publishing silently falls back to failing auth on older npm, so pin explicitly,
don't assume)google-github-actions/auth@v2 with workload_identity_provider + the dedicated
service account — exports credentials for the next step without ever materializing a
JSON key filegcloud with output
redirected straight into env, never through a step that could echo it:
RUFLO_HELPERS_SIGNING_SECRET=ruflo-helpers-signing-key RUFLO_HELPERS_SIGNING_PROJECT=ruv-dev
— reuses the existing env-var contract sign-helpers.mjs already supports; no code
change needed in prepare-publish.mjs or sign-helpers.mjs.package.jsons (npm version <input> --no-git-tag-version),
in the mandated order: @claude-flow/cli → claude-flow (root) → ruflo.npm publish --provenance for @claude-flow/cli — Trusted Publishing activates
automatically once npm detects the OIDC context and a matching registered publisher;
no NODE_AUTH_TOKEN anywhere in the workflow. --provenance is free once CI is doing
the publish and produces a public Sigstore-signed build attestation — a supply-chain
improvement beyond what this ADR was originally asked to solve.claude-flow (root) and ruflo.npm dist-tag add × 9 (3 packages × latest/alpha/v3alpha) — script this as a small
loop rather than 9 literal lines.npm view <pkg>@latest version for all three, assert equality with the
input version and with each other — same check the manual runbook already performs,
now enforced as a CI gate instead of an eyeballed step.git tag v<version> main, git push origin v<version>, gh release create — using
the workflow's default GITHUB_TOKEN (already scoped by contents: write), no new
secret needed.The existing local/manual runbook in CLAUDE.md stays documented as a fallback for at least one full release cycle after this workflow is proven, and permanently as a break-glass path for whenever the workflow itself is broken (e.g., mid-migration on a workflow-filename rename — see Consequences).
NPM_TOKEN GitHub secret. Rejected — this is
precisely the option npm's own UI is warning against for CI/CD use; it's long-lived, must be
manually rotated in two places (npmjs.com and GitHub) on any suspected compromise, and has
no expiry unless one is manually set.main. Rejected — this repo's release cadence is
deliberate (semver-bump judgment call, hand-written GitHub release notes, sometimes a linked
gist). Automatic-on-merge would remove a decision point this repo currently wants to keep.
workflow_dispatch preserves it.meta-llm's CD) to reuse the pattern instead.NPM_TOKEN secret class entirely for CI — a short-lived, per-run
token is minted via OIDC exchange and never stored.--provenance becomes essentially free to add once CI does the publish, improving the
supply-chain trust signal for all three packages beyond what this ADR set out to fix.~/.ruflo/helpers-signing.key
local-file workaround documented for the 2026-07-14 incident.prepublishOnly shell-quirk risk class for CI-driven publishes, since
the runner is always ubuntu-latest — the documented Git-Bash-manual-steps fallback stops
being relevant for this path (it remains relevant only for someone still publishing locally
from Windows, which is now explicitly the fallback path, not the primary one)..github/workflows/npm-publish.yml
is ever renamed, moved, or the repo is renamed/transferred, npmjs.com's Trusted Publisher
entries must be updated in lockstep for all three packages or every publish starts failing
auth — an easy step to forget during an unrelated refactor. Document this explicitly in
CLAUDE.md alongside the workflow.npm-publish environment's required-reviewers setting is the only thing standing in
for "a person is running this by hand." If that protection rule is ever weakened or removed,
a compromised or malicious workflow-file change could self-approve a publish. Mitigate with
branch protection on changes to .github/workflows/npm-publish.yml itself, in addition to
the environment gate.meta-llm's existing CD).npm-publish environment with required reviewers is what makes
this at least as strong as today's "an operator runs this by hand" gate, not weaker.roles/secretmanager.secretAccessor on exactly
projects/ruv-dev/secrets/ruflo-helpers-signing-key — verify this at implementation time
rather than copying whatever scope GCP_DEPLOY_SA currently has for meta-llm, which is a
different principal for a different purpose.stdout. sign-helpers.mjs
already consumes it via env vars, not by reading a printed value, so no code change is
needed there — but the CI step that populates those env vars must not itself echo, cat,
or otherwise surface the value. This is exactly the mistake that caused the 2026-07-14 leak
(gcloud secrets versions access writing to stdout, captured into a tool-call transcript);
an authenticated-client-library or --out-file-style fetch avoids that failure mode by
construction..github/workflows/npm-publish.yml per Decision §3.
Gate on workflow_dispatch + the npm-publish environment with required reviewers.npm publish --dry-run in CI to validate the full
build → sign → pack pipeline without touching the registry. Do not publish a real
prerelease version to test — CLAUDE.md explicitly forbids pre-release tags (-alpha.N etc.)
outside an explicit user request, and --dry-run covers everything except the actual
registry write.npm view, GitHub release) matches
what the manual process would have produced.CLAUDE.md to point at this workflow as the primary
path, keeping the manual steps as documented fallback rather than deleting them.v*) trigger be added alongside workflow_dispatch once the flow is
trusted, or should it stay manual-trigger-only indefinitely? (Leaning: stay manual — the
semver-bump decision in CLAUDE.md is explicitly a judgment call, not a mechanical one.)CLAUDE.md → "Publishing to npm" (existing manual runbook, dist-tag matrix, signing-key
handling, Windows prepublishOnly caveat)CLAUDE.md → Cognitum Platform deployment table — existing WIF precedent
(meta-llm's GCP_WIF_PROVIDER + GCP_DEPLOY_SA)CLAUDE.md → 2026-07-14 helpers-signing-key leak incident and its "never let a secret
reach tool output" rule — the direct motivating precedent for the GCP-fetch design in this
ADRv3/@claude-flow/cli/scripts/prepare-publish.mjs, scripts/sign-helpers.mjs — existing
prepublishOnly chain this ADR's CI workflow must reproduce unchanged