fix: update test server to bind to dynamic port for compatibility wit… #211
Workflow file for this run
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: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| build_linux_x86: | |
| description: "Build Linux x86" | |
| type: boolean | |
| required: false | |
| default: true | |
| build_linux_arm: | |
| description: "Build Linux ARM" | |
| type: boolean | |
| required: false | |
| default: true | |
| build_macos: | |
| description: "Build macOS" | |
| type: boolean | |
| required: false | |
| default: true | |
| build_windows: | |
| description: "Build Windows" | |
| type: boolean | |
| required: false | |
| default: true | |
| publish_npm: | |
| description: "Publish to npm" | |
| type: boolean | |
| required: false | |
| default: true | |
| publish_crates: | |
| description: "Publish to crates.io" | |
| type: boolean | |
| required: false | |
| default: true | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write # Required for npm trusted publishing with OIDC (no token needed) | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| pre-flight-checks: | |
| name: Pre-flight checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Check version synchronization | |
| run: ./scripts/sync-version.sh | |
| prepare: | |
| name: Prepare release | |
| needs: pre-flight-checks | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.create_release.outputs.tag }} | |
| is_prerelease: ${{ steps.detect_prerelease.outputs.is_prerelease }} | |
| npm_tag: ${{ steps.detect_prerelease.outputs.npm_tag }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - id: create_release | |
| name: Get version via latest git tag | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: ./scripts/workflow-create-release.sh | |
| - id: detect_prerelease | |
| name: Detect prerelease type | |
| run: | | |
| TAG="${{ steps.create_release.outputs.tag }}" | |
| # Check if tag contains prerelease identifier | |
| if [[ "$TAG" =~ -alpha ]]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "npm_tag=alpha" >> $GITHUB_OUTPUT | |
| elif [[ "$TAG" =~ -beta ]]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "npm_tag=beta" >> $GITHUB_OUTPUT | |
| elif [[ "$TAG" =~ -rc ]]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "npm_tag=rc" >> $GITHUB_OUTPUT | |
| elif [[ "$TAG" =~ -dev ]]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "npm_tag=dev" >> $GITHUB_OUTPUT | |
| else | |
| echo "is_prerelease=false" >> $GITHUB_OUTPUT | |
| echo "npm_tag=latest" >> $GITHUB_OUTPUT | |
| fi | |
| echo "Release: $TAG" | |
| echo "Prerelease: $(grep is_prerelease $GITHUB_OUTPUT | cut -d= -f2)" | |
| echo "npm tag: $(grep npm_tag $GITHUB_OUTPUT | cut -d= -f2)" | |
| build-linux-arm: | |
| name: Build Linux ARM | |
| runs-on: ubuntu-24.04-arm | |
| needs: prepare | |
| env: | |
| BUILD_ENABLED: ${{ github.event_name != 'workflow_dispatch' || inputs.build_linux_arm }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| libc: [gnu, musl] | |
| steps: | |
| - name: Gate — fast pass when deselected | |
| if: env.BUILD_ENABLED != 'true' | |
| run: echo "Linux ARM build deselected via workflow_dispatch; fast-passing job." | |
| - name: Checkout code | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Login to Docker Hub | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Test and Build Binary | |
| if: env.BUILD_ENABLED == 'true' | |
| run: | | |
| docker buildx build \ | |
| --platform="linux/arm64" \ | |
| --file="docker/build-linux.Dockerfile" \ | |
| --build-arg="ARCH=aarch64" \ | |
| --build-arg="LIBC=${{ matrix.libc }}" \ | |
| --build-arg="RUN_TESTS=false" \ | |
| --progress="plain" \ | |
| --cache-from=type=gha,scope=${{ matrix.libc }}-aarch64 \ | |
| --cache-to=type=gha,mode=max,scope=${{ matrix.libc }}-aarch64 \ | |
| --output="type=local,dest=output/" \ | |
| . | |
| - name: Pack and upload CLI binary | |
| if: env.BUILD_ENABLED == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: ./scripts/workflow-pack-upload.sh "output" "linux-${{ matrix.libc }}-aarch64" "${{ needs.prepare.outputs.tag }}" | |
| - name: Upload NAPI binding as artifact | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-binding-linux-${{ matrix.libc }}-aarch64 | |
| path: output/node/libversatiles_node.so | |
| retention-days: 1 | |
| # Re-run safe: replace a same-named artifact from a prior attempt. | |
| overwrite: true | |
| build-linux-x86: | |
| name: Build Linux x86 | |
| runs-on: ubuntu-24.04 | |
| needs: prepare | |
| env: | |
| BUILD_ENABLED: ${{ github.event_name != 'workflow_dispatch' || inputs.build_linux_x86 }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| libc: [gnu, musl] | |
| steps: | |
| - name: Gate — fast pass when deselected | |
| if: env.BUILD_ENABLED != 'true' | |
| run: echo "Linux x86 build deselected via workflow_dispatch; fast-passing job." | |
| - name: Checkout code | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Login to Docker Hub | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Test and Build Binary | |
| if: env.BUILD_ENABLED == 'true' | |
| run: | | |
| docker buildx build \ | |
| --platform="linux/amd64" \ | |
| --file="docker/build-linux.Dockerfile" \ | |
| --build-arg="ARCH=x86_64" \ | |
| --build-arg="LIBC=${{ matrix.libc }}" \ | |
| --build-arg="RUN_TESTS=false" \ | |
| --progress="plain" \ | |
| --cache-from=type=gha,scope=${{ matrix.libc }}-x86_64 \ | |
| --cache-to=type=gha,mode=max,scope=${{ matrix.libc }}-x86_64 \ | |
| --output="type=local,dest=output/" \ | |
| . | |
| - name: Pack and upload CLI binary | |
| if: env.BUILD_ENABLED == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: ./scripts/workflow-pack-upload.sh "output" "linux-${{ matrix.libc }}-x86_64" "${{ needs.prepare.outputs.tag }}" | |
| - name: Upload NAPI binding as artifact | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-binding-linux-${{ matrix.libc }}-x86_64 | |
| path: output/node/libversatiles_node.so | |
| retention-days: 1 | |
| # Re-run safe: replace a same-named artifact from a prior attempt. | |
| overwrite: true | |
| build-macos: | |
| name: Build MacOS | |
| runs-on: ${{ matrix.runner }} | |
| needs: prepare | |
| env: | |
| BUILD_ENABLED: ${{ github.event_name != 'workflow_dispatch' || inputs.build_macos }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Use native runners to avoid cross-compilation; pkg-config finds OpenSSL without extra setup. | |
| - arch: x86_64 | |
| runner: macos-26-intel | |
| - arch: aarch64 | |
| runner: macos-latest | |
| steps: | |
| - name: Gate — fast pass when deselected | |
| if: env.BUILD_ENABLED != 'true' | |
| run: echo "macOS build deselected via workflow_dispatch; fast-passing job." | |
| - name: Checkout code | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.arch }}-apple-darwin | |
| - name: Cache cargo/macOS | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: macos-${{ matrix.arch }} | |
| cache-bin: false | |
| - name: Run Tests | |
| if: env.BUILD_ENABLED == 'true' | |
| run: cargo test | |
| - name: Build CLI Binary | |
| if: env.BUILD_ENABLED == 'true' | |
| run: cargo build --bin "versatiles" --package "versatiles" --release --target "${{ matrix.arch }}-apple-darwin" | |
| - name: Build NAPI Binding | |
| if: env.BUILD_ENABLED == 'true' | |
| run: cargo build --package "versatiles_node" --release --target "${{ matrix.arch }}-apple-darwin" | |
| - name: Prepare release structure | |
| if: env.BUILD_ENABLED == 'true' | |
| run: | | |
| mkdir -p "target/${{ matrix.arch }}-apple-darwin/release/cli" | |
| mkdir -p "target/${{ matrix.arch }}-apple-darwin/release/node" | |
| cp "target/${{ matrix.arch }}-apple-darwin/release/versatiles" "target/${{ matrix.arch }}-apple-darwin/release/cli/" | |
| cp "target/${{ matrix.arch }}-apple-darwin/release/libversatiles_node.dylib" "target/${{ matrix.arch }}-apple-darwin/release/node/" | |
| - name: Pack and upload CLI binary | |
| if: env.BUILD_ENABLED == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: ./scripts/workflow-pack-upload.sh "target/${{ matrix.arch }}-apple-darwin/release" "macos-${{ matrix.arch }}" "${{ needs.prepare.outputs.tag }}" | |
| - name: Upload NAPI binding as artifact | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-binding-macos-${{ matrix.arch }} | |
| path: target/${{ matrix.arch }}-apple-darwin/release/node/libversatiles_node.dylib | |
| retention-days: 1 | |
| # Re-run safe: replace a same-named artifact from a prior attempt. | |
| overwrite: true | |
| build-windows: | |
| name: Build Windows | |
| runs-on: windows-latest | |
| needs: prepare | |
| env: | |
| BUILD_ENABLED: ${{ github.event_name != 'workflow_dispatch' || inputs.build_windows }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x86_64, aarch64] | |
| steps: | |
| - name: Gate — fast pass when deselected | |
| if: env.BUILD_ENABLED != 'true' | |
| run: echo "Windows build deselected via workflow_dispatch; fast-passing job." | |
| - name: Configure Git | |
| if: env.BUILD_ENABLED == 'true' | |
| shell: bash | |
| run: | | |
| # Cargo's libgit2 needs core.longpaths to clone the gdal git dep, | |
| # whose `gdal-src/source` submodule contains files whose absolute | |
| # path exceeds Windows' MAX_PATH (260). Remove when the gdal crate | |
| # is back on crates.io (see versatiles_pipeline/Cargo.toml). | |
| git config --global core.longpaths true | |
| - name: Checkout code | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.arch }}-pc-windows-msvc | |
| - name: Cache cargo/Windows | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: windows-${{ matrix.arch }} | |
| cache-bin: false | |
| - name: Run Tests | |
| if: env.BUILD_ENABLED == 'true' && matrix.arch == 'x86_64' | |
| run: | | |
| cargo test --bins | |
| cargo test --lib | |
| - name: Build CLI binary | |
| if: env.BUILD_ENABLED == 'true' | |
| run: cargo build --bin "versatiles" --package "versatiles" --release --target "${{ matrix.arch }}-pc-windows-msvc" | |
| - name: Build NAPI binding | |
| if: env.BUILD_ENABLED == 'true' | |
| run: cargo build --package "versatiles_node" --release --target "${{ matrix.arch }}-pc-windows-msvc" | |
| - name: Prepare release structure | |
| if: env.BUILD_ENABLED == 'true' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path "target\${{ matrix.arch }}-pc-windows-msvc\release\cli" | |
| New-Item -ItemType Directory -Force -Path "target\${{ matrix.arch }}-pc-windows-msvc\release\node" | |
| Copy-Item "target\${{ matrix.arch }}-pc-windows-msvc\release\versatiles.exe" "target\${{ matrix.arch }}-pc-windows-msvc\release\cli\" | |
| Copy-Item "target\${{ matrix.arch }}-pc-windows-msvc\release\versatiles_node.dll" "target\${{ matrix.arch }}-pc-windows-msvc\release\node\" | |
| - name: Pack and upload CLI binary | |
| if: env.BUILD_ENABLED == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: scripts\workflow-pack-upload.ps1 "target\${{ matrix.arch }}-pc-windows-msvc\release" "windows-${{ matrix.arch }}" "${{ needs.prepare.outputs.tag }}" | |
| - name: Upload NAPI binding as artifact | |
| if: env.BUILD_ENABLED == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: node-binding-windows-${{ matrix.arch }} | |
| path: target\${{ matrix.arch }}-pc-windows-msvc\release\node\versatiles_node.dll | |
| retention-days: 1 | |
| # Re-run safe: replace a same-named artifact from a prior attempt. | |
| overwrite: true | |
| package-npm-binaries: | |
| name: Package npm binaries | |
| needs: | |
| - prepare | |
| - build-linux-x86 | |
| - build-linux-arm | |
| - build-macos | |
| - build-windows | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| working-directory: ./versatiles_node | |
| run: npm install | |
| - name: Download NAPI bindings from workflow artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: node-binding-* | |
| path: artifacts/ | |
| merge-multiple: false | |
| - name: Rename binaries to NAPI convention | |
| run: | | |
| mkdir -p versatiles_node | |
| # Function to rename binaries | |
| rename_binary() { | |
| local source_path=$1 | |
| local target_name=$2 | |
| if [ -f "$source_path" ]; then | |
| cp "$source_path" "versatiles_node/$target_name" | |
| chmod +x "versatiles_node/$target_name" | |
| echo "✓ Created $target_name" | |
| else | |
| echo "✗ Missing: $source_path" | |
| exit 1 | |
| fi | |
| } | |
| # Linux x64 | |
| rename_binary "artifacts/node-binding-linux-gnu-x86_64/libversatiles_node.so" "versatiles.linux-x64-gnu.node" | |
| rename_binary "artifacts/node-binding-linux-musl-x86_64/libversatiles_node.so" "versatiles.linux-x64-musl.node" | |
| # Linux ARM64 | |
| rename_binary "artifacts/node-binding-linux-gnu-aarch64/libversatiles_node.so" "versatiles.linux-arm64-gnu.node" | |
| rename_binary "artifacts/node-binding-linux-musl-aarch64/libversatiles_node.so" "versatiles.linux-arm64-musl.node" | |
| # macOS | |
| rename_binary "artifacts/node-binding-macos-x86_64/libversatiles_node.dylib" "versatiles.darwin-x64.node" | |
| rename_binary "artifacts/node-binding-macos-aarch64/libversatiles_node.dylib" "versatiles.darwin-arm64.node" | |
| # Windows | |
| rename_binary "artifacts/node-binding-windows-x86_64/versatiles_node.dll" "versatiles.win32-x64-msvc.node" | |
| rename_binary "artifacts/node-binding-windows-aarch64/versatiles_node.dll" "versatiles.win32-arm64-msvc.node" | |
| echo "" | |
| echo "All NAPI bindings prepared:" | |
| ls -lah versatiles_node/*.node | |
| - name: Generate platform packages | |
| working-directory: ./versatiles_node | |
| run: | | |
| # Create platform-specific package directories with package.json files | |
| npx napi create-npm-dirs | |
| # Copy .node files to their platform directories and pack them | |
| for platform in darwin-arm64 darwin-x64 linux-arm64-gnu linux-arm64-musl linux-x64-gnu linux-x64-musl win32-arm64-msvc win32-x64-msvc; do | |
| if [ -f "versatiles.$platform.node" ]; then | |
| cp "versatiles.$platform.node" "npm/$platform/" | |
| (cd "npm/$platform" && npm pack) | |
| echo "✓ Created package for $platform" | |
| else | |
| echo "✗ Missing versatiles.$platform.node" | |
| fi | |
| done | |
| # List created packages | |
| echo "" | |
| echo "Platform packages created:" | |
| ls -lah npm/*/*.tgz | |
| - name: Upload npm packages as artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: npm-packages | |
| path: versatiles_node/npm/*/*.tgz | |
| retention-days: 7 | |
| # Re-run safe: replace a same-named artifact from a prior attempt. | |
| overwrite: true | |
| publish-npm: | |
| name: Publish to npm | |
| needs: | |
| - prepare | |
| - package-npm-binaries | |
| runs-on: ubuntu-latest | |
| env: | |
| PUBLISH_ENABLED: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_npm }} | |
| steps: | |
| - name: Gate — fast pass when deselected | |
| if: env.PUBLISH_ENABLED != 'true' | |
| run: echo "npm publish deselected via workflow_dispatch; fast-passing job." | |
| - name: Checkout code | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Ensure npm supports OIDC trusted publishing | |
| if: env.PUBLISH_ENABLED == 'true' | |
| run: | | |
| echo "Current npm version: $(npm --version)" | |
| # `npm install -g npm@latest` has been failing on ubuntu-latest | |
| # because the runner image's bundled npm 10.9.7 ships an | |
| # incomplete @npmcli/arborist (missing promise-retry), so the | |
| # bundled npm can't install another npm. corepack fetches the | |
| # tarball directly from the registry and activates it via a | |
| # shim — no dependency on the broken global tree. | |
| corepack enable npm | |
| corepack prepare npm@latest --activate | |
| echo "Updated npm version: $(npm --version)" | |
| - name: Install dependencies | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| run: npm install | |
| - name: Download npm packages | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: npm-packages | |
| path: versatiles_node/npm-downloaded | |
| - name: Move packages to correct location | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| run: | | |
| # The artifact download may flatten the directory structure | |
| # Let's see what we got and reorganize if needed | |
| echo "Downloaded artifacts:" | |
| find npm-downloaded -type f -name "*.tgz" || echo "No .tgz files found" | |
| # Create npm directory structure | |
| mkdir -p npm | |
| # Move tgz files to npm directory, preserving or recreating structure | |
| if [ -d "npm-downloaded" ]; then | |
| # If files are in subdirectories, preserve them | |
| if ls npm-downloaded/*/*.tgz 1> /dev/null 2>&1; then | |
| mv npm-downloaded/* npm/ | |
| # If files are flattened, just move them | |
| elif ls npm-downloaded/*.tgz 1> /dev/null 2>&1; then | |
| mv npm-downloaded/*.tgz npm/ | |
| fi | |
| fi | |
| echo "Final npm directory structure:" | |
| find npm -type f -name "*.tgz" || echo "No .tgz files in npm directory" | |
| - name: Validate package version | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| run: | | |
| TAG_VERSION="${{ needs.prepare.outputs.tag }}" | |
| TAG_VERSION="${TAG_VERSION#v}" | |
| PKG_VERSION=$(node -p "require('./package.json').version") | |
| if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then | |
| echo "ERROR: package.json version ($PKG_VERSION) doesn't match tag ($TAG_VERSION)" | |
| exit 1 | |
| fi | |
| echo "✓ Version validated" | |
| - name: Download NAPI bindings for main package | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: node-binding-* | |
| path: node-bindings/ | |
| merge-multiple: false | |
| - name: Copy native binding for build | |
| if: env.PUBLISH_ENABLED == 'true' | |
| run: | | |
| # Copy any available native binding to enable the build | |
| # The actual platform-specific bindings will be downloaded by npm | |
| if [ -f "node-bindings/node-binding-linux-gnu-x86_64/libversatiles_node.so" ]; then | |
| cp "node-bindings/node-binding-linux-gnu-x86_64/libversatiles_node.so" "versatiles_node/versatiles.linux-x64-gnu.node" | |
| elif [ -f "node-bindings/node-binding-macos-x86_64/libversatiles_node.dylib" ]; then | |
| cp "node-bindings/node-binding-macos-x86_64/libversatiles_node.dylib" "versatiles_node/versatiles.darwin-x64.node" | |
| elif [ -f "node-bindings/node-binding-macos-aarch64/libversatiles_node.dylib" ]; then | |
| cp "node-bindings/node-binding-macos-aarch64/libversatiles_node.dylib" "versatiles_node/versatiles.darwin-arm64.node" | |
| fi | |
| echo "Native binding copied for build" | |
| - name: Build JavaScript wrappers | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| run: | | |
| echo "Building index.cjs and index.js..." | |
| npm run build:debug | |
| # Remove native bindings - they shouldn't be in the main package | |
| echo "Removing native bindings from main package..." | |
| rm -f versatiles.*.node | |
| echo "" | |
| echo "Generated files:" | |
| ls -lh index.* | |
| echo "" | |
| echo "✓ JavaScript wrappers built (native bindings removed)" | |
| - name: Validate package contents | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| run: | | |
| echo "Validating package contents..." | |
| # Check that wrapper files exist | |
| for file in index.js index.cjs index.d.ts; do | |
| if [ ! -f "$file" ]; then | |
| echo "ERROR: Missing required file: $file" | |
| exit 1 | |
| fi | |
| done | |
| echo "✓ All required files present and valid" | |
| echo "✓ No native bindings in main package (correct)" | |
| - name: Publish npm packages | |
| if: env.PUBLISH_ENABLED == 'true' | |
| working-directory: ./versatiles_node | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| NPM_TAG="${{ needs.prepare.outputs.npm_tag }}" | |
| echo "Publishing npm packages with tag: $NPM_TAG" | |
| # Publish a package idempotently, so the job is safe to re-run after a | |
| # partial or flaky failure: | |
| # - skip if that exact name@version is already on the registry; | |
| # - tolerate the Sigstore transparency-log 409 ("equivalent entry | |
| # already exists"), which npm can raise *after* the package itself | |
| # landed, by re-checking the registry instead of trusting the exit | |
| # code; retry once if it genuinely did not publish. | |
| # Args: <name> <version> <target> (target: a .tgz path or "." for cwd) | |
| publish_idempotent() { | |
| local name="$1" version="$2" target="$3" | |
| local spec="$name@$version" | |
| if npm view "$spec" version >/dev/null 2>&1; then | |
| echo "✓ $spec already on npm — skipping" | |
| return 0 | |
| fi | |
| local attempt | |
| for attempt in 1 2; do | |
| echo "Publishing $spec ($target) — attempt $attempt" | |
| if npm publish "$target" --access public --provenance --tag "$NPM_TAG"; then | |
| echo "✓ Published $spec" | |
| return 0 | |
| fi | |
| echo "⚠️ publish of $spec reported an error — re-checking the registry…" | |
| sleep 15 | |
| if npm view "$spec" version >/dev/null 2>&1; then | |
| echo "✓ $spec is on npm despite the error — treating as published" | |
| return 0 | |
| fi | |
| done | |
| echo "❌ $spec failed to publish after 2 attempts" | |
| return 1 | |
| } | |
| # --- Platform packages (must publish before the main package, which | |
| # lists them as optionalDependencies) --- | |
| packages=$(find npm -name "*.tgz" -type f) | |
| if [ -z "$packages" ]; then | |
| echo "ERROR: No .tgz files found in npm directory" | |
| exit 1 | |
| fi | |
| echo "Found platform packages:" | |
| echo "$packages" | |
| for pkg in $packages; do | |
| echo "" | |
| meta=$(tar -xzOf "$pkg" package/package.json) | |
| name=$(node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).name" <<<"$meta") | |
| version=$(node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" <<<"$meta") | |
| publish_idempotent "$name" "$version" "$pkg" | |
| done | |
| # --- Main package --- | |
| echo "" | |
| main_name=$(node -p "require('./package.json').name") | |
| main_version=$(node -p "require('./package.json').version") | |
| publish_idempotent "$main_name" "$main_version" "." | |
| echo "" | |
| echo "✓ All npm packages published" | |
| - name: Verify publication | |
| if: env.PUBLISH_ENABLED == 'true' | |
| run: | | |
| sleep 60 | |
| TAG_VERSION="${{ needs.prepare.outputs.tag }}" | |
| TAG_VERSION="${TAG_VERSION#v}" | |
| echo "Verifying publication of version: $TAG_VERSION" | |
| npm view @versatiles/versatiles-rs@$TAG_VERSION version | |
| npm view @versatiles/versatiles-rs-darwin-arm64@$TAG_VERSION version | |
| publish-crates: | |
| name: Publish to crates.io | |
| needs: | |
| - prepare | |
| - build-linux-x86 | |
| - build-linux-arm | |
| - build-macos | |
| - build-windows | |
| runs-on: ubuntu-latest | |
| env: | |
| PUBLISH_ENABLED: ${{ (github.event_name != 'workflow_dispatch' || inputs.publish_crates) && needs.prepare.outputs.is_prerelease == 'false' }} | |
| steps: | |
| - name: Gate — fast pass when deselected or prerelease | |
| if: env.PUBLISH_ENABLED != 'true' | |
| run: echo "crates.io publish deselected or is prerelease; fast-passing job." | |
| - name: Checkout code | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.prepare.outputs.tag }} | |
| - name: Install Rust toolchain | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build artifacts | |
| if: env.PUBLISH_ENABLED == 'true' | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: 'crates-publish' | |
| cache-bin: false | |
| - name: Publish crates to crates.io | |
| if: env.PUBLISH_ENABLED == 'true' | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} | |
| run: | | |
| set -o pipefail | |
| echo "Publishing crates in dependency order..." | |
| # Publish in dependency order (derived from Cargo.toml dependencies) | |
| crates=( | |
| "versatiles_derive" | |
| "versatiles_core" | |
| "versatiles_geometry" | |
| "versatiles_image" | |
| "versatiles_container" | |
| "versatiles_pipeline" | |
| "versatiles" | |
| ) | |
| for crate in "${crates[@]}"; do | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "Processing $crate..." | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| cd "$crate" | |
| VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') | |
| # Skip if this version is already on crates.io — makes the job | |
| # safe to re-run after a partial publish. Any non-200 (incl. a | |
| # network error → "000") falls through to the publish attempt, | |
| # which has its own already-published guard below. | |
| http=$(curl -sS -o /dev/null -w '%{http_code}' \ | |
| -A 'versatiles-rs-release-ci' \ | |
| "https://crates.io/api/v1/crates/$crate/$VERSION" || echo "000") | |
| if [ "$http" = "200" ]; then | |
| echo "✓ $crate@$VERSION already on crates.io — skipping" | |
| cd .. | |
| continue | |
| fi | |
| # Try to publish, but don't fail if the version already exists | |
| echo "Publishing $crate@$VERSION..." | |
| if cargo publish --allow-dirty 2>&1 | tee /tmp/publish.log; then | |
| echo "✓ Published $crate@$VERSION" | |
| # Wait to avoid rate limiting (except for the last crate) | |
| if [ "$crate" != "versatiles" ]; then | |
| echo "Waiting 30 seconds before next publish..." | |
| sleep 30 | |
| fi | |
| else | |
| # Tolerate a version that already exists (crates.io words this as | |
| # "already uploaded"); anything else is a real error. | |
| if grep -qiE "already (uploaded|exists)" /tmp/publish.log; then | |
| echo "⚠️ $crate@$VERSION already on crates.io — skipping" | |
| else | |
| # It's a real error, fail the build | |
| echo "❌ Failed to publish $crate@$VERSION" | |
| cat /tmp/publish.log | |
| exit 1 | |
| fi | |
| fi | |
| cd .. | |
| done | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "✓ Crates publish process completed!" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| finish-release: | |
| name: Finish release | |
| needs: | |
| - prepare | |
| - build-linux-x86 | |
| - build-linux-arm | |
| - build-macos | |
| - build-windows | |
| - publish-npm | |
| - publish-crates | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Set Environment Variables | |
| run: | | |
| echo "TAG=${{ needs.prepare.outputs.tag }}" >> $GITHUB_ENV | |
| echo "IS_PRERELEASE=${{ needs.prepare.outputs.is_prerelease }}" >> $GITHUB_ENV | |
| - name: Update and Release Install scripts | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| mkdir -p release | |
| OLD_URL="versatiles-org/versatiles-rs/releases/latest/download/" | |
| NEW_URL="versatiles-org/versatiles-rs/releases/download/${TAG}/" | |
| sed "s|${OLD_URL}|${NEW_URL}|g" ./scripts/install-unix.sh > ./release/install-unix.sh | |
| sed "s|${OLD_URL}|${NEW_URL}|g" ./scripts/install-windows.ps1 > ./release/install-windows.ps1 | |
| gh auth status -h github.com | |
| gh release upload "${TAG}" ./release/install-unix.sh --clobber | |
| gh release upload "${TAG}" ./release/install-windows.ps1 --clobber | |
| - name: Finalize the release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ "$IS_PRERELEASE" = "true" ]; then | |
| echo "Publishing as prerelease (no --latest flag)" | |
| gh release edit "$TAG" --draft=false --prerelease | |
| else | |
| echo "Publishing as stable release" | |
| gh release edit "$TAG" --draft=false --latest --prerelease=false | |
| fi | |
| - name: Trigger Docker release | |
| if: needs.prepare.outputs.is_prerelease == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN }} | |
| run: | | |
| echo "Triggering Docker release for stable version" | |
| gh auth status -h github.com | |
| gh workflow view release.yml --repo versatiles-org/versatiles-docker --yaml | |
| gh workflow run release.yml --repo versatiles-org/versatiles-docker --ref main | |
| - name: Trigger Homebrew formula update | |
| if: needs.prepare.outputs.is_prerelease == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN }} | |
| run: | | |
| echo "Triggering Homebrew formula update for stable version" | |
| gh workflow run update_formula.yml --repo versatiles-org/homebrew-versatiles --ref main |