Skip to content

fix: update test server to bind to dynamic port for compatibility wit… #211

fix: update test server to bind to dynamic port for compatibility wit…

fix: update test server to bind to dynamic port for compatibility wit… #211

Workflow file for this run

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