📦 npm publish #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish to npm registry | |
| # Release process for the v1.x line — three packages from one source | |
| # (socket, @socketsecurity/cli, @socketsecurity/cli-with-sentry): | |
| # | |
| # 1. Between releases the tree carries the next-version hint | |
| # (`X.Y.Z-prerelease` in package.json) and user-facing notes accrue | |
| # under the CHANGELOG's `## [Unreleased]` section. | |
| # 2. The release bump strips the hint to X.Y.Z and promotes [Unreleased] | |
| # to the `## [X.Y.Z]` heading — never a hand-coded version. | |
| # 3. Dispatch this workflow with dry-run=true (the default): builds all | |
| # three variants, uploads nothing, marks nothing. | |
| # 4. Dispatch with dry-run=false + a NON-latest dist-tag (the guard below | |
| # refuses `latest` off the default branch): cuts the v<X.Y.Z> tag + the | |
| # immutable GitHub release — both belong to the `socket` package, | |
| # exactly one of each per run — then STAGES all three packages. | |
| # 5. A human promotes each staged upload (`pnpm stage approve` with web | |
| # 2FA, or the npm web UI); nothing is public until then. | |
| # 6. A stage rejected after the markers BURNS the version: the next | |
| # release is a patch bump, and the burned number is never re-published. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| dist-tag: | |
| description: 'npm dist-tag (latest, next, beta, canary, backport, etc.)' | |
| 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 | |
| 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 | |
| runs-on: ubuntu-latest | |
| # npm's trusted-publisher config pins this GitHub environment name (npm TP | |
| # is PER-PACKAGE, not per-branch: the socket / @socketsecurity/cli / | |
| # @socketsecurity/cli-with-sentry entries point at npm-publish.yml + the | |
| # npm-publish environment). The OIDC token exchange 404s if this job runs | |
| # outside it — so v1.x publishes from the same workflow filename + env as main. | |
| environment: npm-publish | |
| permissions: | |
| # `contents: write` needed to create the v<version> tag via gh api | |
| # at the end of this job. Token is scoped to the dedicated tag step | |
| # via GH_TOKEN env; never persisted in `.git/config` (checkout keeps | |
| # persist-credentials: false so build/install steps can't reach it). | |
| contents: write | |
| id-token: write # NPM trusted publishing via OIDC | |
| steps: | |
| # npm trusted publishing authorizes on repository + workflow filename + | |
| # GitHub environment. It does NOT pin a branch. The `npm-publish` | |
| # environment's deployment-branch policy (main + v1.x) is the outer | |
| # gate; this guard is the in-repo half: `latest` may only be published | |
| # from the default branch, so a v1.x dispatch must pick an explicit | |
| # non-latest dist-tag (next, beta, canary, backport, ...). Dry runs | |
| # pass regardless of dist-tag — they upload nothing, and a | |
| # default-input dry run (dist-tag defaults to latest) must stay green. | |
| - name: Guard the latest dist-tag to the default branch | |
| if: ${{ inputs.dry-run == false && inputs.dist-tag == 'latest' }} | |
| env: | |
| REF: ${{ github.ref }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| if [ "$REF" != "refs/heads/$DEFAULT_BRANCH" ]; then | |
| echo "Refusing to publish dist-tag 'latest' from $REF." >&2 | |
| echo "Only refs/heads/$DEFAULT_BRANCH may publish 'latest'." >&2 | |
| echo "Re-dispatch from the default branch, or pick a non-latest dist-tag." >&2 | |
| exit 1 | |
| fi | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-20) | |
| with: | |
| persist-credentials: false | |
| # A version carrying a prerelease suffix is the committed NEXT-version | |
| # hint (X.Y.Z-prerelease — the release tooling consumes it), not a | |
| # releasable artifact: the bump that strips the hint and promotes the | |
| # CHANGELOG's [Unreleased] section must land first. Fail closed so a | |
| # dispatch on a hint-carrying tree can never reach the registry. | |
| - name: Refuse a real publish on a prerelease-hint version | |
| if: ${{ inputs.dry-run == false }} | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| case "$VERSION" in | |
| *-*) | |
| echo "package.json version is '$VERSION' — a prerelease-hint version, not a releasable one." >&2 | |
| echo "Wanted: a bare X.Y.Z (run the release bump: strip the hint, promote [Unreleased])." >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "Version $VERSION is release-shaped." | |
| - name: Install pnpm | |
| shell: bash | |
| run: | # zizmor: ignore[github-env] | |
| # pnpm 11 is required for `pnpm stage publish` (the staged upload | |
| # the per-package trusted-publisher grants allow) and ships tar.gz | |
| # release assets (a `pnpm` binary + its dist/ tree). The job only | |
| # runs ubuntu-latest, so only the Linux assets are pinned. | |
| PNPM_VERSION="11.17.0" | |
| PNPM_DIR="${RUNNER_TEMP:-/tmp}/pnpm-bin" | |
| KERNEL="$(uname -s | cut -d- -f1)" | |
| ARCH="$(uname -m)" | |
| case "${KERNEL}-${ARCH}" in | |
| Linux-x86_64) ASSET="pnpm-linux-x64.tar.gz" ; EXPECTED_SHA256="bdb1db01bf0f757495405a59a09c5c287f315889dc98d3b14bc374b9fe43a0bf" ;; | |
| Linux-aarch64) ASSET="pnpm-linux-arm64.tar.gz" ; EXPECTED_SHA256="730d17de742a3efbb020ba91d7acfc0456c6ba6ad1cd8eb49f4c229fe9f504d3" ;; | |
| *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; | |
| esac | |
| PNPM_BIN="$PNPM_DIR/pnpm" | |
| if [ ! -x "$PNPM_BIN" ]; then | |
| mkdir -p "$PNPM_DIR" | |
| curl -fsSL -o "$PNPM_DIR/$ASSET" "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${ASSET}" | |
| ACTUAL_SHA256="$( (sha256sum "$PNPM_DIR/$ASSET" 2>/dev/null || shasum -a 256 "$PNPM_DIR/$ASSET") | cut -d' ' -f1)" | |
| if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then | |
| echo "Checksum mismatch for ${ASSET}!" >&2 | |
| echo " Expected: ${EXPECTED_SHA256}" >&2 | |
| echo " Actual: ${ACTUAL_SHA256}" >&2 | |
| rm -f "$PNPM_DIR/$ASSET" | |
| exit 1 | |
| fi | |
| tar -xzf "$PNPM_DIR/$ASSET" -C "$PNPM_DIR" | |
| chmod +x "$PNPM_BIN" | |
| fi | |
| echo "$PNPM_DIR" >> "${GITHUB_PATH:-/dev/null}" | |
| # Prove the pinned pnpm owns `stage` BEFORE anything else runs — from a | |
| # neutral cwd so the packageManager delegation cannot swap it out. Runs | |
| # on dry runs too, so the weekly validation catches a broken stage | |
| # toolchain without burning a version. | |
| - name: Verify the stage command resolves | |
| working-directory: ${{ runner.temp }} | |
| run: | | |
| pnpm --version | |
| pnpm stage --help > /dev/null | |
| echo "pnpm stage resolves." | |
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: 25.9.0 | |
| cache: pnpm | |
| registry-url: https://registry.npmjs.org | |
| scope: '@socketsecurity' | |
| - name: Download sfw | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SOCKET_API_KEY: ${{ secrets.SOCKET_API_KEY }} # zizmor: ignore[secrets-outside-env] | |
| run: | # zizmor: ignore[github-env] | |
| # Pinned version + per-platform checksum pairs. Bumping a tool | |
| # requires updating the matching version AND every platform's | |
| # SHA256 in the same commit, otherwise the download / verify | |
| # steps will diverge. | |
| SFW_FREE_VERSION="1.7.2" | |
| SFW_ENTERPRISE_VERSION="1.7.2" | |
| SFW_DIR="${RUNNER_TEMP:-/tmp}/sfw-bin" | |
| KERNEL="$(uname -s | cut -d- -f1)" | |
| ARCH="$(uname -m)" | |
| USE_ENTERPRISE=false | |
| [ -n "$SOCKET_API_KEY" ] && USE_ENTERPRISE=true | |
| if [ "$USE_ENTERPRISE" = "true" ]; then | |
| REPO="SocketDev/firewall-release" | |
| SFW_VERSION="$SFW_ENTERPRISE_VERSION" | |
| case "${KERNEL}-${ARCH}" in | |
| Linux-x86_64) ASSET="sfw-linux-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="4482b52e6367bd4610519bfd57a104d5907ec87d5399142ed3bb3d222de1f33d" ;; | |
| Linux-aarch64) ASSET="sfw-linux-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="c24a79c27e1a01a59b7a160c165930ae029816c72b141fcfcdb2f73e0774898a" ;; | |
| Darwin-x86_64) ASSET="sfw-macos-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="da252d2a9a5d0edb271bb771e0d01b9cd6fa1635b6d765f61efd61edb6739f12" ;; | |
| Darwin-arm64) ASSET="sfw-macos-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="b1cdc3bdbd2a3161247bd5cc215eb3c44a90b87fe0b800a33889a14f61bb0d6d" ;; | |
| MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="sfw-windows-x86_64.exe" ; SFW_BIN="$SFW_DIR/sfw.exe" ; EXPECTED_SHA256="e52ad806a1c41b440f04098eb1c7e407845f03f5740a6a79006ba6fd172056ec" ;; | |
| *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; | |
| esac | |
| else | |
| REPO="SocketDev/sfw-free" | |
| SFW_VERSION="$SFW_FREE_VERSION" | |
| case "${KERNEL}-${ARCH}" in | |
| Linux-x86_64) ASSET="sfw-free-linux-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="93e2d9dfa244b82a74e014dc26b1c6af18b4adec20f35254378943db5fe91411" ;; | |
| Linux-aarch64) ASSET="sfw-free-linux-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="84a045e4e1bb320cc5c0d3929f02e53f199398b5be0637e8846d02d9ef0027b1" ;; | |
| Darwin-x86_64) ASSET="sfw-free-macos-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="a5427d479d440f08e3789fa191ba57599be64997196daf42e67d964fec0382b4" ;; | |
| Darwin-arm64) ASSET="sfw-free-macos-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="248fb588e1e1a27e7192f7b079f739fc29a9de61f0bad7e90928363022dc5643" ;; | |
| MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="sfw-free-windows-x86_64.exe" ; SFW_BIN="$SFW_DIR/sfw.exe" ; EXPECTED_SHA256="6d333b4cac9d7c5712e2e99677ca634ac8a3020d550c6308312c60bea97f0a28" ;; | |
| *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; | |
| esac | |
| fi | |
| if [ ! -x "$SFW_BIN" ]; then | |
| mkdir -p "$SFW_DIR" | |
| DOWNLOAD_URL="$(gh api "repos/${REPO}/releases/tags/v${SFW_VERSION}" \ | |
| --jq ".assets[] | select(.name == \"$ASSET\") | .browser_download_url")" | |
| if [ -z "$DOWNLOAD_URL" ]; then | |
| echo "Asset ${ASSET} not found in ${REPO}@v${SFW_VERSION}" >&2 | |
| 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 | |
| echo " Expected: ${EXPECTED_SHA256}" >&2 | |
| echo " Actual: ${ACTUAL_SHA256}" >&2 | |
| rm -f "$SFW_BIN" | |
| exit 1 | |
| fi | |
| chmod +x "$SFW_BIN" | |
| fi | |
| echo "SFW_BIN=$SFW_BIN" >> "${GITHUB_ENV:-/dev/null}" | |
| echo "SFW_IS_ENTERPRISE=$USE_ENTERPRISE" >> "${GITHUB_ENV:-/dev/null}" | |
| if [ "$USE_ENTERPRISE" = "true" ]; then | |
| echo "SOCKET_API_KEY=$SOCKET_API_KEY" >> "${GITHUB_ENV:-/dev/null}" | |
| fi | |
| - name: Create sfw shims | |
| shell: bash | |
| run: | # zizmor: ignore[github-env] | |
| SHIM_DIR="${RUNNER_TEMP:-/tmp}/sfw-shim" | |
| rm -rf "$SHIM_DIR" | |
| mkdir -p "$SHIM_DIR" | |
| IS_WINDOWS=false | |
| [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]] && IS_WINDOWS=true | |
| msys_to_win_path() { | |
| if $IS_WINDOWS && [[ "$1" =~ ^/([a-zA-Z])/(.*) ]]; then | |
| echo "${BASH_REMATCH[1]^^}:\\${BASH_REMATCH[2]//\//\\}" | |
| else | |
| echo "$1" | |
| fi | |
| } | |
| strip_shim_dir() { echo "$PATH" | tr ':' '\n' | grep -vxF "$SHIM_DIR" | paste -sd: -; } | |
| CLEAN_PATH="$(strip_shim_dir)" | |
| # Wrapper mode ecosystems (sfw-free): | |
| # JavaScript/TypeScript: npm, yarn, pnpm | |
| # Python: pip, uv | |
| # Rust: cargo | |
| # https://github.com/SocketDev/sfw-free?tab=readme-ov-file#supported-package-managers | |
| # | |
| # Additional wrapper mode ecosystems (sfw-enterprise): | |
| # Ruby: gem, bundler | |
| # .NET: nuget | |
| # Go: go (Linux only) | |
| # https://github.com/SocketDev/firewall-release/wiki#support-matrix | |
| SSL_WORKAROUND="" | |
| SHIM_CMDS="npm yarn pnpm pip uv cargo" | |
| if [ "$SFW_IS_ENTERPRISE" = "true" ]; then | |
| SHIM_CMDS="npm yarn pnpm pip uv cargo gem bundler nuget" | |
| # Go wrapper mode is only supported on Linux. | |
| [[ "$OSTYPE" == linux* ]] && SHIM_CMDS="$SHIM_CMDS go" | |
| else | |
| SSL_WORKAROUND='export GIT_SSL_NO_VERIFY=true # Workaround: sfw-free does not yet set GIT_SSL_CAINFO.' | |
| fi | |
| for CMD in $SHIM_CMDS; do | |
| REAL="$(PATH="$CLEAN_PATH" command -v "$CMD" 2>/dev/null || true)" | |
| [ -z "$REAL" ] && continue | |
| REAL="$(msys_to_win_path "$REAL")" | |
| SHIM_LINES=('#!/bin/bash' "export PATH=\"\$(echo \"\$PATH\" | tr ':' '\n' | grep -vxF '${SHIM_DIR}' | paste -sd: -)\"") | |
| [ -n "$SSL_WORKAROUND" ] && SHIM_LINES+=("$SSL_WORKAROUND") | |
| SHIM_LINES+=("exec \"${SFW_BIN}\" \"${REAL}\" \"\$@\"") | |
| printf '%s\n' "${SHIM_LINES[@]}" > "$SHIM_DIR/$CMD" | |
| chmod +x "$SHIM_DIR/$CMD" | |
| if $IS_WINDOWS; then | |
| printf '@echo off\r\nset "PATH=;%%PATH%%;"\r\nset "PATH=%%PATH:;%s;=;%%"\r\nset "PATH=%%PATH:~1,-1%%"\r\n"%s" "%s" %%*\r\n' \ | |
| "$SHIM_DIR" "$SFW_BIN" "$REAL" > "$SHIM_DIR/$CMD.cmd" | |
| fi | |
| done | |
| echo "$SHIM_DIR" >> "${GITHUB_PATH:-/dev/null}" | |
| echo "SFW_SHIM_DIR=$SHIM_DIR" >> "${GITHUB_ENV:-/dev/null}" | |
| - name: Install dependencies | |
| run: pnpm install --loglevel error | |
| # Compile the Maven manifest extension jar so the dist build bundles it | |
| # into dist/manifest-scripts (the jar is never committed; it ships only in | |
| # the published package). Invoke build-jar.sh directly, NOT via `pnpm run`: | |
| # Socket Firewall wraps the package managers (npm/pnpm/...) it shims, so a | |
| # `pnpm run` would route the Maven wrapper's download through sfw, which | |
| # fails on the non-package fetch. Running bash directly keeps the Maven | |
| # download outside the shimmed process tree. The org action allowlist forbids | |
| # actions/setup-java, so use a JDK pre-installed on the runner image | |
| # (JAVA_HOME_17_X64), falling back to the runner's default `java`. | |
| - name: Build Maven manifest extension jar | |
| run: | | |
| if [ -n "${JAVA_HOME_17_X64:-}" ]; then | |
| export JAVA_HOME="$JAVA_HOME_17_X64" | |
| fi | |
| bash src/commands/manifest/scripts/maven-extension/build-jar.sh | |
| # All three uploads are STAGED (`pnpm stage publish`): the tarball + | |
| # provenance land in npm's staging area and NOTHING goes public until a | |
| # human promotes each stage (`pnpm stage approve` with web 2FA, or the | |
| # npm web UI). The per-package trusted publishers for `socket`, | |
| # `@socketsecurity/cli`, and `@socketsecurity/cli-with-sentry` allow | |
| # "stage publish" ONLY — a direct `npm publish` dies at the OIDC token | |
| # exchange. Each package pins SocketDev/socket-cli + this workflow file | |
| # (npm-publish.yml) + the npm-publish environment. | |
| # | |
| # ORDER RULE (markers first): the v<version> tag + immutable GitHub | |
| # release are cut BEFORE the staged uploads so the uploads' provenance | |
| # binds markers that exist. Exactly ONE tag + ONE release per run, and | |
| # they belong to the `socket` package — the tag step reads the version | |
| # from the socket-built manifest right after the socket build, and the | |
| # two variants ride the same version without a tag or release of their | |
| # own. The trade is deliberate and is the burn | |
| # rule: a stage rejected after the markers BURNS the version — the next | |
| # release is a patch bump, never a re-publish of the burned number (the | |
| # tag step's different-SHA hard-fail enforces exactly that; a same-SHA | |
| # re-run is a no-op tag + a fresh stage attempt). | |
| # | |
| # The tag / release / stage steps all skip on a dry run (inputs.dry-run | |
| # defaults to true), so an accidental dispatch builds but never marks or | |
| # uploads anything. | |
| - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist | |
| # Create the v<version> git tag at this commit, idempotently, BEFORE any | |
| # upload (ORDER RULE above). GitHub Release Immutability ("Disallow | |
| # assets and tags from being modified once a release is published") | |
| # freezes tags once bound to a Release, so: | |
| # - existing tag at same SHA → no-op | |
| # - existing tag at different SHA → hard-fail (the burn ratchet: a | |
| # burned version number can never be re-tagged from new content) | |
| # | |
| # Uses gh api (not `git push`) so the token only lives in this step's | |
| # env, never written to `.git/config` by an earlier `actions/checkout` | |
| # with persist-credentials: true (which would leak it to every later | |
| # step including `pnpm install` postinstall scripts). | |
| - name: Tag release (idempotent) | |
| id: tag | |
| if: ${{ inputs.dry-run == false }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| 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 | |
| # exit code — never on whether stdout is empty. EXISTING_JSON is | |
| # only valid JSON when the call succeeded. | |
| if EXISTING_JSON=$(gh api "repos/$REPO/git/ref/tags/$TAG" 2>/dev/null); then | |
| # The ref's object is either a commit (lightweight tag) or a tag | |
| # object (annotated/signed tag, e.g. the hand-created `git tag -s` | |
| # tags). For an annotated tag, object.sha is the tag-object SHA, | |
| # not the commit — dereference it via git/tags to get the commit | |
| # the tag actually points at before comparing. | |
| REF_TYPE=$(echo "$EXISTING_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).object.type") | |
| REF_OBJECT_SHA=$(echo "$EXISTING_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).object.sha") | |
| if [ "$REF_TYPE" = "tag" ]; then | |
| EXISTING_SHA=$(gh api "repos/$REPO/git/tags/$REF_OBJECT_SHA" --jq '.object.sha') | |
| else | |
| EXISTING_SHA="$REF_OBJECT_SHA" | |
| fi | |
| if [ "$EXISTING_SHA" = "$PUBLISHED_SHA" ]; then | |
| echo "Tag $TAG already exists at $PUBLISHED_SHA — no-op." | |
| exit 0 | |
| fi | |
| echo "::error::Tag $TAG exists at $EXISTING_SHA but publish SHA is $PUBLISHED_SHA." | |
| echo "::error::Release immutability is enabled; this requires manual recovery:" | |
| echo "::error:: 1. Delete any GitHub Release tied to $TAG" | |
| echo "::error:: 2. Delete the tag via the API" | |
| echo "::error:: 3. Re-run this workflow" | |
| exit 1 | |
| fi | |
| gh api "repos/$REPO/git/refs" \ | |
| -X POST \ | |
| -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, BEFORE | |
| # the staged uploads (ORDER RULE above). Uses gh release (gh api under | |
| # the hood) so GH_TOKEN only lives in this step's env, never written to | |
| # `.git/config`. Create-as-draft then publish: immutable releases attest | |
| # the locked asset set at publish time, so the release goes live in a | |
| # separate `--draft=false` flip (3-step pattern; no assets ride this | |
| # release, notes only). The flip carries `--latest=false`: v1.x is the | |
| # maintenance line, so its releases never take the repo's Latest badge | |
| # from the 2.x line on main. Re-runs skip a published release and flip | |
| # a stranded draft live. | |
| - name: Cut GitHub release (idempotent) | |
| if: ${{ inputs.dry-run == false }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| TAG: ${{ steps.tag.outputs.tag }} | |
| run: | | |
| IS_DRAFT=$(gh release view "$TAG" --repo "$REPO" --json isDraft --jq '.isDraft' 2>/dev/null || echo "absent") | |
| if [ "$IS_DRAFT" = "false" ]; then | |
| echo "Release $TAG already exists — no-op." | |
| exit 0 | |
| fi | |
| if [ "$IS_DRAFT" = "absent" ]; then | |
| # --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 \ | |
| --draft | |
| fi | |
| gh release edit "$TAG" --repo "$REPO" --draft=false --latest=false | |
| echo "Published GitHub release $TAG" | |
| # --no-git-checks: the INLINED_* builds rewrite package.json + dist/ in | |
| # the checkout, so pnpm's clean-tree publish check would always refuse; | |
| # the tree state is machine-built on a pinned SHA, not hand-edited. | |
| # --ignore-scripts: each variant's dist is built explicitly above, so no | |
| # lifecycle script may run (or mutate anything) at pack/upload time. | |
| # The stage steps run from runner.temp with the checkout passed as the | |
| # directory argument: pnpm self-delegates to the packageManager pin | |
| # (pnpm@10.33.0, which has no `stage` command) whenever its cwd sits | |
| # under a package.json, and no env or npmrc knob disables that in | |
| # pnpm 11 — a neutral cwd keeps the workflow-pinned pnpm 11 in charge | |
| # (the v1.1.148 and v1.1.149 burns). Install/build steps keep the | |
| # delegate on purpose: they match v1.x CI exactly. | |
| # Both flags mirror the fleet staged-publish runner | |
| # (scripts/fleet/publish-infra/npm/staged.mts on main). | |
| - name: Stage socket | |
| if: ${{ inputs.dry-run == false }} | |
| working-directory: ${{ runner.temp }} | |
| run: pnpm stage publish "$GITHUB_WORKSPACE" --provenance --access public --tag "${NPM_DIST_TAG}" --no-git-checks --ignore-scripts | |
| env: | |
| NPM_DIST_TAG: ${{ inputs.dist-tag }} | |
| 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 }} | |
| - name: Stage @socketsecurity/cli (legacy) | |
| if: ${{ inputs.dry-run == false }} | |
| working-directory: ${{ runner.temp }} | |
| run: pnpm stage publish "$GITHUB_WORKSPACE" --provenance --access public --tag "${NPM_DIST_TAG}" --no-git-checks --ignore-scripts | |
| env: | |
| NPM_DIST_TAG: ${{ inputs.dist-tag }} | |
| 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 }} | |
| - name: Stage @socketsecurity/cli-with-sentry | |
| if: ${{ inputs.dry-run == false }} | |
| working-directory: ${{ runner.temp }} | |
| run: pnpm stage publish "$GITHUB_WORKSPACE" --provenance --access public --tag "${NPM_DIST_TAG}" --no-git-checks --ignore-scripts | |
| env: | |
| NPM_DIST_TAG: ${{ inputs.dist-tag }} | |
| SOCKET_CLI_DEBUG: ${{ inputs.debug }} |