Skip to content

Node.js Smol

Node.js Smol #220

Workflow file for this run

name: Node.js Smol
on:
release:
types: [published]
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (build only, no release)'
type: boolean
default: true
force:
description: 'Force rebuild (ignore cache)'
type: boolean
default: false
build_mode:
description: 'Build mode'
type: choice
options:
- prod
- dev
default: prod
workflow_call:
inputs:
dry_run:
type: boolean
default: true
force:
type: boolean
default: false
build_mode:
type: string
default: prod
permissions:
contents: read
env:
CACHE_VERSION: v9
jobs:
build:
permissions:
contents: read
name: Build (${{ matrix.platform }}-${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 210
strategy:
fail-fast: false
matrix:
include:
# macOS builds - build natively on each architecture (no cross-compilation)
# This avoids ninja "multiple rules generate" errors in v8_inspector_headers
# and matches Node.js's official build strategy (they don't cross-compile either)
- runner: macos-14
platform: darwin
arch: arm64
os: macos
# Use large runner for darwin-x64: 12 vCPU, 30GB RAM (vs standard 3 vCPU, 14GB)
# Standard runner takes ~5hrs to build (too slow), large completes in ~2.5hrs with 2 jobs
# Limited to 2 parallel jobs to prevent clang segfaults from memory pressure during V8 compilation
# Note: macos-15-large is the last Intel runner (supported until August 2027)
- runner: macos-15-large
platform: darwin
arch: x64
os: macos
# Linux glibc builds
- runner: ubuntu-22.04
platform: linux
arch: x64
os: linux
- runner: ubuntu-22.04-arm
platform: linux
arch: arm64
os: linux
# Linux musl builds (Alpine)
- runner: ubuntu-22.04
platform: linux-musl
arch: x64
os: linux
- runner: ubuntu-22.04-arm
platform: linux-musl
arch: arm64
os: linux
# Windows builds
- runner: windows-2022
platform: win32
arch: x64
os: windows
# Windows ARM64: Cross-compile on x64 runner (no ARM64 hosted runners available)
- runner: windows-2022
platform: win32
arch: arm64
os: windows
cross_compile: true
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Load tool versions
id: tool-versions
shell: bash
run: |
NODE_BUILD_VERSION=$(jq -r '.sources.node.version' packages/node-smol-builder/package.json)
NODE_VERSION=$(jq -r '.tools.node.versions.recommendedVersion' packages/build-infra/external-tools.json)
echo "node-build-version=$NODE_BUILD_VERSION" >> $GITHUB_OUTPUT
echo "node-version=$NODE_VERSION" >> $GITHUB_OUTPUT
echo "Loaded Node.js build version: $NODE_BUILD_VERSION, CI version: $NODE_VERSION"
- name: Setup compiler (Linux)
if: matrix.os == 'linux'
run: |
# Node.js v24+ requires GCC 12+ for ada URL parser C++20 constexpr support
# Ubuntu 22.04 ships with GCC 11, so install GCC 13 from toolchain PPA
# Load version from external-tools.json (single source of truth)
sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
sudo apt-get update
GCC_VERSION=$(node packages/build-infra/scripts/get-tool-version.mjs gcc-13 apt --package-root packages/node-smol-builder --checkpoint binary-released)
sudo apt-get install -y gcc-13=${GCC_VERSION}-* g++-13=${GCC_VERSION}-*
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-13 100
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-13 100
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-13 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-13 100
gcc --version
g++ --version
- name: Select Xcode version (macOS)
if: matrix.os == 'macos'
run: |
# Select appropriate Xcode version for each runner
# macos-14: Use Xcode 16.1 (ARM64)
# macos-15-large: Use Xcode 16.4 (Intel x64, default on macOS 15)
if [ "${{ matrix.runner }}" = "macos-14" ]; then
sudo xcode-select -s /Applications/Xcode_16.1.app
elif [ "${{ matrix.runner }}" = "macos-15-large" ]; then
sudo xcode-select -s /Applications/Xcode_16.4.app
fi
xcodebuild -version
clang --version
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
# Note: version is specified in package.json packageManager field, not here
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: ${{ steps.tool-versions.outputs.node-version }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Free disk space (Linux)
if: matrix.os == 'linux'
run: |
echo "Disk space before cleanup:"
df -h
# Remove large unused packages to free up ~10GB for cache extraction
# These are pre-installed on GitHub runners but not needed for our Node.js builds
sudo rm -rf /usr/share/dotnet # .NET SDK (~3GB) - unused
sudo rm -rf /usr/local/lib/android # Android SDK (~4GB) - unused
sudo rm -rf /opt/ghc # Haskell GHC (~1GB) - unused
sudo rm -rf /opt/hostedtoolcache/CodeQL # CodeQL analysis (~2GB) - unused
sudo docker image prune --all --force # Docker images (~1-2GB) - unused
echo "Disk space after cleanup:"
df -h
- name: Setup ccache (Unix)
if: matrix.os != 'windows'
uses: hendrikmuhs/ccache-action@ed74d11c0b343532753ecead8a951bb09bb34bc9 # v1.2.14
with:
key: ${{ matrix.os }}-${{ matrix.platform }}-${{ matrix.arch }}-${{ inputs.build_mode || 'prod' }}
max-size: 2G
- name: Setup ccache (Windows)
if: matrix.os == 'windows'
uses: hendrikmuhs/ccache-action@ed74d11c0b343532753ecead8a951bb09bb34bc9 # v1.2.14
with:
key: windows-${{ matrix.platform }}-${{ matrix.arch }}-${{ inputs.build_mode || 'prod' }}
max-size: 2G
variant: sccache
- name: Build compression tools
shell: bash
run: |
cd packages/node-smol-builder/compression-tools
if [ "${{ matrix.os }}" = "linux" ]; then
# Install liblzma and OpenSSL development headers for Linux builds
# Load versions from external-tools.json (single source of truth)
if [ "${{ matrix.platform }}" = "linux" ] || [ "${{ matrix.platform }}" = "linux-musl" ]; then
sudo apt-get update
LIBLZMA_VERSION=$(node ../../../packages/build-infra/scripts/get-tool-version.mjs liblzma-dev apt --package-root ../../../packages/node-smol-builder --checkpoint binary-compressed)
LIBSSL_VERSION=$(node ../../../packages/build-infra/scripts/get-tool-version.mjs libssl-dev apt --package-root ../../../packages/node-smol-builder --checkpoint binary-compressed)
sudo apt-get install -y liblzma-dev=${LIBLZMA_VERSION}-* libssl-dev=${LIBSSL_VERSION}-*
fi
# Build Linux ELF compression tools
echo "Building Linux compression tools..."
make -f Makefile.linux
ls -lh socketsecurity_elf_*
elif [ "${{ matrix.os }}" = "windows" ]; then
# Build Windows PE compression tools using MinGW
echo "Building Windows compression tools..."
make -f Makefile.windows
ls -lh socketsecurity_pe_*.exe
elif [ "${{ matrix.os }}" = "macos" ]; then
# Build macOS Mach-O compression tools
echo "Building macOS compression tools..."
make -f Makefile
ls -lh socketsecurity_macho_*
fi
- name: Generate smol build cache key
id: smol-cache-key
shell: bash
env:
BUILD_MODE: ${{ inputs.build_mode || 'prod' }}
run: |
# Cross-platform hash function
if command -v shasum &> /dev/null; then
hash_cmd="shasum -a 256"
elif command -v sha256sum &> /dev/null; then
hash_cmd="sha256sum"
else
echo "Error: No SHA-256 command found"
exit 1
fi
# Helper function to hash files, returns empty string if no files found
# Callers must pass -type f and optionally -name "*.mjs" to filter appropriately
hash_files() {
find "$@" 2>/dev/null | sort | xargs $hash_cmd 2>/dev/null | $hash_cmd | cut -d' ' -f1 || echo ""
}
# Helper function to get hierarchical paths for a category/phase/platform/arch
# Returns: shared/, platform/shared/, platform/arch/
get_hierarchical_paths() {
local category=$1 # scripts, patches, additions
local phase=$2 # binary-released, binary-stripped, binary-compressed, finalized
local platform=$3 # darwin, linux, linux-musl, win32
local arch=$4 # arm64, x64
local base="packages/node-smol-builder/${category}/${phase}"
echo "${base}/shared ${base}/${platform}/shared ${base}/${platform}/${arch}"
}
# Get platform and arch from matrix
PLATFORM="${{ matrix.platform }}"
ARCH="${{ matrix.arch }}"
# Phase-specific hashing for granular cache invalidation
# Each phase includes its dependencies (cumulative hashing)
# Now with hierarchical paths: shared/ → platform/shared/ → platform/arch/
# Common scripts (used by all phases) - hierarchical
# Each checkpoint now has its own paths.mjs that re-exports from scripts/paths.mjs
# This makes dependencies explicit and ensures changes to paths.mjs are tracked per-checkpoint
COMMON_PATHS=$(get_hierarchical_paths scripts common "$PLATFORM" "$ARCH")
COMMON_SCRIPTS=$(hash_files $COMMON_PATHS -type f -name "*.mjs")
# source-cloned phase: patches/source-cloned + additions/source-cloned + scripts/source-cloned + common
SOURCE_CLONED_PATCH_PATHS=$(get_hierarchical_paths patches source-cloned "$PLATFORM" "$ARCH")
SOURCE_CLONED_ADDITION_PATHS=$(get_hierarchical_paths additions source-cloned "$PLATFORM" "$ARCH")
SOURCE_CLONED_SCRIPT_PATHS=$(get_hierarchical_paths scripts source-cloned "$PLATFORM" "$ARCH")
SOURCE_CLONED_PATCHES=$(hash_files $SOURCE_CLONED_PATCH_PATHS -type f)
SOURCE_CLONED_ADDITIONS=$(hash_files $SOURCE_CLONED_ADDITION_PATHS -type f)
SOURCE_CLONED_SCRIPTS=$(hash_files $SOURCE_CLONED_SCRIPT_PATHS -type f -name "*.mjs")
SOURCE_CLONED_KEY=$(echo "${COMMON_SCRIPTS}${SOURCE_CLONED_PATCHES}${SOURCE_CLONED_ADDITIONS}${SOURCE_CLONED_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
# source-patched phase: source-cloned + patches/source-patched + additions/source-patched + scripts/source-patched
SOURCE_PATCHED_PATCH_PATHS=$(get_hierarchical_paths patches source-patched "$PLATFORM" "$ARCH")
SOURCE_PATCHED_ADDITION_PATHS=$(get_hierarchical_paths additions source-patched "$PLATFORM" "$ARCH")
SOURCE_PATCHED_SCRIPT_PATHS=$(get_hierarchical_paths scripts source-patched "$PLATFORM" "$ARCH")
SOURCE_PATCHED_PATCHES=$(hash_files $SOURCE_CLONED_PATCH_PATHS $SOURCE_PATCHED_PATCH_PATHS -type f)
SOURCE_PATCHED_ADDITIONS=$(hash_files $SOURCE_CLONED_ADDITION_PATHS $SOURCE_PATCHED_ADDITION_PATHS -type f)
SOURCE_PATCHED_SCRIPTS=$(hash_files $SOURCE_CLONED_SCRIPT_PATHS $SOURCE_PATCHED_SCRIPT_PATHS -type f -name "*.mjs")
SOURCE_PATCHED_KEY=$(echo "${COMMON_SCRIPTS}${SOURCE_PATCHED_PATCHES}${SOURCE_PATCHED_ADDITIONS}${SOURCE_PATCHED_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
# Binary-release phase: source-patched + patches/binary-released + additions/binary-released + scripts/binary-released
BINARY_RELEASED_PATCH_PATHS=$(get_hierarchical_paths patches binary-released "$PLATFORM" "$ARCH")
BINARY_RELEASED_ADDITION_PATHS=$(get_hierarchical_paths additions binary-released "$PLATFORM" "$ARCH")
BINARY_RELEASED_SCRIPT_PATHS=$(get_hierarchical_paths scripts binary-released "$PLATFORM" "$ARCH")
BINARY_RELEASED_PATCHES=$(hash_files $SOURCE_CLONED_PATCH_PATHS $SOURCE_PATCHED_PATCH_PATHS $BINARY_RELEASED_PATCH_PATHS -type f)
BINARY_RELEASED_ADDITIONS=$(hash_files $SOURCE_CLONED_ADDITION_PATHS $SOURCE_PATCHED_ADDITION_PATHS $BINARY_RELEASED_ADDITION_PATHS -type f)
BINARY_RELEASED_SCRIPTS=$(hash_files $SOURCE_CLONED_SCRIPT_PATHS $SOURCE_PATCHED_SCRIPT_PATHS $BINARY_RELEASED_SCRIPT_PATHS -type f -name "*.mjs")
BINARY_RELEASED_KEY=$(echo "${COMMON_SCRIPTS}${BINARY_RELEASED_PATCHES}${BINARY_RELEASED_ADDITIONS}${BINARY_RELEASED_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
# Binary-stripped phase: binary-released + patches/binary-stripped + additions/binary-stripped + scripts/binary-stripped
BINARY_STRIPPED_PATCH_PATHS=$(get_hierarchical_paths patches binary-stripped "$PLATFORM" "$ARCH")
BINARY_STRIPPED_ADDITION_PATHS=$(get_hierarchical_paths additions binary-stripped "$PLATFORM" "$ARCH")
BINARY_STRIPPED_SCRIPT_PATHS=$(get_hierarchical_paths scripts binary-stripped "$PLATFORM" "$ARCH")
BINARY_STRIPPED_PATCHES=$(hash_files $BINARY_RELEASED_PATCH_PATHS $BINARY_STRIPPED_PATCH_PATHS -type f)
BINARY_STRIPPED_ADDITIONS=$(hash_files $BINARY_RELEASED_ADDITION_PATHS $BINARY_STRIPPED_ADDITION_PATHS -type f)
BINARY_STRIPPED_SCRIPTS=$(hash_files $BINARY_RELEASED_SCRIPT_PATHS $BINARY_STRIPPED_SCRIPT_PATHS -type f -name "*.mjs")
BINARY_STRIPPED_KEY=$(echo "${COMMON_SCRIPTS}${BINARY_STRIPPED_PATCHES}${BINARY_STRIPPED_ADDITIONS}${BINARY_STRIPPED_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
# Binary-compressed phase: binary-released + binary-stripped + patches/binary-compressed + additions/binary-compressed + scripts/binary-compressed
BINARY_COMPRESSED_PATCH_PATHS=$(get_hierarchical_paths patches binary-compressed "$PLATFORM" "$ARCH")
BINARY_COMPRESSED_ADDITION_PATHS=$(get_hierarchical_paths additions binary-compressed "$PLATFORM" "$ARCH")
BINARY_COMPRESSED_SCRIPT_PATHS=$(get_hierarchical_paths scripts binary-compressed "$PLATFORM" "$ARCH")
BINARY_COMPRESSED_PATCHES=$(hash_files $BINARY_RELEASED_PATCH_PATHS $BINARY_STRIPPED_PATCH_PATHS $BINARY_COMPRESSED_PATCH_PATHS -type f)
BINARY_COMPRESSED_ADDITIONS=$(hash_files $BINARY_RELEASED_ADDITION_PATHS $BINARY_STRIPPED_ADDITION_PATHS $BINARY_COMPRESSED_ADDITION_PATHS -type f)
BINARY_COMPRESSED_SCRIPTS=$(hash_files $BINARY_RELEASED_SCRIPT_PATHS $BINARY_STRIPPED_SCRIPT_PATHS $BINARY_COMPRESSED_SCRIPT_PATHS -type f -name "*.mjs")
BINARY_COMPRESSED_KEY=$(echo "${COMMON_SCRIPTS}${BINARY_COMPRESSED_PATCHES}${BINARY_COMPRESSED_ADDITIONS}${BINARY_COMPRESSED_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
# Finalized phase: binary-released + binary-stripped + binary-compressed + patches/finalized + additions/finalized + scripts/finalized
FINAL_PATCH_PATHS=$(get_hierarchical_paths patches finalized "$PLATFORM" "$ARCH")
FINAL_ADDITION_PATHS=$(get_hierarchical_paths additions finalized "$PLATFORM" "$ARCH")
FINAL_SCRIPT_PATHS=$(get_hierarchical_paths scripts finalized "$PLATFORM" "$ARCH")
FINAL_PATCHES=$(hash_files $BINARY_RELEASED_PATCH_PATHS $BINARY_STRIPPED_PATCH_PATHS $BINARY_COMPRESSED_PATCH_PATHS $FINAL_PATCH_PATHS -type f)
FINAL_ADDITIONS=$(hash_files $BINARY_RELEASED_ADDITION_PATHS $BINARY_STRIPPED_ADDITION_PATHS $BINARY_COMPRESSED_ADDITION_PATHS $FINAL_ADDITION_PATHS -type f)
FINAL_SCRIPTS=$(hash_files $BINARY_RELEASED_SCRIPT_PATHS $BINARY_STRIPPED_SCRIPT_PATHS $BINARY_COMPRESSED_SCRIPT_PATHS $FINAL_SCRIPT_PATHS -type f -name "*.mjs")
FINAL_KEY=$(echo "${COMMON_SCRIPTS}${FINAL_PATCHES}${FINAL_ADDITIONS}${FINAL_SCRIPTS}" | $hash_cmd | cut -d' ' -f1)
echo "source_cloned_hash=${SOURCE_CLONED_KEY}" >> $GITHUB_OUTPUT
echo "source_patched_hash=${SOURCE_PATCHED_KEY}" >> $GITHUB_OUTPUT
echo "binary_released_hash=${BINARY_RELEASED_KEY}" >> $GITHUB_OUTPUT
echo "binary_stripped_hash=${BINARY_STRIPPED_KEY}" >> $GITHUB_OUTPUT
echo "binary_compressed_hash=${BINARY_COMPRESSED_KEY}" >> $GITHUB_OUTPUT
echo "final_hash=${FINAL_KEY}" >> $GITHUB_OUTPUT
echo "build_mode=${BUILD_MODE}" >> $GITHUB_OUTPUT
- name: Restore checkpoint cache
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: checkpoint-cache
if: ${{ !inputs.force }}
with:
path: |
packages/node-smol-builder/build/shared/checkpoints
packages/node-smol-builder/build/${{ steps.smol-cache-key.outputs.build_mode }}/checkpoints
key: node-checkpoints-${{ env.CACHE_VERSION }}-${{ steps.tool-versions.outputs.node-build-version }}-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.smol-cache-key.outputs.build_mode }}-${{ steps.smol-cache-key.outputs.final_hash }}
- name: Validate checkpoint cache integrity
id: validate-cache
if: steps.checkpoint-cache.outputs.cache-hit == 'true'
shell: bash
run: |
BUILD_MODE="${STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE}"
SHARED_CHECKPOINT_DIR="packages/node-smol-builder/build/shared/checkpoints"
MODE_CHECKPOINT_DIR="packages/node-smol-builder/build/${BUILD_MODE}/checkpoints"
echo "Validating cached checkpoints for ${{ matrix.platform }}-${{ matrix.arch }}..."
# Check if checkpoint tarballs and JSON files exist
# source-cloned is in shared directory, others are in mode-specific directory
if [ ! -f "${SHARED_CHECKPOINT_DIR}/source-cloned.tar.gz" ] || \
[ ! -f "${SHARED_CHECKPOINT_DIR}/source-cloned.json" ] || \
[ ! -f "${MODE_CHECKPOINT_DIR}/binary-compressed.tar.gz" ] || \
[ ! -f "${MODE_CHECKPOINT_DIR}/binary-compressed.json" ]; then
echo "❌ Checkpoint files incomplete"
rm -rf "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}"
echo "cache_valid=false" >> $GITHUB_OUTPUT
exit 0
fi
# Check tarball integrity for both directories
for checkpoint_dir in "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}"; do
for tarball in "${checkpoint_dir}"/*.tar.gz; do
if [ -f "$tarball" ]; then
if ! gzip -t "$tarball" 2>/dev/null; then
echo "❌ Corrupted tarball: $(basename "$tarball")"
rm -rf "${SHARED_CHECKPOINT_DIR}" "${MODE_CHECKPOINT_DIR}"
echo "cache_valid=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
done
done
echo "✅ Checkpoint cache validation passed"
echo "cache_valid=true" >> $GITHUB_OUTPUT
env:
STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE: ${{ steps.smol-cache-key.outputs.build_mode }}
- name: Determine build parallelism
id: build-parallelism
shell: bash
run: |
# Set step output using GitHub Actions file-based approach
# Usage: echo "key=value" >> $GITHUB_OUTPUT
# Reference later as: ${{ steps.build-parallelism.outputs.key }}
#
# Limit parallelism to prevent memory exhaustion during V8 compilation
# V8 compilation with -O3 uses ~4-6GB RAM per job
# Linux ARM64: 3 jobs (8GB RAM, 4 vCPU) - increased for timeout concerns
if [[ "${{ matrix.platform }}" == "linux" || "${{ matrix.platform }}" == "linux-musl" ]] && [[ "${{ matrix.arch }}" == "arm64" ]]; then
echo "jobs=3" >> $GITHUB_OUTPUT # → steps.build-parallelism.outputs.jobs = "3"
# macOS ARM64: 2 jobs (7GB RAM, 3 vCPU) - conservative for memory
elif [[ "${{ matrix.arch }}" == "arm64" ]]; then
echo "jobs=2" >> $GITHUB_OUTPUT # → steps.build-parallelism.outputs.jobs = "2"
# macOS x64 large: 2 jobs (30GB RAM, 12 vCPU) - prevent clang segfaults
elif [[ "${{ matrix.os }}" == "macos" && "${{ matrix.arch }}" == "x64" ]]; then
echo "jobs=2" >> $GITHUB_OUTPUT # → steps.build-parallelism.outputs.jobs = "2"
# Linux x64: Auto-calculate based on RAM (typically 3-4 jobs)
else
echo "jobs=" >> $GITHUB_OUTPUT # → steps.build-parallelism.outputs.jobs = "" (triggers adaptive calculation in build script)
fi
- name: Get checkpoint chain
id: checkpoint-chain
shell: bash
env:
BUILD_MODE: ${{ steps.smol-cache-key.outputs.build_mode }}
run: |
# Get checkpoint chain from build script (single source of truth)
CHAIN=$(node packages/node-smol-builder/scripts/get-checkpoint-chain.mjs --$BUILD_MODE)
echo "checkpoint_chain=$CHAIN" >> $GITHUB_OUTPUT
echo "Checkpoint chain for $BUILD_MODE mode: $CHAIN"
- name: Restore build output from checkpoint chain
id: restore-checkpoint
uses: ./.github/actions/restore-checkpoint
with:
package-name: 'node-smol-builder'
build-mode: ${{ steps.smol-cache-key.outputs.build_mode }}
checkpoint-chain: ${{ steps.checkpoint-chain.outputs.checkpoint_chain }}
cache-hit: ${{ steps.checkpoint-cache.outputs.cache-hit }}
cache-valid: ${{ steps.validate-cache.outputs.cache_valid }}
- name: Build Node.js smol
if: |
(steps.checkpoint-cache.outputs.cache-hit != 'true' || steps.validate-cache.outputs.cache_valid == 'false') ||
steps.restore-checkpoint.outputs.needs-build == 'true'
shell: bash
env:
BUILD_MODE: ${{ inputs.build_mode || 'prod' }}
MATRIX_PLATFORM: ${{ matrix.platform }}
MATRIX_ARCH: ${{ matrix.arch }}
NODE_BUILD_JOBS: ${{ steps.build-parallelism.outputs.jobs }}
run: |
if [ "$BUILD_MODE" = "prod" ]; then
pnpm --filter node-smol-builder build --prod --platform="${MATRIX_PLATFORM}" --arch="${MATRIX_ARCH}"
else
pnpm --filter node-smol-builder build --dev --platform="${MATRIX_PLATFORM}" --arch="${MATRIX_ARCH}"
fi
- name: Collect build metrics
if: always()
shell: bash
run: |
BUILD_MODE="${STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE}"
FINAL_DIR="packages/node-smol-builder/build/${BUILD_MODE}/out/Final"
if [ "${{ matrix.os }}" = "windows" ]; then
BINARY_NAME="node.exe"
else
BINARY_NAME="node"
fi
if [ -f "${FINAL_DIR}/${BINARY_NAME}" ]; then
if [ "${{ matrix.os }}" = "windows" ]; then
BINARY_SIZE_MB=$(powershell -Command "[math]::Round((Get-Item '${FINAL_DIR}/${BINARY_NAME}').Length / 1MB, 2)")
else
BINARY_SIZE=$(stat -f%z "${FINAL_DIR}/${BINARY_NAME}" 2>/dev/null || stat -c%s "${FINAL_DIR}/${BINARY_NAME}")
BINARY_SIZE_MB=$(echo "scale=2; $BINARY_SIZE / 1024 / 1024" | bc 2>/dev/null || awk "BEGIN {printf \"%.2f\", $BINARY_SIZE / 1024 / 1024}")
fi
echo "✅ Binary size: ${BINARY_SIZE_MB} MB"
echo "binary_size_mb=${BINARY_SIZE_MB}" >> $GITHUB_OUTPUT
else
echo "❌ Binary not found"
fi
env:
STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE: ${{ steps.smol-cache-key.outputs.build_mode }}
- name: Upload Node.js smol artifacts
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: node-smol-${{ matrix.platform }}-${{ matrix.arch }}
path: packages/node-smol-builder/build/${{ steps.smol-cache-key.outputs.build_mode }}/out/Final/node${{ matrix.os == 'windows' && '.exe' || '' }}
retention-days: 30
if-no-files-found: error
- name: Cleanup before cache save
if: always()
shell: bash
run: |
BUILD_MODE="${STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE}"
OUT_DIR="packages/node-smol-builder/build/${BUILD_MODE}/source/out"
if [ -d "$OUT_DIR" ]; then
echo "Removing build artifacts from source before cache save..."
rm -rf "$OUT_DIR"
echo "✅ Cleaned $OUT_DIR (prevents caching ~4GB of build artifacts)"
fi
env:
STEPS_SMOL_CACHE_KEY_OUTPUTS_BUILD_MODE: ${{ steps.smol-cache-key.outputs.build_mode }}
release:
needs: build
if: |
(github.event_name == 'workflow_dispatch' && !inputs.dry_run) ||
(github.event_name == 'release')
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Load Node.js version from package.json
id: tool-versions
run: |
NODE_VERSION=$(jq -r '.sources.node.version' packages/node-smol-builder/package.json)
echo "node-version=$NODE_VERSION" >> $GITHUB_OUTPUT
echo "Loaded Node.js: $NODE_VERSION"
- name: Download all artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
path: artifacts/
pattern: node-smol-*
- name: Organize release assets
run: |
mkdir -p packages/node-smol-builder/dist
# Darwin ARM64
mv artifacts/node-smol-darwin-arm64/node packages/node-smol-builder/dist/node-compiled-darwin-arm64
# Darwin x64
mv artifacts/node-smol-darwin-x64/node packages/node-smol-builder/dist/node-compiled-darwin-x64
# Linux x64
mv artifacts/node-smol-linux-x64/node packages/node-smol-builder/dist/node-compiled-linux-x64
# Linux ARM64
mv artifacts/node-smol-linux-arm64/node packages/node-smol-builder/dist/node-compiled-linux-arm64
# Linux musl x64
mv artifacts/node-smol-linux-musl-x64/node packages/node-smol-builder/dist/node-compiled-linux-musl-x64
# Linux musl ARM64
mv artifacts/node-smol-linux-musl-arm64/node packages/node-smol-builder/dist/node-compiled-linux-musl-arm64
# Windows x64
mv artifacts/node-smol-win32-x64/node.exe packages/node-smol-builder/dist/node-compiled-win32-x64.exe
# Windows ARM64 (cross-compiled)
mv artifacts/node-smol-win32-arm64/node.exe packages/node-smol-builder/dist/node-compiled-win32-arm64.exe
# Make Unix binaries executable
chmod +x packages/node-smol-builder/dist/node-compiled-*
- name: Validate all platform binaries exist
run: |
REQUIRED_FILES=(
"node-compiled-darwin-arm64"
"node-compiled-darwin-x64"
"node-compiled-linux-x64"
"node-compiled-linux-arm64"
"node-compiled-linux-musl-x64"
"node-compiled-linux-musl-arm64"
"node-compiled-win32-x64.exe"
"node-compiled-win32-arm64.exe"
)
MISSING=()
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "packages/node-smol-builder/dist/$file" ]; then
MISSING+=("$file")
fi
done
if [ ${#MISSING[@]} -ne 0 ]; then
echo "❌ Missing required binaries:"
printf '%s\n' "${MISSING[@]}"
exit 1
fi
echo "✅ All platform binaries present"
ls -lh packages/node-smol-builder/dist/
- name: Generate version
id: version
run: |
source .github/scripts/generate-version.sh
PACKAGE_NAME=$(node -p "require('./packages/node-smol-builder/package.json').name.replace('@socketbin/', '')")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
echo "Package: $PACKAGE_NAME"
- name: Generate checksums
shell: bash
run: |
cd packages/node-smol-builder/dist
# Use sha256sum on Windows/Linux, shasum on macOS
if command -v shasum &> /dev/null; then
shasum -a 256 node-compiled-* > checksums.txt
elif command -v sha256sum &> /dev/null; then
sha256sum node-compiled-* > checksums.txt
else
echo "Error: No SHA-256 command found"
exit 1
fi
cat checksums.txt
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
STEPS_VERSION_OUTPUTS_PACKAGE_NAME: ${{ steps.version.outputs.package_name }}
run: |
VERSION="${STEPS_VERSION_OUTPUTS_VERSION}"
PACKAGE_NAME="${STEPS_VERSION_OUTPUTS_PACKAGE_NAME}"
TAG="${PACKAGE_NAME}-${VERSION}"
# Check if release already exists
if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists, uploading assets..."
gh release upload "$TAG" \
packages/node-smol-builder/dist/node-compiled-* \
packages/node-smol-builder/dist/checksums.txt \
--clobber
else
echo "Creating new release $TAG..."
gh release create "$TAG" \
--title "${PACKAGE_NAME} ${VERSION}" \
--notes "Minimal Node.js v${{ steps.tool-versions.outputs.node-version }} binaries for all platforms.
## Platforms
- **macOS**: arm64, x64
- **Linux (glibc)**: x64, arm64
- **Linux (musl/Alpine)**: x64, arm64
- **Windows**: x64, arm64
## Files
- \`node-compiled-darwin-arm64\` - macOS Apple Silicon
- \`node-compiled-darwin-x64\` - macOS Intel
- \`node-compiled-linux-x64\` - Linux x64 (glibc)
- \`node-compiled-linux-arm64\` - Linux ARM64 (glibc)
- \`node-compiled-linux-musl-x64\` - Alpine Linux x64
- \`node-compiled-linux-musl-arm64\` - Alpine Linux ARM64
- \`node-compiled-win32-x64.exe\` - Windows x64
- \`node-compiled-win32-arm64.exe\` - Windows ARM64 (cross-compiled)
- \`checksums.txt\` - SHA256 checksums
## Usage
Download the appropriate binary for your platform and run it:
\`\`\`bash
./node-compiled-darwin-arm64 script.js
\`\`\`
Built from Node.js v${{ steps.tool-versions.outputs.node-version }}" \
packages/node-smol-builder/dist/node-compiled-* \
packages/node-smol-builder/dist/checksums.txt
fi