From 6c023ac9c4564d2a490a0f9e72ba530b96820b20 Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Thu, 27 Aug 2026 15:56:29 +0200 Subject: [PATCH 1/4] Add npm publishing step for CLI --- .github/workflows/ci.yml | 382 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 353 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fea8b2b..74d1994 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,6 @@ name: ci -on: - push: - branches: [next, main] - pull_request: - types: [opened, synchronize, reopened] +on: [push] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,6 +16,26 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: cargo-${{ runner.os }}-check-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}-check- + - name: Check run: cargo check @@ -29,45 +45,353 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: cargo-${{ runner.os }}-compile-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}-compile- + - name: Compile run: cargo build - audit: + test: runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v6 - # A regeneration that adds a dependency without refreshing Cargo.lock - # leaves it unresolvable, which silently disables `cargo audit`. Fail here - # instead: `--locked` proves the committed lockfile still describes the - # dependency graph. The fix is to run `cargo build` and commit the lock. - - name: Verify Cargo.lock is complete - run: cargo metadata --locked --format-version 1 > /dev/null + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi - - name: Install cargo-audit - run: cargo install cargo-audit --locked + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: cargo-${{ runner.os }}-test-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}-test- - # Fails on advisories only. Warnings (unmaintained, yanked) are left - # non-blocking on purpose: the dependency graph comes from the generator, - # so a warning we cannot promptly fix would block every unrelated PR. - - name: Audit dependencies - run: cargo audit + - name: Test + run: cargo test - test: + # The npm packages take their version from the release tag, while the + # binary reports the version in Cargo.toml. Publishing when they disagree + # ships a package whose --version names a release that isn't on the + # registry, so refuse to publish instead (cargo-dist hard-fails on the + # same mismatch for the GitHub Release). + version: + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@v6 - - name: Test - run: cargo test + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + - name: Check tag matches crate version + shell: bash + run: | + set -euo pipefail + + TAG_VERSION="${GITHUB_REF_NAME#v}" + CRATE_VERSION=$(cargo metadata --no-deps --format-version 1 \ + | jq -r --arg manifest "${PWD}/Cargo.toml" \ + '.packages[] | select(.manifest_path == $manifest) | .version') + + if [[ -z "${CRATE_VERSION}" ]]; then + echo "::error::Could not determine the crate version from cargo metadata (no package matched ${PWD}/Cargo.toml)." + exit 1 + fi + + if [[ "${TAG_VERSION}" != "${CRATE_VERSION}" ]]; then + echo "::error::Tag ${GITHUB_REF_NAME} publishes version ${TAG_VERSION}, but Cargo.toml is ${CRATE_VERSION}." + echo "::error::The binary would report ${CRATE_VERSION} from --version while the npm packages say ${TAG_VERSION}." + echo "::error::Set the generator's output version to ${TAG_VERSION} and regenerate, or tag v${CRATE_VERSION} instead." + exit 1 + fi + echo "Tag and crate version agree: ${TAG_VERSION}" + + publish: + needs: [check, compile, test, version] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ${{ matrix.runner }} + permissions: + contents: read + id-token: write + strategy: + # Don't cancel sibling matrix jobs on first failure — a transient + # failure on one platform would otherwise leave npm in a partial + # state (some platform packages published, others not, launcher + # never published), with no clean re-run since the already- + # published versions reject re-publish. + fail-fast: false + matrix: + include: + - rust-target: x86_64-unknown-linux-musl + runner: ubuntu-latest + npm-platform-suffix: linux-x64 + - rust-target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm + npm-platform-suffix: linux-arm64 + - rust-target: x86_64-apple-darwin + runner: macos-latest + npm-platform-suffix: darwin-x64 + - rust-target: aarch64-apple-darwin + runner: macos-latest + npm-platform-suffix: darwin-arm64 + - rust-target: x86_64-pc-windows-msvc + runner: windows-latest + npm-platform-suffix: win32-x64 + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + - name: Add Rust target + shell: bash + run: rustup target add ${{ matrix.rust-target }} + + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target + key: cargo-${{ runner.os }}-${{ matrix.rust-target }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}-${{ matrix.rust-target }}- + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "lts/Krypton" + registry-url: "https://registry.npmjs.org" + + - name: Install musl build tools + if: contains(matrix.rust-target, '-linux-musl') + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + # The TLS backend and the keyring backend are selected per target by + # Cargo.toml, so no per-target feature flags are needed here. + # + # musl targets get a C compiler for the C dependencies, but not a + # linker: musl-gcc cannot produce static-pie, so it yields a binary + # that needs /lib/ld-musl-*.so.1 at runtime — which defeats the point + # of a musl build and crashes where that loader is absent. Left alone, + # rustc links the self-contained musl objects statically. + - name: Build release binary + shell: bash + run: | + if [[ "${{ matrix.rust-target }}" == *-linux-musl ]]; then + TARGET_UNDERSCORE=$(echo "${{ matrix.rust-target }}" | tr '-' '_') + export "CC_${TARGET_UNDERSCORE}=musl-gcc" + fi + cargo build --release --target ${{ matrix.rust-target }} + + - name: Package and publish npm platform package + shell: bash + run: | + set -euo pipefail + + VERSION="${GITHUB_REF_NAME#v}" + PLATFORM_PKG="@elevenlabs/cli-${{ matrix.npm-platform-suffix }}" + PKG_DIR="npm-pkg/${PLATFORM_PKG}" + mkdir -p "${PKG_DIR}" + + # Locate the compiled binary + BINARY_NAME="elevenlabs" + if [[ "${{ matrix.rust-target }}" == *"windows"* ]]; then + BINARY_NAME="elevenlabs.exe" + fi + cp "target/${{ matrix.rust-target }}/release/${BINARY_NAME}" "${PKG_DIR}/" + + # Write platform package.json + cat > "${PKG_DIR}/package.json" </dev/null || echo "0.0.0") + if npx -y semver@7.8.1 "${PKG_VERSION}" -r "<${CURRENT_LATEST}" > /dev/null 2>&1; then + echo "Publishing ${PKG_VERSION} with --tag backport (current latest is ${CURRENT_LATEST})" + npm publish --access public --tag backport + else + npm publish --access public + fi + fi + + publish-launcher: + needs: [publish] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "lts/Krypton" + registry-url: "https://registry.npmjs.org" + + - name: Publish launcher package + shell: bash + run: | + set -euo pipefail + + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="npm-pkg/launcher" + mkdir -p "${PKG_DIR}" + + BINARY_NAME="elevenlabs" + + # Build optionalDependencies map + OPTIONAL_DEPS="" + OPTIONAL_DEPS="${OPTIONAL_DEPS}\"@elevenlabs/cli-linux-x64\": \"${VERSION}\"," + OPTIONAL_DEPS="${OPTIONAL_DEPS}\"@elevenlabs/cli-linux-arm64\": \"${VERSION}\"," + OPTIONAL_DEPS="${OPTIONAL_DEPS}\"@elevenlabs/cli-darwin-x64\": \"${VERSION}\"," + OPTIONAL_DEPS="${OPTIONAL_DEPS}\"@elevenlabs/cli-darwin-arm64\": \"${VERSION}\"," + OPTIONAL_DEPS="${OPTIONAL_DEPS}\"@elevenlabs/cli-win32-x64\": \"${VERSION}\"" + + cat > "${PKG_DIR}/package.json" < "${PKG_DIR}/bin/cli.js" <<'LAUNCHER' + #!/usr/bin/env node + "use strict"; + const { execFileSync } = require("child_process"); + const path = require("path"); + const os = require("os"); + + const PLATFORMS = { + "linux-x64": "@elevenlabs/cli-linux-x64", + "linux-arm64": "@elevenlabs/cli-linux-arm64", + "darwin-x64": "@elevenlabs/cli-darwin-x64", + "darwin-arm64": "@elevenlabs/cli-darwin-arm64", + "win32-x64": "@elevenlabs/cli-win32-x64", + }; + + const platformKey = os.platform() + "-" + os.arch(); + const pkg = PLATFORMS[platformKey]; + if (!pkg) { + console.error("Unsupported platform: " + platformKey); + process.exit(1); + } + + const binName = os.platform() === "win32" ? "elevenlabs.exe" : "elevenlabs"; + const binPath = path.join(require.resolve(pkg + "/package.json"), "..", binName); - # elevenlabs-sdk and elevenlabs-types are path dependencies, not members - # of the cargo workspace, so `cargo test` above skips them. Run their - # suites explicitly so the generated serialization tests are covered. - - name: Test generated SDK crate - run: cargo test --manifest-path elevenlabs-sdk/Cargo.toml + try { + execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); + } catch (e) { + if (e && typeof e === "object" && "status" in e) { + process.exit(e.status); + } + throw e; + } + LAUNCHER - - name: Test generated types crate - run: cargo test --manifest-path elevenlabs-types/Cargo.toml + cd "${PKG_DIR}" + # Pre-release detection — require the semver "-" separator so a + # release tag like v1.0.0 for a package whose version string + # happens to contain "alpha"/"beta" as a substring isn't + # mis-tagged on npm. + if [[ "${VERSION}" == *-alpha* ]]; then + npm publish --access public --tag alpha + elif [[ "${VERSION}" == *-beta* ]]; then + npm publish --access public --tag beta + else + PKG_NAME=$(node -p "require('./package.json').name") + PKG_VERSION=$(node -p "require('./package.json').version") + CURRENT_LATEST=$(npm view "${PKG_NAME}" dist-tags.latest 2>/dev/null || echo "0.0.0") + if npx -y semver@7.8.1 "${PKG_VERSION}" -r "<${CURRENT_LATEST}" > /dev/null 2>&1; then + echo "Publishing ${PKG_VERSION} with --tag backport (current latest is ${CURRENT_LATEST})" + npm publish --access public --tag backport + else + npm publish --access public + fi + fi \ No newline at end of file From 285849878c13d3262c49968c9f0d0824c61c5c06 Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Thu, 27 Aug 2026 16:09:47 +0200 Subject: [PATCH 2/4] preserve audit task --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74d1994..b7f594c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,11 @@ name: ci -on: [push] +on: + push: + branches: [next, main] + tags: ["v*"] + pull_request: + types: [opened, synchronize, reopened] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -68,6 +73,46 @@ jobs: - name: Compile run: cargo build + audit: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Set up Rust + shell: bash + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + fi + + # A regeneration that adds a dependency without refreshing Cargo.lock + # leaves it unresolvable, which silently disables `cargo audit`. Fail here + # instead: `--locked` proves the committed lockfile still describes the + # dependency graph. The fix is to run `cargo build` and commit the lock. + - name: Verify Cargo.lock is complete + run: cargo metadata --locked --format-version 1 > /dev/null + + - name: Cache cargo-audit + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/cargo-audit + key: cargo-audit-${{ runner.os }} + + - name: Install cargo-audit + shell: bash + run: | + if ! command -v cargo-audit > /dev/null 2>&1; then + cargo install cargo-audit --locked + fi + + # Fails on advisories only. Warnings (unmaintained, yanked) are left + # non-blocking on purpose: the dependency graph comes from the generator, + # so a warning we cannot promptly fix would block every unrelated PR. + - name: Audit dependencies + run: cargo audit + test: runs-on: ubuntu-latest steps: @@ -96,6 +141,10 @@ jobs: - name: Test run: cargo test + - name: Test elevenlabs-types + run: cargo test --manifest-path elevenlabs-types/Cargo.toml + - name: Test elevenlabs-sdk + run: cargo test --manifest-path elevenlabs-sdk/Cargo.toml # The npm packages take their version from the release tag, while the # binary reports the version in Cargo.toml. Publishing when they disagree @@ -394,4 +443,5 @@ jobs: else npm publish --access public fi - fi \ No newline at end of file + fi + From fcc404e7d5edfcc1ff087bf8cdd614b206392769 Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Thu, 27 Aug 2026 16:31:11 +0200 Subject: [PATCH 3/4] fix: propagate signals, gate publish on audit, explain missing platform pkg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The launcher exited 0 when the CLI was killed by a signal: execFileSync reports status === null in that case and `"status" in e` is still true, so an interrupted run looked like a clean one. Exit 128 + signum instead. A failed require.resolve now names the missing optional dependency and how to reinstall, rather than printing a bare MODULE_NOT_FOUND stack. publish now needs audit, so a release cannot go out while cargo-audit is failing — npm versions are immutable once published. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f594c..70c97ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,7 +190,7 @@ jobs: echo "Tag and crate version agree: ${TAG_VERSION}" publish: - needs: [check, compile, test, version] + needs: [audit, check, compile, test, version] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ${{ matrix.runner }} permissions: @@ -412,15 +412,34 @@ jobs: } const binName = os.platform() === "win32" ? "elevenlabs.exe" : "elevenlabs"; - const binPath = path.join(require.resolve(pkg + "/package.json"), "..", binName); + + let binPath; + try { + binPath = path.join(require.resolve(pkg + "/package.json"), "..", binName); + } catch (e) { + console.error( + "The platform package " + pkg + " is not installed.\n" + + "It ships the elevenlabs binary for " + platformKey + " and is an\n" + + "optional dependency, so an install run with --no-optional or\n" + + "--omit=optional will skip it. Reinstall without those flags:\n" + + " npm install -g @elevenlabs/cli" + ); + process.exit(1); + } try { execFileSync(binPath, process.argv.slice(2), { stdio: "inherit" }); } catch (e) { - if (e && typeof e === "object" && "status" in e) { + // status is null when a signal killed the child, so exiting on it + // would report success for an interrupted run. + if (e && typeof e === "object" && e.signal) { + process.exit(128 + (os.constants.signals[e.signal] || 0)); + } + if (e && typeof e === "object" && typeof e.status === "number") { process.exit(e.status); } - throw e; + console.error(e && e.message ? e.message : String(e)); + process.exit(1); } LAUNCHER From dab01c8b63c885971e3c6b95184d5a247accb52d Mon Sep 17 00:00:00 2001 From: Paul Asjes Date: Thu, 27 Aug 2026 16:40:38 +0200 Subject: [PATCH 4/4] fix: repackage the release archives for npm instead of rebuilding The publish matrix compiled its own binaries on five runners while release.yml (cargo-dist) built and attested the same commit for the same tag. The two disagreed: with default features empty, Cargo.toml selects native-tls on macOS/Windows, whereas cargo-dist builds every target with features = ["rustls"]. npm users therefore got different TLS and cert behaviour from brew and scoop users, and the npm binaries carried no attestation. Download the archives from the release instead, verify each one with `gh attestation verify` before republishing it, and drop the rust toolchain, target, cache, musl-tools and cargo build steps. All five matrix legs now run on ubuntu-latest. A new await-release job waits for cargo-dist to upload the archives, since both workflows fire on the same tag push. The matrix carries os and arch, so the npm suffix is composed from them rather than split back apart with cut, and package.json os/cpu are literal matrix values (review feedback). Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 184 +++++++++++++++++++++++---------------- 1 file changed, 107 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70c97ab..c40277d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,13 +189,65 @@ jobs: fi echo "Tag and crate version agree: ${TAG_VERSION}" + # release.yml (cargo-dist) builds and attests the binaries for this tag, and + # the npm packages repackage those rather than compiling their own. Both + # workflows fire on the same tag push, so wait for the archives to land. + await-release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + permissions: + contents: read + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Wait for the release archives + shell: bash + run: | + set -euo pipefail + + REQUIRED=( + elevenlabs-cli-x86_64-unknown-linux-musl.tar.gz + elevenlabs-cli-aarch64-unknown-linux-musl.tar.gz + elevenlabs-cli-x86_64-apple-darwin.tar.gz + elevenlabs-cli-aarch64-apple-darwin.tar.gz + elevenlabs-cli-x86_64-pc-windows-msvc.zip + ) + + DEADLINE=$(( $(date +%s) + 45 * 60 )) + while :; do + ASSETS=$(gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ + --json assets --jq '.assets[].name' 2>/dev/null || true) + + MISSING=() + for a in "${REQUIRED[@]}"; do + grep -qxF "${a}" <<< "${ASSETS}" || MISSING+=("${a}") + done + + if [[ ${#MISSING[@]} -eq 0 ]]; then + echo "All ${#REQUIRED[@]} archives are present on ${GITHUB_REF_NAME}." + exit 0 + fi + + if [[ $(date +%s) -ge ${DEADLINE} ]]; then + echo "::error::Timed out waiting for the release archives on ${GITHUB_REF_NAME}." + echo "::error::Still missing: ${MISSING[*]}" + echo "::error::Check the 'release' workflow for this tag — npm publishing repackages its output." + exit 1 + fi + + echo "Waiting on ${#MISSING[@]} archive(s); retrying in 30s." + sleep 30 + done + publish: - needs: [audit, check, compile, test, version] - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - runs-on: ${{ matrix.runner }} + needs: [audit, check, compile, test, version, await-release] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest permissions: contents: read id-token: write + env: + GH_TOKEN: ${{ github.token }} strategy: # Don't cancel sibling matrix jobs on first failure — a transient # failure on one platform would otherwise leave npm in a partial @@ -205,107 +257,85 @@ jobs: fail-fast: false matrix: include: - - rust-target: x86_64-unknown-linux-musl - runner: ubuntu-latest - npm-platform-suffix: linux-x64 - - rust-target: aarch64-unknown-linux-musl - runner: ubuntu-24.04-arm - npm-platform-suffix: linux-arm64 - - rust-target: x86_64-apple-darwin - runner: macos-latest - npm-platform-suffix: darwin-x64 - - rust-target: aarch64-apple-darwin - runner: macos-latest - npm-platform-suffix: darwin-arm64 - - rust-target: x86_64-pc-windows-msvc - runner: windows-latest - npm-platform-suffix: win32-x64 + - os: linux + arch: x64 + rust-target: x86_64-unknown-linux-musl + - os: linux + arch: arm64 + rust-target: aarch64-unknown-linux-musl + - os: darwin + arch: x64 + rust-target: x86_64-apple-darwin + - os: darwin + arch: arm64 + rust-target: aarch64-apple-darwin + - os: win32 + arch: x64 + rust-target: x86_64-pc-windows-msvc steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Set up Rust - shell: bash - run: | - if ! command -v cargo > /dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - fi - - - name: Add Rust target - shell: bash - run: rustup target add ${{ matrix.rust-target }} - - - name: Cache cargo registry and build artifacts - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git/db - target - key: cargo-${{ runner.os }}-${{ matrix.rust-target }}-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - cargo-${{ runner.os }}-${{ matrix.rust-target }}- - - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: "lts/Krypton" registry-url: "https://registry.npmjs.org" - - name: Install musl build tools - if: contains(matrix.rust-target, '-linux-musl') - run: | - sudo apt-get update - sudo apt-get install -y musl-tools - - # The TLS backend and the keyring backend are selected per target by - # Cargo.toml, so no per-target feature flags are needed here. - # - # musl targets get a C compiler for the C dependencies, but not a - # linker: musl-gcc cannot produce static-pie, so it yields a binary - # that needs /lib/ld-musl-*.so.1 at runtime — which defeats the point - # of a musl build and crashes where that loader is absent. Left alone, - # rustc links the self-contained musl objects statically. - - name: Build release binary - shell: bash - run: | - if [[ "${{ matrix.rust-target }}" == *-linux-musl ]]; then - TARGET_UNDERSCORE=$(echo "${{ matrix.rust-target }}" | tr '-' '_') - export "CC_${TARGET_UNDERSCORE}=musl-gcc" - fi - cargo build --release --target ${{ matrix.rust-target }} - - name: Package and publish npm platform package shell: bash + env: + PLATFORM_OS: ${{ matrix.os }} + PLATFORM_ARCH: ${{ matrix.arch }} + RUST_TARGET: ${{ matrix.rust-target }} run: | set -euo pipefail VERSION="${GITHUB_REF_NAME#v}" - PLATFORM_PKG="@elevenlabs/cli-${{ matrix.npm-platform-suffix }}" + SUFFIX="${PLATFORM_OS}-${PLATFORM_ARCH}" + PLATFORM_PKG="@elevenlabs/cli-${SUFFIX}" PKG_DIR="npm-pkg/${PLATFORM_PKG}" mkdir -p "${PKG_DIR}" - # Locate the compiled binary - BINARY_NAME="elevenlabs" - if [[ "${{ matrix.rust-target }}" == *"windows"* ]]; then + if [[ "${PLATFORM_OS}" == "win32" ]]; then + ARCHIVE="elevenlabs-cli-${RUST_TARGET}.zip" BINARY_NAME="elevenlabs.exe" + else + ARCHIVE="elevenlabs-cli-${RUST_TARGET}.tar.gz" + BINARY_NAME="elevenlabs" + fi + + gh release download "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \ + --pattern "${ARCHIVE}" --dir dl + + # Fail closed unless the archive is provably the one this repo's + # release workflow built, since we are republishing it under our name. + gh attestation verify "dl/${ARCHIVE}" --repo "${GITHUB_REPOSITORY}" + + # tar.gz wraps its contents in elevenlabs-cli-/ while the zip + # holds them at the root, so locate the binary instead of assuming. + mkdir -p extract + case "${ARCHIVE}" in + *.zip) unzip -q "dl/${ARCHIVE}" -d extract ;; + *.tar.gz) tar -xzf "dl/${ARCHIVE}" -C extract ;; + esac + + BIN=$(find extract -type f -name "${BINARY_NAME}" -print -quit) + if [[ -z "${BIN}" ]]; then + echo "::error::${BINARY_NAME} not found in ${ARCHIVE}." + exit 1 fi - cp "target/${{ matrix.rust-target }}/release/${BINARY_NAME}" "${PKG_DIR}/" + install -m 0755 "${BIN}" "${PKG_DIR}/${BINARY_NAME}" - # Write platform package.json cat > "${PKG_DIR}/package.json" <