Skip to content

fix: rename npm packages and fix npm publishing #4

fix: rename npm packages and fix npm publishing

fix: rename npm packages and fix npm publishing #4

Workflow file for this run

name: CD
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
id-token: write # Sigstore OIDC for attestations
attestations: write # Persist attestations
concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
PROJECT_NAME: bito
jobs:
generate-changelog:
name: Generate changelog
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
release_body: ${{ steps.changelog.outputs.release_body }}
version: ${{ steps.changelog.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Generate release changelog
id: changelog
uses: ./.github/actions/generate-release-changelog
publish-binaries:
name: Build ${{ matrix.build.NAME }}
needs: generate-changelog
runs-on: ${{ matrix.build.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
build:
- {
NAME: linux-x64-gnu,
os: ubuntu-22.04,
target: x86_64-unknown-linux-gnu,
}
- {
NAME: linux-x64-musl,
os: ubuntu-22.04,
target: x86_64-unknown-linux-musl,
}
- {
NAME: linux-arm64-gnu,
os: ubuntu-22.04,
target: aarch64-unknown-linux-gnu,
}
- {
NAME: linux-arm64-musl,
os: ubuntu-22.04,
target: aarch64-unknown-linux-musl,
}
- { NAME: darwin-x64, os: macos-15, target: x86_64-apple-darwin }
- { NAME: darwin-arm64, os: macos-15, target: aarch64-apple-darwin }
- {
NAME: windows-x64-msvc,
os: windows-2025,
target: x86_64-pc-windows-msvc,
}
- {
NAME: windows-arm64-msvc,
os: windows-2025,
target: aarch64-pc-windows-msvc,
}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install musl tools
if: contains(matrix.build.target, 'musl') && contains(matrix.build.target, 'x86_64')
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Install arm64 cross-compilation tools
if: contains(matrix.build.target, 'aarch64-unknown-linux')
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
if [[ "${{ matrix.build.target }}" == *"musl"* ]]; then
sudo apt-get install -y musl-tools
fi
- name: Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Add compilation target
run: rustup target add ${{ matrix.build.target }}
- name: Rust cache
uses: ./.github/actions/setup-rust-cache
with:
cache-key-suffix: release-${{ matrix.build.target }}
toolchain: stable
- name: Install cargo-auditable
uses: ./.github/actions/setup-cargo-tools
with:
binstall-tools: cargo-auditable
- name: Build
shell: bash
run: |
if [[ "${{ matrix.build.target }}" == "aarch64-unknown-linux-gnu" ]]; then
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
elif [[ "${{ matrix.build.target }}" == "aarch64-unknown-linux-musl" ]]; then
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc
fi
cargo auditable build --release --locked --target ${{ matrix.build.target }}
- name: Generate man pages
if: runner.os != 'Windows'
shell: bash
run: |
mkdir -p dist/share/man/man1
cargo run --package xtask -- man --out-dir dist/share/man/man1
- name: Generate shell completions
if: runner.os != 'Windows'
shell: bash
run: |
mkdir -p dist/share/completions
cargo run --package xtask -- completions --out-dir dist/share/completions
- name: Install cargo-cyclonedx
if: vars.SBOM_ENABLED == 'true'
uses: ./.github/actions/setup-cargo-tools
with:
binstall-tools: cargo-cyclonedx
- name: Generate SBOM
if: vars.SBOM_ENABLED == 'true'
run: cargo cyclonedx --target ${{ matrix.build.target }} --format json
- name: Prepare release assets
shell: bash
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
TARGET: ${{ matrix.build.target }}
run: |
mkdir -p release/bin release/share
cp LICENSE-* THIRD-PARTY-NOTICES README.md CHANGELOG.md release/ 2>/dev/null || true
# Binary
bin="$PROJECT_NAME"
if [[ "${{ matrix.build.os }}" == "windows-2025" ]]; then
bin="${bin}.exe"
fi
cp "target/${TARGET}/release/${bin}" release/bin/
# Man pages and completions (Unix only)
if [[ -d dist/share/man ]]; then
cp -r dist/share/man release/share/
fi
if [[ -d dist/share/completions ]]; then
cp -r dist/share/completions release/share/
fi
# Package
cd release
if [[ "${{ matrix.build.os }}" == "windows-2025" ]]; then
7z a -tzip "../${PROJECT_NAME}-${VERSION}-${TARGET}.zip" *
else
tar -czvf "../${PROJECT_NAME}-${VERSION}-${TARGET}.tar.gz" *
shasum -a 256 "../${PROJECT_NAME}-${VERSION}-${TARGET}.tar.gz" > "../${PROJECT_NAME}-${VERSION}-${TARGET}.tar.gz.sha256"
fi
- name: Sign release (GPG)
if: (matrix.build.os == 'ubuntu-22.04' || matrix.build.os == 'macos-15') && vars.GPG_SIGNING_ENABLED == 'true'
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
TARGET: ${{ matrix.build.target }}
run: |
echo "${{ secrets.GPG_RELEASE_KEY }}" | base64 --decode > private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --import private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --detach-sign "${PROJECT_NAME}-${VERSION}-${TARGET}.tar.gz"
rm -f private.key
- name: Upload release
if: ${{ !contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}-${{ matrix.build.target }}*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Release v${{ needs.generate-changelog.outputs.version }}"
body: "${{ needs.generate-changelog.outputs.release_body }}"
- name: Upload pre-release
if: ${{ contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}-${{ matrix.build.target }}*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Pre-release v${{ needs.generate-changelog.outputs.version }}"
prerelease: true
- name: Generate artifact attestation
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}-${{ matrix.build.target }}*
publish-crates-io:
name: Publish to crates.io
if: ${{ !contains(github.ref, '-') && vars.CRATES_IO_ENABLED == 'true' }}
needs: [generate-changelog, publish-binaries]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Publish
run: cargo publish --locked --token ${{ secrets.CARGO_TOKEN }}
publish-homebrew:
name: Publish Homebrew formula
if: ${{ !contains(github.ref, '-') && vars.HOMEBREW_ENABLED == 'true' }}
needs: [generate-changelog, publish-binaries]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
steps:
- name: Download SHA256 checksums
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p checksums
gh release download "v${VERSION}" \
--pattern "*.sha256" \
--dir checksums \
--repo "${{ github.repository }}"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: .github/formula.rb.tmpl
sparse-checkout-cone-mode: false
- name: Generate formula
run: |
get_sha() {
local target="$1"
awk '{print $1}' "checksums/${PROJECT_NAME}-${VERSION}-${target}.tar.gz.sha256"
}
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
sed \
-e "s|__HOMEPAGE__|https://github.com/${{ github.repository }}|" \
-e "s|__VERSION__|${VERSION}|" \
-e "s|__RELEASE_URL__|${RELEASE_URL}|g" \
-e "s|__SHA_DARWIN_ARM64__|$(get_sha aarch64-apple-darwin)|" \
-e "s|__SHA_DARWIN_X64__|$(get_sha x86_64-apple-darwin)|" \
-e "s|__SHA_LINUX_ARM64__|$(get_sha aarch64-unknown-linux-gnu)|" \
-e "s|__SHA_LINUX_X64__|$(get_sha x86_64-unknown-linux-gnu)|" \
.github/formula.rb.tmpl > formula.rb
echo "Generated formula:"
cat formula.rb
- name: Push to tap
env:
GH_TOKEN: ${{ secrets.HOMEBREW_COMMITTER_TOKEN }}
run: |
TAP_REPO="${{ vars.HOMEBREW_TAP }}"
gh api "repos/${TAP_REPO}/contents/Formula/${PROJECT_NAME}.rb" \
--jq '.sha' > /tmp/old_sha 2>/dev/null || true
CONTENT=$(base64 -w0 formula.rb)
OLD_SHA=$(cat /tmp/old_sha 2>/dev/null || echo "")
JSON=$(jq -n \
--arg message "${PROJECT_NAME} ${VERSION}" \
--arg content "$CONTENT" \
--arg sha "$OLD_SHA" \
'if $sha != "" then {message: $message, content: $content, sha: $sha} else {message: $message, content: $content} end')
gh api "repos/${TAP_REPO}/contents/Formula/${PROJECT_NAME}.rb" \
--method PUT \
--input - <<< "$JSON"
echo "Updated Formula/${PROJECT_NAME}.rb in ${TAP_REPO}"
publish-deb:
name: Publish Debian package
if: vars.DEB_ENABLED == 'true'
needs: [generate-changelog, publish-binaries]
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu
- name: Install cargo-deb
uses: ./.github/actions/setup-cargo-tools
with:
binstall-tools: cargo-deb
- name: Build Debian package
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
run: |
cargo build --release --locked
cargo deb --no-build -o "${PROJECT_NAME}-${VERSION}.deb"
- name: Sign package
if: vars.GPG_SIGNING_ENABLED == 'true'
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
run: |
echo "${{ secrets.GPG_RELEASE_KEY }}" | base64 --decode > private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --import private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --detach-sign "${PROJECT_NAME}-${VERSION}.deb"
rm -f private.key
- name: Upload release
if: ${{ !contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}.deb*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Release v${{ needs.generate-changelog.outputs.version }}"
body: "${{ needs.generate-changelog.outputs.release_body }}"
- name: Upload pre-release
if: ${{ contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}.deb*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Pre-release v${{ needs.generate-changelog.outputs.version }}"
prerelease: true
publish-rpm:
name: Publish RPM package
if: vars.RPM_ENABLED == 'true'
needs: [generate-changelog, publish-binaries]
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu
- name: Install cargo-generate-rpm
uses: ./.github/actions/setup-cargo-tools
with:
binstall-tools: cargo-generate-rpm
- name: Build RPM package
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
run: |
cargo build --release --locked
cargo generate-rpm -o "${PROJECT_NAME}-${VERSION}.x86_64.rpm"
- name: Sign package
if: vars.GPG_SIGNING_ENABLED == 'true'
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
run: |
echo "${{ secrets.GPG_RELEASE_KEY }}" | base64 --decode > private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --import private.key
echo "${{ secrets.GPG_PASSPHRASE }}" | gpg --pinentry-mode=loopback --passphrase-fd 0 --detach-sign "${PROJECT_NAME}-${VERSION}.x86_64.rpm"
rm -f private.key
- name: Upload release
if: ${{ !contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}.x86_64.rpm*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Release v${{ needs.generate-changelog.outputs.version }}"
body: "${{ needs.generate-changelog.outputs.release_body }}"
- name: Upload pre-release
if: ${{ contains(github.ref, '-') }}
uses: svenstaro/upload-release-action@29e53e917877a24fad85510ded594ab3c9ca12de # 2.11.5
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.PROJECT_NAME }}-${{ needs.generate-changelog.outputs.version }}.x86_64.rpm*
file_glob: true
overwrite: true
tag: ${{ github.ref }}
release_name: "Pre-release v${{ needs.generate-changelog.outputs.version }}"
prerelease: true
publish-npm:
name: Publish to npm
if: ${{ !contains(github.ref, '-') && vars.NPM_ENABLED == 'true' }}
needs: [generate-changelog, publish-binaries]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write # OIDC for npm provenance (after initial publish)
env:
VERSION: ${{ needs.generate-changelog.outputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Download release artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p artifacts
cd artifacts
# Download all release tarballs
gh release download "v${VERSION}" \
--pattern "*.tar.gz" \
--pattern "*.zip" \
--repo "${{ github.repository }}"
- name: Prepare platform packages
run: |
# Map Rust targets to npm platform names
declare -A TARGET_MAP=(
["x86_64-unknown-linux-gnu"]="linux-x64"
["aarch64-unknown-linux-gnu"]="linux-arm64"
["x86_64-apple-darwin"]="darwin-x64"
["aarch64-apple-darwin"]="darwin-arm64"
["x86_64-pc-windows-msvc"]="win32-x64"
["aarch64-pc-windows-msvc"]="win32-arm64"
)
for target in "${!TARGET_MAP[@]}"; do
platform="${TARGET_MAP[$target]}"
pkg_dir="npm/platforms/${PROJECT_NAME}-${platform}"
# Skip if platform package doesn't exist
[[ -d "$pkg_dir" ]] || continue
echo "Preparing ${platform}..."
# Create bin directory
mkdir -p "${pkg_dir}/bin"
# Extract binary from release tarball
tarball="artifacts/${PROJECT_NAME}-${VERSION}-${target}.tar.gz"
zipfile="artifacts/${PROJECT_NAME}-${VERSION}-${target}.zip"
if [[ -f "$tarball" ]]; then
tar -xzf "$tarball" -C "${pkg_dir}/bin" --strip-components=1 "bin/${PROJECT_NAME}"
chmod +x "${pkg_dir}/bin/${PROJECT_NAME}"
elif [[ -f "$zipfile" ]]; then
unzip -j "$zipfile" "bin/${PROJECT_NAME}.exe" -d "${pkg_dir}/bin"
else
echo "Warning: No artifact found for ${target}"
continue
fi
# Update version in package.json
jq --arg v "$VERSION" '.version = $v' "${pkg_dir}/package.json" > tmp.json
mv tmp.json "${pkg_dir}/package.json"
echo "✓ Prepared ${platform}"
done
- name: Update main package version
run: |
main_pkg="npm/${PROJECT_NAME}"
# Update main package version
jq --arg v "$VERSION" '.version = $v' "${main_pkg}/package.json" > tmp.json
mv tmp.json "${main_pkg}/package.json"
# Update optionalDependencies versions
jq --arg v "$VERSION" '
.optionalDependencies |= with_entries(.value = $v)
' "${main_pkg}/package.json" > tmp.json
mv tmp.json "${main_pkg}/package.json"
- name: Add READMEs to npm packages
run: |
# Main package gets the repo README
cp README.md "npm/${PROJECT_NAME}/README.md"
# Platform packages get a pointer to the main package
for pkg_dir in npm/platforms/*/; do
[[ -d "$pkg_dir" ]] || continue
platform=$(basename "$pkg_dir" | sed "s/${PROJECT_NAME}-//")
printf '%s\n' \
"# @claylo/${PROJECT_NAME}-${platform}" \
"" \
"This is the **${platform}** binary for [\`@claylo/${PROJECT_NAME}\`](https://www.npmjs.com/package/@claylo/${PROJECT_NAME})." \
"" \
"Install the main package instead:" \
"" \
'```sh' \
"npm install @claylo/${PROJECT_NAME}" \
'```' \
"" \
"The correct platform binary will be selected automatically." \
> "${pkg_dir}README.md"
done
- name: Publish platform packages
run: |
failures=0
for pkg_dir in npm/platforms/*/; do
[[ -d "$pkg_dir" ]] || continue
# Skip if no binary was added
if [[ ! -f "${pkg_dir}bin/${PROJECT_NAME}" && ! -f "${pkg_dir}bin/${PROJECT_NAME}.exe" ]]; then
echo "Skipping $(basename "$pkg_dir") - no binary"
continue
fi
echo "Publishing $(basename "$pkg_dir")..."
cd "$pkg_dir"
# Try with provenance (OIDC) first; fall back to token-only
# for initial publish where OIDC isn't available yet
if ! npm publish --provenance --access public 2>/dev/null; then
echo "Provenance publish failed, retrying without provenance..."
if ! npm publish --access public; then
echo "::error::Failed to publish $(basename "$pkg_dir")"
failures=$((failures + 1))
fi
fi
cd - > /dev/null
done
if [[ $failures -gt 0 ]]; then
echo "::error::${failures} platform package(s) failed to publish"
exit 1
fi
- name: Publish main package
run: |
cd "npm/${PROJECT_NAME}"
# Try with provenance (OIDC) first; fall back to token-only
if ! npm publish --provenance --access public 2>/dev/null; then
echo "Provenance publish failed, retrying without provenance..."
npm publish --access public
fi