From 651ad81b4a151ce03c057b86ffe55935092242b8 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 24 Jul 2026 11:40:09 -0400 Subject: [PATCH] ci(provenance): OIDC trusted publishing + dry-run gate + auto GitHub release Switch the v1.x standalone publish workflow off the long-lived NPM_TOKEN to npm trusted publishing (OIDC via id-token: write), so no npm secret is stored in the repo. Add a dry-run input that defaults to true, gating all three package publishes so an accidental dispatch builds but never reaches the registry. After a successful publish, cut the immutable GitHub Release for the auto-created v tag, idempotently. SURF-726 --- .github/workflows/provenance.yml | 75 ++++++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index 358a7b2d51..fe46cf97b0 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -8,18 +8,27 @@ on: required: false default: 'latest' type: string + dry-run: + description: 'Build everything but do NOT publish, tag, or cut a release. Defaults to true so an accidental dispatch never reaches the registry — set to false for a real release.' + required: false + default: true + type: boolean debug: description: 'Enable debug output' required: false default: '0' type: string - options: - - '0' - - '1' permissions: contents: read +# Serialize publishes per dist-tag. Two concurrent dispatches with the same +# tag would race on `npm publish` (one wins, the other 409s). Don't cancel an +# in-flight publish — a half-published release is worse than a queued one. +concurrency: + group: publish-${{ inputs.dist-tag }} + cancel-in-progress: false + jobs: build: name: Build and Publish @@ -57,6 +66,7 @@ jobs: if [ ! -x "$PNPM_BIN" ]; then mkdir -p "$PNPM_DIR" curl -fsSL -o "$PNPM_BIN" "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${ASSET}" + # shellcheck disable=SC1003 # `tr -d '\\'` strips the leading backslash GNU coreutils prepends to a checksum line when the path has a backslash (Windows RUNNER_TEMP). ACTUAL_SHA256="$( (sha256sum "$PNPM_BIN" 2>/dev/null || shasum -a 256 "$PNPM_BIN") | cut -d' ' -f1 | tr -d '\\')" if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then echo "Checksum mismatch for ${ASSET}!" >&2 @@ -131,6 +141,7 @@ jobs: exit 1 fi curl -fsSL -o "$SFW_BIN" "$DOWNLOAD_URL" + # shellcheck disable=SC1003 # `tr -d '\\'` strips the leading backslash GNU coreutils prepends to a checksum line when the path has a backslash (Windows RUNNER_TEMP). ACTUAL_SHA256="$( (sha256sum "$SFW_BIN" 2>/dev/null || shasum -a 256 "$SFW_BIN") | cut -d' ' -f1 | tr -d '\\')" if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then echo "Checksum mismatch for ${ASSET} (${REPO}@v${SFW_VERSION})!" >&2 @@ -220,32 +231,46 @@ jobs: fi bash src/commands/manifest/scripts/maven-extension/build-jar.sh + # All three publishes use npm trusted publishing (OIDC): no long-lived + # NPM_TOKEN is set, so npm mints a short-lived registry token from the + # `id-token: write` OIDC token at publish time and attaches provenance. + # Each package (`socket`, `@socketsecurity/cli`, + # `@socketsecurity/cli-with-sentry`) must have a matching trusted + # publisher configured on npm for SocketDev/socket-cli + this workflow + # file + the v1.x branch, or the publish 404s on the token exchange. + # + # The publish steps are skipped entirely on a dry run (inputs.dry-run + # defaults to true), so an accidental dispatch builds but never reaches + # the registry. When skipped, publish_socket.outcome is not 'success', + # so the tag + release steps below also skip. - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist - name: Publish socket id: publish_socket + if: ${{ inputs.dry-run == false }} run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" continue-on-error: true env: NPM_DIST_TAG: ${{ inputs.dist-tag }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] SOCKET_CLI_DEBUG: ${{ inputs.debug }} - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_LEGACY_BUILD=1 pnpm run build:dist env: SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" + - name: Publish @socketsecurity/cli (legacy) + if: ${{ inputs.dry-run == false }} + run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" continue-on-error: true env: NPM_DIST_TAG: ${{ inputs.dist-tag }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] SOCKET_CLI_DEBUG: ${{ inputs.debug }} - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_SENTRY_BUILD=1 pnpm run build:dist env: SOCKET_CLI_DEBUG: ${{ inputs.debug }} - - run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" + - name: Publish @socketsecurity/cli-with-sentry + if: ${{ inputs.dry-run == false }} + run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" continue-on-error: true env: NPM_DIST_TAG: ${{ inputs.dist-tag }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] SOCKET_CLI_DEBUG: ${{ inputs.debug }} # Create v git tag at the published commit SHA after a @@ -264,6 +289,7 @@ jobs: # with persist-credentials: true (which would leak it to every later # step including `pnpm install` postinstall scripts). - name: Tag release (idempotent) + id: tag if: steps.publish_socket.outcome == 'success' env: GH_TOKEN: ${{ github.token }} @@ -272,6 +298,10 @@ jobs: PUBLISHED_SHA=$(git rev-parse HEAD) PUBLISHED_VERSION=$(node -p "require('./package.json').version") TAG="v$PUBLISHED_VERSION" + # Emit the tag before any early exit so the release step below can + # consume it on every non-error path (fresh tag AND the same-SHA + # no-op). + echo "tag=$TAG" >> "$GITHUB_OUTPUT" # Look up any existing tag ref. gh api exits non-zero on 404 (tag # absent) and writes the error body to stdout, so branch on the @@ -307,3 +337,32 @@ jobs: -f "ref=refs/tags/$TAG" \ -f "sha=$PUBLISHED_SHA" echo "Created tag $TAG at $PUBLISHED_SHA" + + # Cut the immutable GitHub Release for the tag, idempotently. ORDER + # RULE: the tag + GH release are the FINAL markers of a release — they + # only exist AFTER the npm publish is confirmed live (this step is gated + # on the same publish_socket success as the tag step, and runs after + # it). Uses gh release (gh api under the hood) so GH_TOKEN only lives in + # this step's env, never written to `.git/config`. Skips cleanly if a + # release already exists for the tag so re-runs are safe. + - name: Cut GitHub release (idempotent) + if: steps.publish_socket.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + TAG: ${{ steps.tag.outputs.tag }} + run: | + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + echo "Release $TAG already exists — no-op." + exit 0 + fi + # --verify-tag: refuse to create if the tag ref is somehow missing + # (the tag step above creates it; this guards against a race). + # --generate-notes: auto-populate notes from commits since the last + # release. + gh release create "$TAG" \ + --repo "$REPO" \ + --title "$TAG" \ + --verify-tag \ + --generate-notes + echo "Created GitHub release $TAG"