Build, Smoke & Publish #39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'], | |
| }); |