|
| 1 | +name: Publish to npm registry |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + inputs: |
| 6 | + dist-tag: |
| 7 | + description: 'npm dist-tag (latest, next, beta, canary, backport, etc.)' |
| 8 | + required: false |
| 9 | + default: 'latest' |
| 10 | + type: string |
| 11 | + debug: |
| 12 | + description: 'Enable debug output' |
| 13 | + required: false |
| 14 | + default: '0' |
| 15 | + type: string |
| 16 | + options: |
| 17 | + - '0' |
| 18 | + - '1' |
| 19 | + |
| 20 | +permissions: |
| 21 | + contents: read |
| 22 | + |
| 23 | +jobs: |
| 24 | + build: |
| 25 | + name: Build and Publish |
| 26 | + runs-on: ubuntu-latest |
| 27 | + |
| 28 | + permissions: |
| 29 | + # `contents: write` needed to create the v<version> tag via gh api |
| 30 | + # at the end of this job. Token is scoped to the dedicated tag step |
| 31 | + # via GH_TOKEN env; never persisted in `.git/config` (checkout keeps |
| 32 | + # persist-credentials: false so build/install steps can't reach it). |
| 33 | + contents: write |
| 34 | + id-token: write # NPM trusted publishing via OIDC |
| 35 | + |
| 36 | + steps: |
| 37 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-20) |
| 38 | + with: |
| 39 | + persist-credentials: false |
| 40 | + |
| 41 | + - name: Install pnpm |
| 42 | + shell: bash |
| 43 | + run: | # zizmor: ignore[github-env] |
| 44 | + PNPM_VERSION="10.33.0" |
| 45 | + PNPM_DIR="${RUNNER_TEMP:-/tmp}/pnpm-bin" |
| 46 | + KERNEL="$(uname -s | cut -d- -f1)" |
| 47 | + ARCH="$(uname -m)" |
| 48 | + case "${KERNEL}-${ARCH}" in |
| 49 | + Linux-x86_64) ASSET="pnpm-linux-x64" ; EXPECTED_SHA256="8d4e8f7d778e8ac482022e2577011706a872542f6f6f233e795a4d9f978ea8b5" ;; |
| 50 | + Linux-aarch64) ASSET="pnpm-linux-arm64" ; EXPECTED_SHA256="06755ad2817548b84317d857d5c8003dc6e9e28416a3ea7467256c49ab400d48" ;; |
| 51 | + Darwin-x86_64) ASSET="pnpm-macos-x64" ; EXPECTED_SHA256="c31e29554b0e3f4e03f4617195c949595e4dca36085922003de4896c3ca4057d" ;; |
| 52 | + Darwin-arm64) ASSET="pnpm-macos-arm64" ; EXPECTED_SHA256="ed8a1f140f4de457b01ebe0be3ae28e9a7e28863315dcd53d22ff1e5a32d63ae" ;; |
| 53 | + MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="pnpm-win-x64.exe" ; EXPECTED_SHA256="afc96009dc39fe23a835d65192049e6a995f342496b175585dc2beda7d42d33f" ;; |
| 54 | + *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; |
| 55 | + esac |
| 56 | + PNPM_BIN="$PNPM_DIR/$ASSET" |
| 57 | + if [ ! -x "$PNPM_BIN" ]; then |
| 58 | + mkdir -p "$PNPM_DIR" |
| 59 | + curl -fsSL -o "$PNPM_BIN" "https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${ASSET}" |
| 60 | + ACTUAL_SHA256="$( (sha256sum "$PNPM_BIN" 2>/dev/null || shasum -a 256 "$PNPM_BIN") | cut -d' ' -f1 | tr -d '\\')" |
| 61 | + if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then |
| 62 | + echo "Checksum mismatch for ${ASSET}!" >&2 |
| 63 | + echo " Expected: ${EXPECTED_SHA256}" >&2 |
| 64 | + echo " Actual: ${ACTUAL_SHA256}" >&2 |
| 65 | + rm -f "$PNPM_BIN" |
| 66 | + exit 1 |
| 67 | + fi |
| 68 | + chmod +x "$PNPM_BIN" |
| 69 | + # Create pnpm alias. Windows needs a .exe copy; Unix uses a symlink. |
| 70 | + if [[ "$ASSET" == *.exe ]]; then |
| 71 | + cp "$PNPM_BIN" "$PNPM_DIR/pnpm.exe" |
| 72 | + else |
| 73 | + ln -sf "$PNPM_BIN" "$PNPM_DIR/pnpm" |
| 74 | + fi |
| 75 | + fi |
| 76 | + echo "$PNPM_DIR" >> "${GITHUB_PATH:-/dev/null}" |
| 77 | +
|
| 78 | + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 |
| 79 | + with: |
| 80 | + node-version: 25.9.0 |
| 81 | + cache: pnpm |
| 82 | + registry-url: https://registry.npmjs.org |
| 83 | + scope: '@socketsecurity' |
| 84 | + |
| 85 | + - name: Download sfw |
| 86 | + shell: bash |
| 87 | + env: |
| 88 | + GH_TOKEN: ${{ github.token }} |
| 89 | + SOCKET_API_KEY: ${{ secrets.SOCKET_API_KEY }} # zizmor: ignore[secrets-outside-env] |
| 90 | + run: | # zizmor: ignore[github-env] |
| 91 | + # Pinned version + per-platform checksum pairs. Bumping a tool |
| 92 | + # requires updating the matching version AND every platform's |
| 93 | + # SHA256 in the same commit, otherwise the download / verify |
| 94 | + # steps will diverge. |
| 95 | + SFW_FREE_VERSION="1.7.2" |
| 96 | + SFW_ENTERPRISE_VERSION="1.7.2" |
| 97 | + SFW_DIR="${RUNNER_TEMP:-/tmp}/sfw-bin" |
| 98 | + KERNEL="$(uname -s | cut -d- -f1)" |
| 99 | + ARCH="$(uname -m)" |
| 100 | + USE_ENTERPRISE=false |
| 101 | + [ -n "$SOCKET_API_KEY" ] && USE_ENTERPRISE=true |
| 102 | + if [ "$USE_ENTERPRISE" = "true" ]; then |
| 103 | + REPO="SocketDev/firewall-release" |
| 104 | + SFW_VERSION="$SFW_ENTERPRISE_VERSION" |
| 105 | + case "${KERNEL}-${ARCH}" in |
| 106 | + Linux-x86_64) ASSET="sfw-linux-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="4482b52e6367bd4610519bfd57a104d5907ec87d5399142ed3bb3d222de1f33d" ;; |
| 107 | + Linux-aarch64) ASSET="sfw-linux-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="c24a79c27e1a01a59b7a160c165930ae029816c72b141fcfcdb2f73e0774898a" ;; |
| 108 | + Darwin-x86_64) ASSET="sfw-macos-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="da252d2a9a5d0edb271bb771e0d01b9cd6fa1635b6d765f61efd61edb6739f12" ;; |
| 109 | + Darwin-arm64) ASSET="sfw-macos-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="b1cdc3bdbd2a3161247bd5cc215eb3c44a90b87fe0b800a33889a14f61bb0d6d" ;; |
| 110 | + MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="sfw-windows-x86_64.exe" ; SFW_BIN="$SFW_DIR/sfw.exe" ; EXPECTED_SHA256="e52ad806a1c41b440f04098eb1c7e407845f03f5740a6a79006ba6fd172056ec" ;; |
| 111 | + *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; |
| 112 | + esac |
| 113 | + else |
| 114 | + REPO="SocketDev/sfw-free" |
| 115 | + SFW_VERSION="$SFW_FREE_VERSION" |
| 116 | + case "${KERNEL}-${ARCH}" in |
| 117 | + Linux-x86_64) ASSET="sfw-free-linux-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="93e2d9dfa244b82a74e014dc26b1c6af18b4adec20f35254378943db5fe91411" ;; |
| 118 | + Linux-aarch64) ASSET="sfw-free-linux-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="84a045e4e1bb320cc5c0d3929f02e53f199398b5be0637e8846d02d9ef0027b1" ;; |
| 119 | + Darwin-x86_64) ASSET="sfw-free-macos-x86_64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="a5427d479d440f08e3789fa191ba57599be64997196daf42e67d964fec0382b4" ;; |
| 120 | + Darwin-arm64) ASSET="sfw-free-macos-arm64" ; SFW_BIN="$SFW_DIR/sfw" ; EXPECTED_SHA256="248fb588e1e1a27e7192f7b079f739fc29a9de61f0bad7e90928363022dc5643" ;; |
| 121 | + MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="sfw-free-windows-x86_64.exe" ; SFW_BIN="$SFW_DIR/sfw.exe" ; EXPECTED_SHA256="6d333b4cac9d7c5712e2e99677ca634ac8a3020d550c6308312c60bea97f0a28" ;; |
| 122 | + *) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;; |
| 123 | + esac |
| 124 | + fi |
| 125 | + if [ ! -x "$SFW_BIN" ]; then |
| 126 | + mkdir -p "$SFW_DIR" |
| 127 | + DOWNLOAD_URL="$(gh api "repos/${REPO}/releases/tags/v${SFW_VERSION}" \ |
| 128 | + --jq ".assets[] | select(.name == \"$ASSET\") | .browser_download_url")" |
| 129 | + if [ -z "$DOWNLOAD_URL" ]; then |
| 130 | + echo "Asset ${ASSET} not found in ${REPO}@v${SFW_VERSION}" >&2 |
| 131 | + exit 1 |
| 132 | + fi |
| 133 | + curl -fsSL -o "$SFW_BIN" "$DOWNLOAD_URL" |
| 134 | + ACTUAL_SHA256="$( (sha256sum "$SFW_BIN" 2>/dev/null || shasum -a 256 "$SFW_BIN") | cut -d' ' -f1 | tr -d '\\')" |
| 135 | + if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then |
| 136 | + echo "Checksum mismatch for ${ASSET} (${REPO}@v${SFW_VERSION})!" >&2 |
| 137 | + echo " Expected: ${EXPECTED_SHA256}" >&2 |
| 138 | + echo " Actual: ${ACTUAL_SHA256}" >&2 |
| 139 | + rm -f "$SFW_BIN" |
| 140 | + exit 1 |
| 141 | + fi |
| 142 | + chmod +x "$SFW_BIN" |
| 143 | + fi |
| 144 | + echo "SFW_BIN=$SFW_BIN" >> "${GITHUB_ENV:-/dev/null}" |
| 145 | + echo "SFW_IS_ENTERPRISE=$USE_ENTERPRISE" >> "${GITHUB_ENV:-/dev/null}" |
| 146 | + if [ "$USE_ENTERPRISE" = "true" ]; then |
| 147 | + echo "SOCKET_API_KEY=$SOCKET_API_KEY" >> "${GITHUB_ENV:-/dev/null}" |
| 148 | + fi |
| 149 | +
|
| 150 | + - name: Create sfw shims |
| 151 | + shell: bash |
| 152 | + run: | # zizmor: ignore[github-env] |
| 153 | + SHIM_DIR="${RUNNER_TEMP:-/tmp}/sfw-shim" |
| 154 | + rm -rf "$SHIM_DIR" |
| 155 | + mkdir -p "$SHIM_DIR" |
| 156 | + IS_WINDOWS=false |
| 157 | + [[ "$OSTYPE" == msys* || "$OSTYPE" == cygwin* ]] && IS_WINDOWS=true |
| 158 | + msys_to_win_path() { |
| 159 | + if $IS_WINDOWS && [[ "$1" =~ ^/([a-zA-Z])/(.*) ]]; then |
| 160 | + echo "${BASH_REMATCH[1]^^}:\\${BASH_REMATCH[2]//\//\\}" |
| 161 | + else |
| 162 | + echo "$1" |
| 163 | + fi |
| 164 | + } |
| 165 | + strip_shim_dir() { echo "$PATH" | tr ':' '\n' | grep -vxF "$SHIM_DIR" | paste -sd: -; } |
| 166 | + CLEAN_PATH="$(strip_shim_dir)" |
| 167 | + # Wrapper mode ecosystems (sfw-free): |
| 168 | + # JavaScript/TypeScript: npm, yarn, pnpm |
| 169 | + # Python: pip, uv |
| 170 | + # Rust: cargo |
| 171 | + # https://github.com/SocketDev/sfw-free?tab=readme-ov-file#supported-package-managers |
| 172 | + # |
| 173 | + # Additional wrapper mode ecosystems (sfw-enterprise): |
| 174 | + # Ruby: gem, bundler |
| 175 | + # .NET: nuget |
| 176 | + # Go: go (Linux only) |
| 177 | + # https://github.com/SocketDev/firewall-release/wiki#support-matrix |
| 178 | + SSL_WORKAROUND="" |
| 179 | + SHIM_CMDS="npm yarn pnpm pip uv cargo" |
| 180 | + if [ "$SFW_IS_ENTERPRISE" = "true" ]; then |
| 181 | + SHIM_CMDS="npm yarn pnpm pip uv cargo gem bundler nuget" |
| 182 | + # Go wrapper mode is only supported on Linux. |
| 183 | + [[ "$OSTYPE" == linux* ]] && SHIM_CMDS="$SHIM_CMDS go" |
| 184 | + else |
| 185 | + SSL_WORKAROUND='export GIT_SSL_NO_VERIFY=true # Workaround: sfw-free does not yet set GIT_SSL_CAINFO.' |
| 186 | + fi |
| 187 | + for CMD in $SHIM_CMDS; do |
| 188 | + REAL="$(PATH="$CLEAN_PATH" command -v "$CMD" 2>/dev/null || true)" |
| 189 | + [ -z "$REAL" ] && continue |
| 190 | + REAL="$(msys_to_win_path "$REAL")" |
| 191 | + SHIM_LINES=('#!/bin/bash' "export PATH=\"\$(echo \"\$PATH\" | tr ':' '\n' | grep -vxF '${SHIM_DIR}' | paste -sd: -)\"") |
| 192 | + [ -n "$SSL_WORKAROUND" ] && SHIM_LINES+=("$SSL_WORKAROUND") |
| 193 | + SHIM_LINES+=("exec \"${SFW_BIN}\" \"${REAL}\" \"\$@\"") |
| 194 | + printf '%s\n' "${SHIM_LINES[@]}" > "$SHIM_DIR/$CMD" |
| 195 | + chmod +x "$SHIM_DIR/$CMD" |
| 196 | + if $IS_WINDOWS; then |
| 197 | + printf '@echo off\r\nset "PATH=;%%PATH%%;"\r\nset "PATH=%%PATH:;%s;=;%%"\r\nset "PATH=%%PATH:~1,-1%%"\r\n"%s" "%s" %%*\r\n' \ |
| 198 | + "$SHIM_DIR" "$SFW_BIN" "$REAL" > "$SHIM_DIR/$CMD.cmd" |
| 199 | + fi |
| 200 | + done |
| 201 | + echo "$SHIM_DIR" >> "${GITHUB_PATH:-/dev/null}" |
| 202 | + echo "SFW_SHIM_DIR=$SHIM_DIR" >> "${GITHUB_ENV:-/dev/null}" |
| 203 | +
|
| 204 | + - name: Install dependencies |
| 205 | + run: pnpm install --loglevel error |
| 206 | + |
| 207 | + # Compile the Maven manifest extension jar so the dist build bundles it |
| 208 | + # into dist/manifest-scripts (the jar is never committed; it ships only in |
| 209 | + # the published package). Invoke build-jar.sh directly, NOT via `pnpm run`: |
| 210 | + # Socket Firewall wraps the package managers (npm/pnpm/...) it shims, so a |
| 211 | + # `pnpm run` would route the Maven wrapper's download through sfw, which |
| 212 | + # fails on the non-package fetch. Running bash directly keeps the Maven |
| 213 | + # download outside the shimmed process tree. The org action allowlist forbids |
| 214 | + # actions/setup-java, so use a JDK pre-installed on the runner image |
| 215 | + # (JAVA_HOME_17_X64), falling back to the runner's default `java`. |
| 216 | + - name: Build Maven manifest extension jar |
| 217 | + run: | |
| 218 | + if [ -n "${JAVA_HOME_17_X64:-}" ]; then |
| 219 | + export JAVA_HOME="$JAVA_HOME_17_X64" |
| 220 | + fi |
| 221 | + bash src/commands/manifest/scripts/maven-extension/build-jar.sh |
| 222 | +
|
| 223 | + - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist |
| 224 | + - name: Publish socket |
| 225 | + id: publish_socket |
| 226 | + run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" |
| 227 | + continue-on-error: true |
| 228 | + env: |
| 229 | + NPM_DIST_TAG: ${{ inputs.dist-tag }} |
| 230 | + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] |
| 231 | + SOCKET_CLI_DEBUG: ${{ inputs.debug }} |
| 232 | + - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_LEGACY_BUILD=1 pnpm run build:dist |
| 233 | + env: |
| 234 | + SOCKET_CLI_DEBUG: ${{ inputs.debug }} |
| 235 | + - run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" |
| 236 | + continue-on-error: true |
| 237 | + env: |
| 238 | + NPM_DIST_TAG: ${{ inputs.dist-tag }} |
| 239 | + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] |
| 240 | + SOCKET_CLI_DEBUG: ${{ inputs.debug }} |
| 241 | + - run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 INLINED_SOCKET_CLI_SENTRY_BUILD=1 pnpm run build:dist |
| 242 | + env: |
| 243 | + SOCKET_CLI_DEBUG: ${{ inputs.debug }} |
| 244 | + - run: npm publish --provenance --access public --tag "${NPM_DIST_TAG}" |
| 245 | + continue-on-error: true |
| 246 | + env: |
| 247 | + NPM_DIST_TAG: ${{ inputs.dist-tag }} |
| 248 | + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # zizmor: ignore[secrets-outside-env] |
| 249 | + SOCKET_CLI_DEBUG: ${{ inputs.debug }} |
| 250 | + |
| 251 | + # Create v<version> git tag at the published commit SHA after a |
| 252 | + # successful socket-package publish, idempotently. GitHub Release |
| 253 | + # Immutability ("Disallow assets and tags from being modified once a |
| 254 | + # release is published") freezes tags once bound to a Release, so: |
| 255 | + # - existing tag at same SHA → no-op |
| 256 | + # - existing tag at different SHA → hard-fail (operator recovery |
| 257 | + # required) |
| 258 | + # Gated on the first publish step (publish_socket — the `socket` npm |
| 259 | + # package) actually succeeding; the cli / cli-with-sentry publishes |
| 260 | + # use `continue-on-error: true` and don't gate the tag. |
| 261 | + # |
| 262 | + # Uses gh api (not `git push`) so the token only lives in this step's |
| 263 | + # env, never written to `.git/config` by an earlier `actions/checkout` |
| 264 | + # with persist-credentials: true (which would leak it to every later |
| 265 | + # step including `pnpm install` postinstall scripts). |
| 266 | + - name: Tag release (idempotent) |
| 267 | + if: steps.publish_socket.outcome == 'success' |
| 268 | + env: |
| 269 | + GH_TOKEN: ${{ github.token }} |
| 270 | + REPO: ${{ github.repository }} |
| 271 | + run: | |
| 272 | + PUBLISHED_SHA=$(git rev-parse HEAD) |
| 273 | + PUBLISHED_VERSION=$(node -p "require('./package.json').version") |
| 274 | + TAG="v$PUBLISHED_VERSION" |
| 275 | +
|
| 276 | + # Look up any existing tag ref. gh api exits non-zero on 404 (tag |
| 277 | + # absent) and writes the error body to stdout, so branch on the |
| 278 | + # exit code — never on whether stdout is empty. EXISTING_JSON is |
| 279 | + # only valid JSON when the call succeeded. |
| 280 | + if EXISTING_JSON=$(gh api "repos/$REPO/git/ref/tags/$TAG" 2>/dev/null); then |
| 281 | + # The ref's object is either a commit (lightweight tag) or a tag |
| 282 | + # object (annotated/signed tag, e.g. the hand-created `git tag -s` |
| 283 | + # tags). For an annotated tag, object.sha is the tag-object SHA, |
| 284 | + # not the commit — dereference it via git/tags to get the commit |
| 285 | + # the tag actually points at before comparing. |
| 286 | + REF_TYPE=$(echo "$EXISTING_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).object.type") |
| 287 | + REF_OBJECT_SHA=$(echo "$EXISTING_JSON" | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).object.sha") |
| 288 | + if [ "$REF_TYPE" = "tag" ]; then |
| 289 | + EXISTING_SHA=$(gh api "repos/$REPO/git/tags/$REF_OBJECT_SHA" --jq '.object.sha') |
| 290 | + else |
| 291 | + EXISTING_SHA="$REF_OBJECT_SHA" |
| 292 | + fi |
| 293 | + if [ "$EXISTING_SHA" = "$PUBLISHED_SHA" ]; then |
| 294 | + echo "Tag $TAG already exists at $PUBLISHED_SHA — no-op." |
| 295 | + exit 0 |
| 296 | + fi |
| 297 | + echo "::error::Tag $TAG exists at $EXISTING_SHA but publish SHA is $PUBLISHED_SHA." |
| 298 | + echo "::error::Release immutability is enabled; this requires manual recovery:" |
| 299 | + echo "::error:: 1. Delete any GitHub Release tied to $TAG" |
| 300 | + echo "::error:: 2. Delete the tag via the API" |
| 301 | + echo "::error:: 3. Re-run this workflow" |
| 302 | + exit 1 |
| 303 | + fi |
| 304 | +
|
| 305 | + gh api "repos/$REPO/git/refs" \ |
| 306 | + -X POST \ |
| 307 | + -f "ref=refs/tags/$TAG" \ |
| 308 | + -f "sha=$PUBLISHED_SHA" |
| 309 | + echo "Created tag $TAG at $PUBLISHED_SHA" |
0 commit comments