Skip to content

Build, Smoke & Publish #39

Build, Smoke & Publish

Build, Smoke & Publish #39

Workflow file for this run

name: Build, Smoke & Publish
on:
pull_request:
push:
branches: [main]
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
issues: write # smoke-registry opens an issue on failure
env:
CARGO_INCREMENTAL: 0
jobs:
# ── Build native binaries ──────────────────────────────────
build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
target: aarch64-apple-darwin
platform: darwin-arm64
- os: macos-latest
target: x86_64-apple-darwin
platform: darwin-x64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
platform: linux-x64-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
platform: linux-arm64-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
platform: win32-x64-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install dependencies
run: npm ci
- name: Build
run: npx napi build --platform --release --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.platform }}
path: duroxide.*.node
if-no-files-found: error
# ── Pack tarballs for smoke (local install simulation) ─────
pack:
name: Pack tarballs
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Download all binary artifacts
uses: actions/download-artifact@v4
with:
pattern: binary-*
merge-multiple: true
- name: Move binaries to platform packages
run: |
mv duroxide.darwin-arm64.node npm/npm/darwin-arm64/
mv duroxide.darwin-x64.node npm/npm/darwin-x64/
mv duroxide.linux-arm64-gnu.node npm/npm/linux-arm64-gnu/
mv duroxide.linux-x64-gnu.node npm/npm/linux-x64-gnu/
mv duroxide.win32-x64-msvc.node npm/npm/win32-x64-msvc/
- name: Pack platform subpackages
run: |
mkdir -p tarballs
for dir in npm/npm/*/; do
(cd "$dir" && npm pack --pack-destination "$GITHUB_WORKSPACE/tarballs")
done
- name: Pack main package
run: npm pack --pack-destination "$GITHUB_WORKSPACE/tarballs"
- name: List tarballs
run: ls -lh tarballs/
- name: Upload tarballs
uses: actions/upload-artifact@v4
with:
name: tarballs
path: tarballs/*.tgz
if-no-files-found: error
# ── Pre-publish smoke: install from local tarballs on real OSes ────
smoke-local:
name: Smoke (local) ${{ matrix.os }} node${{ matrix.node }}
needs: [pack]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: linux-x64-gnu
shell: bash
- os: ubuntu-24.04-arm
platform: linux-arm64-gnu
shell: bash
- os: macos-14 # arm64
platform: darwin-arm64
shell: bash
- os: macos-13 # x64
platform: darwin-x64
shell: bash
- os: windows-latest
platform: windows-x64
shell: pwsh
node: [20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Download tarballs
uses: actions/download-artifact@v4
with:
name: tarballs
path: tarballs
- name: List tarballs
shell: bash
run: ls -lh tarballs/
- name: Resolve tarball paths
id: paths
shell: bash
run: |
MAIN="$(ls "$GITHUB_WORKSPACE"/tarballs/duroxide-[0-9]*.tgz | head -n1)"
PLATFORM="$(ls "$GITHUB_WORKSPACE"/tarballs/duroxide-${{ matrix.platform }}-*.tgz | head -n1)"
if [ -z "$MAIN" ] || [ ! -f "$MAIN" ]; then echo "::error::main tarball missing"; exit 1; fi
if [ -z "$PLATFORM" ] || [ ! -f "$PLATFORM" ]; then echo "::error::platform tarball missing (${{ matrix.platform }})"; exit 1; fi
echo "main=$MAIN" >> "$GITHUB_OUTPUT"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Run smoke (local install)
if: matrix.shell == 'bash'
env:
TARBALL_MAIN: ${{ steps.paths.outputs.main }}
TARBALL_PLATFORM: ${{ steps.paths.outputs.platform }}
SMOKE_SCRIPT: ${{ github.workspace }}/ci/smoke/smoke.mjs
run: bash ci/smoke/run-local.sh
- name: Run smoke (local install, Windows)
if: matrix.shell == 'pwsh'
shell: pwsh
env:
TARBALL_MAIN: ${{ steps.paths.outputs.main }}
TARBALL_PLATFORM: ${{ steps.paths.outputs.platform }}
SMOKE_SCRIPT: ${{ github.workspace }}/ci/smoke/smoke.mjs
run: pwsh ci/smoke/run-local.ps1
# ── Publish to npm ─────────────────────────────────────────
publish:
name: Publish to npm
runs-on: ubuntu-latest
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: [build, smoke-local]
permissions:
contents: read
id-token: write # npm trusted publishing uses GitHub OIDC; do not use NPM_TOKEN.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false
- name: Ensure npm supports trusted publishing
run: npm install -g npm@^11.5.1
- name: Install dependencies
run: npm ci
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
pattern: binary-*
merge-multiple: true
- name: List binaries
run: ls -lh *.node
- name: Move binaries to platform packages
run: |
mv duroxide.darwin-arm64.node npm/npm/darwin-arm64/
mv duroxide.darwin-x64.node npm/npm/darwin-x64/
mv duroxide.linux-arm64-gnu.node npm/npm/linux-arm64-gnu/
mv duroxide.linux-x64-gnu.node npm/npm/linux-x64-gnu/
mv duroxide.win32-x64-msvc.node npm/npm/win32-x64-msvc/
- name: List platform packages
run: |
for dir in npm/npm/*/; do
echo "=== $(basename $dir) ==="
ls -lh "$dir"
done
- name: Determine npm dist-tag
id: disttag
run: |
VERSION=$(node -p "require('./package.json').version")
# Prerelease versions (0.1.20-rc.0, -beta.N, -alpha.N) use `next`
# tag so they don't clobber `latest` for default installs.
if [[ "$VERSION" =~ -(rc|beta|alpha)\. ]]; then
echo "tag=next" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Publishing $VERSION with dist-tag=$(grep ^tag= $GITHUB_OUTPUT | cut -d= -f2)"
- name: Publish platform packages
run: |
failed=0
for dir in npm/npm/*/; do
pkg=$(basename "$dir")
echo "Publishing duroxide-$pkg with tag=${{ steps.disttag.outputs.tag }}..."
if ! (cd "$dir" && npm publish --access public --tag "${{ steps.disttag.outputs.tag }}"); then
echo "::error::Failed to publish duroxide-$pkg"
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo "::error::One or more platform packages failed to publish. Aborting."
exit 1
fi
- name: Verify platform packages on npm
run: |
VERSION=$(node -p "require('./package.json').version")
echo "Verifying all platform packages at version $VERSION..."
PACKAGES=(
"duroxide-darwin-arm64"
"duroxide-darwin-x64"
"duroxide-linux-arm64-gnu"
"duroxide-linux-x64-gnu"
"duroxide-windows-x64"
)
failed=0
for pkg in "${PACKAGES[@]}"; do
echo -n " Checking $pkg@$VERSION... "
# Retry up to 5 times with 10s delay for registry propagation
found=0
for attempt in 1 2 3 4 5; do
if npm view "$pkg@$VERSION" version >/dev/null 2>&1; then
echo "✅ found (attempt $attempt)"
found=1
break
fi
echo -n "."
sleep 10
done
if [ "$found" -eq 0 ]; then
echo "❌ NOT FOUND after 5 attempts"
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
echo "::error::Platform package verification failed. One or more packages missing from npm."
exit 1
fi
echo "All platform packages verified on npm ✅"
- name: Publish main package
# --ignore-scripts skips `prepublishOnly: napi prepublish -t npm` which
# would regenerate optionalDependencies with napi-rs default names (e.g.
# duroxide-win32-x64-msvc), clobbering our renamed duroxide-windows-x64.
# All platform prep already happened in prior workflow steps.
run: npm publish --ignore-scripts --access public --tag "${{ steps.disttag.outputs.tag }}"
- name: Verify main package on npm
run: |
VERSION=$(node -p "require('./package.json').version")
echo "Verifying duroxide@$VERSION on npm..."
found=0
for attempt in 1 2 3 4 5; do
if npm view "duroxide@$VERSION" version >/dev/null 2>&1; then
echo "duroxide@$VERSION verified on npm ✅ (attempt $attempt)"
found=1
break
fi
echo -n "."
sleep 10
done
if [ "$found" -eq 0 ]; then
echo "::error::Main package duroxide@$VERSION not found on npm after 5 attempts"
exit 1
fi
# ── Post-publish smoke: install from real npm registry on real OSes ──
# Validates that platform subpackages resolve correctly through the dispatcher
# (index.js `require('duroxide-<platform>')`). Opens an issue on failure; does
# NOT unpublish — see duroxide-update-manager agent instructions for runbook.
smoke-registry:
name: Smoke (registry) ${{ matrix.os }} node${{ matrix.node }}
if: github.event_name == 'release'
needs: [publish]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
shell: bash
- os: ubuntu-24.04-arm
shell: bash
- os: macos-14
shell: bash
- os: macos-13
shell: bash
- os: windows-latest
shell: pwsh
node: [20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Run smoke (registry install)
if: matrix.shell == 'bash'
env:
DUROXIDE_VERSION: ${{ github.event.release.tag_name }}
SMOKE_SCRIPT: ${{ github.workspace }}/ci/smoke/smoke.mjs
run: |
# Strip optional leading `v` from tag (e.g. v0.1.20 → 0.1.20).
DUROXIDE_VERSION="${DUROXIDE_VERSION#v}"
export DUROXIDE_VERSION
bash ci/smoke/run-registry.sh
- name: Run smoke (registry install, Windows)
if: matrix.shell == 'pwsh'
shell: pwsh
env:
DUROXIDE_VERSION: ${{ github.event.release.tag_name }}
SMOKE_SCRIPT: ${{ github.workspace }}/ci/smoke/smoke.mjs
run: |
$env:DUROXIDE_VERSION = $env:DUROXIDE_VERSION -replace '^v',''
pwsh ci/smoke/run-registry.ps1
# ── Open an issue if registry smoke fails ─────────────────
smoke-registry-report:
name: Report registry smoke failure
if: github.event_name == 'release' && failure() && needs.smoke-registry.result == 'failure'
needs: [smoke-registry]
runs-on: ubuntu-latest
steps:
- name: Open issue
uses: actions/github-script@v7
with:
script: |
const tag = context.payload.release.tag_name;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Post-publish smoke FAILED for ${tag}`,
body: [
`Post-publish (registry) smoke failed for release \`${tag}\`.`,
``,
`Run: ${runUrl}`,
``,
`Consumers installing \`duroxide@${tag.replace(/^v/,'')}\` from npm may be broken on one or more platforms.`,
`The release has NOT been auto-unpublished. Action: investigate, patch, release a new version, and either deprecate (\`npm deprecate\`) or keep this release as prerelease.`,
].join('\n'),
labels: ['bug', 'release-smoke'],
});