Skip to content

Release

Release #13

Workflow file for this run

name: Release
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.dry_run }}
cancel-in-progress: true
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Run the release process without publishing to npm.'
required: false
default: false
type: boolean
jobs:
guard:
name: release-branch-guard
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Enforce release branch
run: |
RELEASE_BRANCH=dev \
RELEASE_DRY_RUN=${{ inputs.dry_run }} \
GITHUB_REF=${{ github.ref }} \
node ./scripts/verify-release-branch.js
plan:
name: plan
runs-on: ubuntu-latest
needs: guard
outputs:
prebuild_matrix: ${{ steps.prebuild_matrix.outputs.json }}
compat_matrix: ${{ steps.compat_matrix.outputs.json }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24.14.0
- name: Resolve prebuild matrix
id: prebuild_matrix
run: echo "json=$(node ./scripts/workflow-matrix.js prebuild)" >> "$GITHUB_OUTPUT"
- name: Resolve compatibility matrix
id: compat_matrix
run: echo "json=$(node ./scripts/workflow-matrix.js compat)" >> "$GITHUB_OUTPUT"
test:
name: test-${{ matrix.os }}-node-${{ matrix.node-version }}
runs-on: ${{ matrix.os }}
needs: guard
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-15]
node-version: [20.x, 22.x, 24.x]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: package-lock.json
- name: Install npm dependencies
run: npm install --omit=optional --no-audit --no-fund
- name: Run test suite
run: npm test -- --runInBand
root-package:
name: package-root
runs-on: ubuntu-latest
needs: guard
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24.14.0
cache: npm
cache-dependency-path: package-lock.json
- name: Install npm dependencies
run: npm install --omit=optional --no-audit --no-fund
- name: Build TypeScript artifacts
run: npm run build
- name: Verify root package tarball
run: npm run verify:pack
- name: Create root package tarball
run: node ./scripts/create-package-tarball.js --out-dir=artifacts
- name: Upload root package artifact
uses: actions/upload-artifact@v7
with:
name: package-function-location
path: artifacts/*.tgz
if-no-files-found: error
prebuild:
name: prebuild-${{ matrix.packageName }}
runs-on: ${{ matrix.runner }}
needs: plan
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.prebuild_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.nodeVersion }}
cache: npm
cache-dependency-path: package-lock.json
- name: Install npm dependencies
run: npm install --omit=optional --no-audit --no-fund
- name: Build platform prebuilds
run: npm run build:prebuilds -- --package=${{ matrix.packageName }}
- name: Verify platform prebuilds
run: npm run verify:prebuild -- --package=${{ matrix.packageName }}
- name: Verify platform package tarball
run: node ./scripts/verify-pack-tarball.js --package-dir=${{ matrix.packageDir }}
- name: Create platform package tarball
run: node ./scripts/create-package-tarball.js --package-dir=${{ matrix.packageDir }} --out-dir=artifacts
- name: Upload platform package artifact
uses: actions/upload-artifact@v7
with:
name: package-${{ matrix.packageName }}
path: artifacts/*.tgz
if-no-files-found: error
compatibility:
name: compat-${{ matrix.compatibilityLabel }}-node-${{ matrix.nodeVersion }}
runs-on: ${{ matrix.runner }}
needs: [plan, root-package, prebuild]
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.plan.outputs.compat_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.nodeVersion }}
architecture: ${{ matrix.nodeArchitecture }}
- name: Download root package artifact
uses: actions/download-artifact@v8
with:
name: package-function-location
path: artifacts/root
- name: Download platform package artifact
uses: actions/download-artifact@v8
with:
name: package-${{ matrix.packageName }}
path: artifacts/platform
- name: Install packed packages and run smoke test
shell: bash
run: |
root_tarball="$(find artifacts/root -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
platform_tarball="$(find artifacts/platform -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
if [ -z "$root_tarball" ] || [ -z "$platform_tarball" ]; then
echo "Required tarball artifacts were not downloaded."
exit 1
fi
node ./scripts/run-compat-smoke.js \
--root-tarball="$root_tarball" \
--platform-tarball="$platform_tarball" \
--expected-node-arch="${{ matrix.nodeArchitecture }}" \
--expected-host-arm64="${{ matrix.expectedHostArm64 }}" \
--expected-translated="${{ matrix.expectedTranslated }}"
publish:
name: publish
runs-on: ubuntu-latest
needs: compatibility
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24.14.0
- name: Download package artifacts
uses: actions/download-artifact@v8
with:
pattern: package-*
path: artifacts
- name: Verify package metadata alignment
id: package_metadata
run: |
packages_json="$(node ./scripts/package-metadata.js)"
root_name="$(node -p "require('./package.json').name")"
root_version="$(node -p "require('./package.json').version")"
publish_tag="$(node -p "require('./package.json').version.includes('-') ? 'next' : 'latest'")"
printf '%s\n' "$packages_json" > "$RUNNER_TEMP/release-packages.json"
echo "root_name=$root_name" >> "$GITHUB_OUTPUT"
echo "root_version=$root_version" >> "$GITHUB_OUTPUT"
echo "publish_tag=$publish_tag" >> "$GITHUB_OUTPUT"
- name: Verify release versions are new
if: ${{ !inputs.dry_run }}
run: |
node - "$RUNNER_TEMP/release-packages.json" <<'EOF' > "$RUNNER_TEMP/release-package-specs.txt"
const fs = require('fs');
const packageSpecs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
for (const entry of packageSpecs) {
console.log(`${entry.name}@${entry.version}`);
}
EOF
while IFS= read -r spec; do
if npm view "$spec" version >/tmp/npm-view.out 2>/tmp/npm-view.err; then
echo "$spec already exists on npm."
echo "Publish would overwrite an existing release. Bump package versions before retrying."
exit 1
fi
if ! grep -q "E404" /tmp/npm-view.err; then
echo "Failed to verify npm version availability for $spec."
cat /tmp/npm-view.err
exit 1
fi
done < "$RUNNER_TEMP/release-package-specs.txt"
- name: Skip version uniqueness check for dry run
if: ${{ inputs.dry_run }}
run: echo "Dry run enabled; skipping npm version uniqueness enforcement."
- name: Build .npm auth config
if: ${{ !inputs.dry_run }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$NODE_AUTH_TOKEN" ]; then
echo "NPM_TOKEN is not set."
exit 1
fi
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > "$RUNNER_TEMP/npmrc"
echo "NPM_CONFIG_USERCONFIG=$RUNNER_TEMP/npmrc" >> "$GITHUB_ENV"
- name: Dry run publish
if: ${{ inputs.dry_run }}
shell: bash
run: |
root_tarball="$(find "artifacts/package-function-location" -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
mapfile -t platform_names < <(node - "$RUNNER_TEMP/release-packages.json" <<'EOF'
const fs = require('fs');
const packages = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
for (const entry of packages.slice(1)) {
console.log(entry.name);
}
EOF
)
if [ -z "$root_tarball" ] || [ "${#platform_names[@]}" -eq 0 ]; then
echo "Release tarball artifacts are incomplete."
exit 1
fi
for package_name in "${platform_names[@]}"; do
tarball="$(find "artifacts/package-$package_name" -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
if [ -z "$tarball" ]; then
echo "Missing tarball for $package_name."
exit 1
fi
npm publish "$tarball" --access public --dry-run --ignore-scripts --tag dry-run
done
npm publish "$root_tarball" --access public --dry-run --ignore-scripts --tag dry-run
- name: Publish packages
if: ${{ !inputs.dry_run }}
shell: bash
run: |
root_tarball="$(find "artifacts/package-function-location" -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
mapfile -t platform_names < <(node - "$RUNNER_TEMP/release-packages.json" <<'EOF'
const fs = require('fs');
const packages = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
for (const entry of packages.slice(1)) {
console.log(entry.name);
}
EOF
)
if [ -z "$root_tarball" ] || [ "${#platform_names[@]}" -eq 0 ]; then
echo "Release tarball artifacts are incomplete."
exit 1
fi
for package_name in "${platform_names[@]}"; do
tarball="$(find "artifacts/package-$package_name" -maxdepth 1 -name '*.tgz' -type f | head -n 1)"
if [ -z "$tarball" ]; then
echo "Missing tarball for $package_name."
exit 1
fi
npm publish "$tarball" --access public --tag "${{ steps.package_metadata.outputs.publish_tag }}"
done
npm publish "$root_tarball" --access public --tag "${{ steps.package_metadata.outputs.publish_tag }}"