Skip to content

📦 Publish CLIs

📦 Publish CLIs #14

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 cache key
id: build-cache-key
shell: bash
run: |
# Generate hash from build-affecting files (matches build-binaries.yml).
if [ "${{ matrix.os }}" = "windows" ]; then
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)
else
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)
fi
echo "hash=$HASH" >> $GITHUB_OUTPUT
echo "Build hash: $HASH"
- name: Restore smol binary from build-socketbin.yml cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: smol-cache
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.build-cache-key.outputs.hash }}
restore-keys: node-smol-${{ matrix.platform }}-${{ matrix.arch }}-
- name: Cache compiled Node.js binary
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: binary-cache
with:
path: |
packages/node-smol-builder/build/out/Release/node
packages/node-smol-builder/build/out/Release/node.exe
key: node-binary-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.build-cache-key.outputs.hash }}
restore-keys: |
node-binary-${{ matrix.platform }}-${{ matrix.arch }}-
- name: Build smol Node.js binary
id: build-smol
if: inputs.method == 'smol' || inputs.method == 'smol-sea'
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.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: Build CLI (required for SEA)
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
shell: bash
run: pnpm --filter @socketsecurity/cli run build
- name: Build SEA binary
id: build-sea
if: inputs.method == 'sea' || inputs.method == 'smol-sea'
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"
publish-main:
name: Publish main socket package
needs: publish-packages
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
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: Update package.json for @socketbin
run: |
cd src/sea/npm-package
# Update version
npm version ${{ steps.version.outputs.version }} --no-git-tag-version
# Update package.json to use optionalDependencies
node -e "
const fs = require('fs');
const { execSync } = require('child_process');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// Remove old postinstall script
delete pkg.scripts?.postinstall;
// Add dispatcher script to bin
pkg.bin = { socket: 'bin/socket.js' };
// Get latest version from registry for each platform package
const platforms = [
'alpine-arm64', 'alpine-x64',
'darwin-arm64', 'darwin-x64',
'linux-arm64', 'linux-x64',
'win32-arm64', 'win32-x64'
];
pkg.optionalDependencies = {};
for (const platform of platforms) {
const pkgName = \`@socketbin/cli-\${platform}\`;
try {
// Get latest published version from registry
const version = execSync(\`npm view \${pkgName} version\`, { encoding: 'utf8' }).trim();
pkg.optionalDependencies[pkgName] = version;
console.log(\`Using published version for \${pkgName}: \${version}\`);
} catch (e) {
// Package doesn't exist yet - skip it
console.log(\`Package \${pkgName} not found in registry, skipping\`);
}
}
// Update files list
pkg.files = ['bin', 'README.md'];
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
# Show the updated package.json
cat package.json
- name: Validate main package
run: |
cd src/sea/npm-package
# Basic validation
test -f package.json || exit 1
test -f bin/socket.js || exit 1
test -f README.md || exit 1
echo "✓ Package structure validated"
- name: Publish main package
if: ${{ !inputs.dry-run }}
working-directory: src/sea/npm-package
run: npm publish --provenance --access public --tag latest
- name: Dry run summary
if: ${{ inputs.dry-run }}
run: |
echo "🚫 Dry run mode - main package was NOT published"
echo ""
echo "Would have published:"
cd src/sea/npm-package
echo "socket@$(jq -r .version package.json)"