Skip to content

📦 Publish CLIs

📦 Publish CLIs #21

name: 📦 Publish @socketbin packages to npm registry
on:
workflow_dispatch:
inputs:
version:
description: 'Version to publish (semver format: 0.0.0-20250122.143052, auto-generated if omitted)'
required: false
type: string
method:
description: 'Build method to use'
required: false
type: choice
options:
- sea
- smol
- smol-sea
default: sea
dry-run:
description: 'Dry run (build but do not publish) - primes cache for CI e2e tests'
required: false
type: boolean
default: false
jobs:
build-binaries:
name: Build ${{ matrix.platform }}-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# Linux builds
- runner: ubuntu-latest
os: linux
platform: linux
arch: x64
- runner: ubuntu-latest
os: linux
platform: linux
arch: arm64
# Alpine Linux builds
- runner: ubuntu-latest
os: linux
platform: alpine
arch: x64
- runner: ubuntu-latest
os: linux
platform: alpine
arch: arm64
# macOS builds
- runner: macos-latest
os: darwin
platform: darwin
arch: x64
- runner: macos-latest
os: darwin
platform: darwin
arch: arm64
# Windows builds
- runner: windows-latest
os: windows
platform: win32
arch: x64
- runner: windows-latest
os: windows
platform: win32
arch: arm64
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
autocrlf: false
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: ^10.20.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup ccache (Linux/macOS)
if: matrix.os != 'windows'
uses: hendrikmuhs/ccache-action@ed74d11c0b343532753ecead8a951bb09bb34bc9 # v1.2.14
with:
key: build-${{ matrix.platform }}-${{ matrix.arch }}
max-size: 2G
- name: Generate build-deps cache key for smol
if: inputs.method == 'smol' || inputs.method == 'smol-sea'
id: smol-deps-cache-key
shell: bash
run: |
# Generate hash from bootstrap/socket packages (matches build-smol.yml).
if [ "${{ matrix.os }}" = "windows" ]; then
HASH=$(find packages/bootstrap packages/socket -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/build/*" | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)
else
HASH=$(find packages/bootstrap packages/socket -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/build/*" | sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
fi
echo "deps-hash=$HASH" >> $GITHUB_OUTPUT
- name: Restore build-deps cache for smol
if: inputs.method == 'smol' || inputs.method == 'smol-sea'
id: smol-deps-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
packages/bootstrap/dist/
packages/socket/dist/
key: build-deps-smol-${{ steps.smol-deps-cache-key.outputs.deps-hash }}
restore-keys: build-deps-smol-
- name: Build bootstrap package for smol
if: (inputs.method == 'smol' || inputs.method == 'smol-sea') && steps.smol-deps-cache.outputs.cache-hit != 'true'
run: pnpm --filter @socketsecurity/bootstrap run build
- name: Build socket package bootstrap for smol
if: (inputs.method == 'smol' || inputs.method == 'smol-sea') && steps.smol-deps-cache.outputs.cache-hit != 'true'
run: pnpm --filter socket run build
- name: Generate smol build cache key
if: inputs.method == 'smol' || inputs.method == 'smol-sea'
id: smol-cache-key
shell: bash
run: |
# Generate hash from patches and build scripts (matches build-smol.yml).
if [ "${{ matrix.os }}" = "windows" ]; then
PATCHES_HASH=$(find patches packages/node-smol-builder/patches packages/node-smol-builder/additions scripts -type f \( -name "*.patch" -o -name "*.mjs" -o -name "*.h" -o -name "*.c" -o -name "*.cc" \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)
COMBINED_HASH=$(echo "$PATCHES_HASH-${{ steps.smol-deps-cache-key.outputs.deps-hash }}" | sha256sum | cut -d' ' -f1)
else
PATCHES_HASH=$(find patches packages/node-smol-builder/patches packages/node-smol-builder/additions scripts -type f \( -name "*.patch" -o -name "*.mjs" -o -name "*.h" -o -name "*.c" -o -name "*.cc" \) | sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
COMBINED_HASH=$(echo "$PATCHES_HASH-${{ steps.smol-deps-cache-key.outputs.deps-hash }}" | shasum -a 256 | cut -d' ' -f1)
fi
echo "hash=$COMBINED_HASH" >> $GITHUB_OUTPUT
- name: Restore smol binary cache
if: inputs.method == 'smol' || inputs.method == 'smol-sea'
id: smol-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: packages/node-smol-builder/dist/socket-smol-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
key: node-smol-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.smol-cache-key.outputs.hash }}
restore-keys: node-smol-${{ matrix.platform }}-${{ matrix.arch }}-
- name: Build smol Node.js binary
id: build-smol
if: (inputs.method == 'smol' || inputs.method == 'smol-sea') && steps.smol-cache.outputs.cache-hit != 'true'
continue-on-error: true
shell: bash
run: |
echo "Building smol Node.js binary..."
pnpm --filter @socketsecurity/node-smol-builder run build -- \
--platform=${{ matrix.platform }} \
--arch=${{ matrix.arch }}
- name: Build smol binary (fallback with clean rebuild)
if: (inputs.method == 'smol' || inputs.method == 'smol-sea') && steps.smol-cache.outputs.cache-hit != 'true' && steps.build-smol.outcome == 'failure'
shell: bash
run: |
echo "Initial smol build failed, attempting clean rebuild..."
pnpm --filter @socketsecurity/node-smol-builder run build -- \
--platform=${{ matrix.platform }} \
--arch=${{ matrix.arch }} \
--clean
- name: Generate build-deps cache key
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
id: deps-cache-key
shell: bash
run: |
# Include pnpm-lock.yaml to detect dependency changes (matches build-sea.yml).
if [ "${{ matrix.os }}" = "windows" ]; then
HASH=$(find packages/bootstrap packages/socket -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/build/*" | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)
LOCK_HASH=$(sha256sum pnpm-lock.yaml | cut -d' ' -f1)
else
HASH=$(find packages/bootstrap packages/socket -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" -o -name "*.json" \) ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/build/*" | sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
LOCK_HASH=$(shasum -a 256 pnpm-lock.yaml | cut -d' ' -f1)
fi
COMBINED_HASH=$(echo "$HASH-$LOCK_HASH" | sha256sum | cut -d' ' -f1 || echo "$HASH-$LOCK_HASH" | shasum -a 256 | cut -d' ' -f1)
echo "deps-hash=$COMBINED_HASH" >> $GITHUB_OUTPUT
- name: Restore build-deps cache
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
id: deps-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
packages/bootstrap/dist/
packages/socket/dist/
key: build-deps-sea-${{ steps.deps-cache-key.outputs.deps-hash }}
restore-keys: build-deps-sea-
- name: Build bootstrap package
if: (inputs.method == 'sea' || inputs.method == 'smol-sea') && steps.deps-cache.outputs.cache-hit != 'true'
run: pnpm --filter @socketsecurity/bootstrap run build
- name: Build socket package bootstrap
if: (inputs.method == 'sea' || inputs.method == 'smol-sea') && steps.deps-cache.outputs.cache-hit != 'true'
run: pnpm --filter socket run build
- name: Generate SEA build cache key
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
id: sea-cache-key
shell: bash
run: |
if [ "${{ matrix.os }}" = "windows" ]; then
SEA_HASH=$(find packages/node-sea-builder packages/cli/src -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)
else
SEA_HASH=$(find packages/node-sea-builder packages/cli/src -type f \( -name "*.mts" -o -name "*.ts" -o -name "*.mjs" -o -name "*.js" \) | sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
fi
# Include bootstrap/socket/cli dependencies in cache key (matches build-sea.yml).
COMBINED_HASH=$(echo "$SEA_HASH-${{ steps.deps-cache-key.outputs.deps-hash }}" | sha256sum | cut -d' ' -f1 || echo "$SEA_HASH-${{ steps.deps-cache-key.outputs.deps-hash }}" | shasum -a 256 | cut -d' ' -f1)
echo "hash=$COMBINED_HASH" >> $GITHUB_OUTPUT
- name: Restore SEA binary cache
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
id: sea-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: packages/node-sea-builder/dist/sea/
key: node-sea-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.sea-cache-key.outputs.hash }}
restore-keys: node-sea-${{ matrix.platform }}-${{ matrix.arch }}-
- name: Build CLI (required for SEA)
if: (inputs.method == 'sea' || inputs.method == 'smol-sea') && steps.sea-cache.outputs.cache-hit != 'true'
shell: bash
run: pnpm --filter @socketsecurity/cli run build
- name: Build SEA binary
id: build-sea
if: (inputs.method == 'sea' || inputs.method == 'smol-sea') && steps.sea-cache.outputs.cache-hit != 'true'
shell: bash
run: |
echo "Building SEA binary..."
pnpm --filter @socketbin/node-sea-builder run build -- \
--platform=${{ matrix.platform }} \
--arch=${{ matrix.arch }}
- name: Copy binary to dist/sea for prepublish script
shell: bash
run: |
mkdir -p dist/sea
# Determine source binary name (from build-sea.yml naming).
if [ "${{ matrix.platform }}" = "win32" ]; then
SOURCE_BINARY="packages/node-sea-builder/dist/sea/socket-win-${{ matrix.arch }}.exe"
TARGET_BINARY="dist/sea/socket-${{ matrix.platform }}-${{ matrix.arch }}.exe"
elif [ "${{ matrix.platform }}" = "darwin" ]; then
SOURCE_BINARY="packages/node-sea-builder/dist/sea/socket-macos-${{ matrix.arch }}"
TARGET_BINARY="dist/sea/socket-${{ matrix.platform }}-${{ matrix.arch }}"
else
SOURCE_BINARY="packages/node-sea-builder/dist/sea/socket-${{ matrix.platform }}-${{ matrix.arch }}"
TARGET_BINARY="dist/sea/socket-${{ matrix.platform }}-${{ matrix.arch }}"
fi
cp "$SOURCE_BINARY" "$TARGET_BINARY"
echo "Copied $SOURCE_BINARY -> $TARGET_BINARY"
- name: Verify binary
shell: bash
run: |
ls -la dist/sea/socket-*
file dist/sea/socket-* || true
- name: Upload binary artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: binary-${{ matrix.platform }}-${{ matrix.arch }}
path: dist/sea/socket-*
retention-days: 1
publish-packages:
name: Publish to npm
needs: build-binaries
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # For provenance
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
autocrlf: false
- name: Setup Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Setup pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: ^10.20.0
- name: Install latest npm
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Determine version
id: version
run: |
VERSION="${{ inputs.version }}"
if [ -z "$VERSION" ]; then
# Read base version from socketbin package and extract X.Y.Z using semver
PKG_VERSION=$(jq -r '.version' packages/socketbin-cli-linux-x64/package.json)
BASE_VERSION=$(node -e "const semver = require('semver'); const v = semver.parse('$PKG_VERSION'); console.log(v ? \`\${v.major}.\${v.minor}.\${v.patch}\` : '0.0.0');")
# Auto-generate version in semver format: X.Y.Z-YYYYMMDD.HHmmss
VERSION="${BASE_VERSION}-$(date -u +'%Y%m%d.%H%M%S')"
echo "Generated version: $VERSION"
else
# Remove 'v' prefix if present
VERSION="${VERSION#v}"
echo "Using provided version: $VERSION"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Check version consistency
run: |
echo "🔍 Checking version consistency for v${{ steps.version.outputs.version }}..."
node scripts/check-version-consistency.mjs ${{ steps.version.outputs.version }}
- name: Download all binaries
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
path: dist/sea
pattern: binary-*
merge-multiple: true
- name: Verify downloaded binaries
run: |
echo "Downloaded binaries:"
ls -la dist/sea/
- name: Prepare and validate Linux x64
run: |
node scripts/prepublish-socketbin.mjs \
--platform=linux --arch=x64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
- name: Publish Linux x64
if: ${{ !inputs.dry-run }}
run: |
cd packages/socketbin-cli-linux-x64
npm publish --provenance --access public --tag latest --tag latest
- name: Prepare and publish Linux ARM64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=linux --arch=arm64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-linux-arm64
npm publish --provenance --access public --tag latest --tag latest
- name: Prepare and publish Alpine x64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=alpine --arch=x64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-alpine-x64
npm publish --provenance --access public --tag latest --tag latest
- name: Prepare and publish Alpine ARM64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=alpine --arch=arm64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-alpine-arm64
npm publish --provenance --access public --tag latest
- name: Prepare and publish macOS x64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=darwin --arch=x64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-darwin-x64
npm publish --provenance --access public --tag latest
- name: Prepare and publish macOS ARM64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=darwin --arch=arm64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-darwin-arm64
npm publish --provenance --access public --tag latest
- name: Prepare and publish Windows x64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=win32 --arch=x64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-win32-x64
npm publish --provenance --access public --tag latest
- name: Prepare and publish Windows ARM64
if: ${{ !inputs.dry-run }}
run: |
node scripts/prepublish-socketbin.mjs \
--platform=win32 --arch=arm64 \
--version=${{ steps.version.outputs.version }} \
--method=${{ inputs.method }}
cd packages/socketbin-cli-win32-arm64
npm publish --provenance --access public --tag latest
- name: Prime npm cache for CI (dry-run)
if: ${{ inputs.dry-run }}
run: |
echo "🔄 Priming npm cache for CI e2e tests..."
# Prime cache for @coana-tech/cli
if [ -n "$INLINED_SOCKET_CLI_COANA_VERSION" ]; then
echo "Priming @coana-tech/cli@~$INLINED_SOCKET_CLI_COANA_VERSION..."
npx --yes @coana-tech/cli@~$INLINED_SOCKET_CLI_COANA_VERSION --help || true
fi
echo "✓ Cache priming complete"
- name: Dry run summary
if: ${{ inputs.dry-run }}
run: |
echo "🚫 Dry run mode - packages were NOT published"
echo ""
echo "Generated packages:"
find packages/binaries -name package.json -exec echo {} \; -exec jq -r '.name + "@" + .version' {} \;
echo ""
echo "💾 Cache primed for CI e2e tests"