Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions .github/workflows/provenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
name: Publish to npm registry

on:
workflow_dispatch:
inputs:
dist-tag:
description: 'npm dist-tag (latest, next, beta, canary, backport, etc.)'
required: false
default: 'latest'
type: string
debug:
description: 'Enable debug output'
required: false
default: '0'
type: string
options:
- '0'
- '1'

permissions:
contents: read

jobs:
build:
name: Build and Publish
runs-on: ubuntu-latest

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:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 (2026-05-20)
with:
persist-credentials: false

- name: Install pnpm
shell: bash
run: | # zizmor: ignore[github-env]
PNPM_VERSION="10.33.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" ; EXPECTED_SHA256="8d4e8f7d778e8ac482022e2577011706a872542f6f6f233e795a4d9f978ea8b5" ;;
Linux-aarch64) ASSET="pnpm-linux-arm64" ; EXPECTED_SHA256="06755ad2817548b84317d857d5c8003dc6e9e28416a3ea7467256c49ab400d48" ;;
Darwin-x86_64) ASSET="pnpm-macos-x64" ; EXPECTED_SHA256="c31e29554b0e3f4e03f4617195c949595e4dca36085922003de4896c3ca4057d" ;;
Darwin-arm64) ASSET="pnpm-macos-arm64" ; EXPECTED_SHA256="ed8a1f140f4de457b01ebe0be3ae28e9a7e28863315dcd53d22ff1e5a32d63ae" ;;
MINGW64_NT-x86_64|MSYS_NT-x86_64) ASSET="pnpm-win-x64.exe" ; EXPECTED_SHA256="afc96009dc39fe23a835d65192049e6a995f342496b175585dc2beda7d42d33f" ;;
*) echo "Unsupported platform: ${KERNEL}-${ARCH}" >&2; exit 1 ;;
esac
PNPM_BIN="$PNPM_DIR/$ASSET"
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}"
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
echo " Expected: ${EXPECTED_SHA256}" >&2
echo " Actual: ${ACTUAL_SHA256}" >&2
rm -f "$PNPM_BIN"
exit 1
fi
chmod +x "$PNPM_BIN"
# Create pnpm alias. Windows needs a .exe copy; Unix uses a symlink.
if [[ "$ASSET" == *.exe ]]; then
cp "$PNPM_BIN" "$PNPM_DIR/pnpm.exe"
else
ln -sf "$PNPM_BIN" "$PNPM_DIR/pnpm"
fi
fi
echo "$PNPM_DIR" >> "${GITHUB_PATH:-/dev/null}"

- 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"
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

- run: INLINED_SOCKET_CLI_PUBLISHED_BUILD=1 pnpm run build:dist
- name: Publish socket
id: publish_socket
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}"
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}"
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<version> git tag at the published commit SHA after a
# successful socket-package publish, idempotently. 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 (operator recovery
# required)
# Gated on the first publish step (publish_socket — the `socket` npm
# package) actually succeeding; the cli / cli-with-sentry publishes
# use `continue-on-error: true` and don't gate the tag.
#
# 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)
if: steps.publish_socket.outcome == 'success'
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"

# 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"
Loading