Skip to content

Add npm authentication checks to release workflow #391

Add npm authentication checks to release workflow

Add npm authentication checks to release workflow #391

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
dry_run:
description: "Perform a dry run (no actual release)"
type: boolean
default: false
permissions:
contents: write
issues: write
pull-requests: write
packages: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
# ==========================================================================
# Release Please - Determine version
# ==========================================================================
release-please:
name: Release Please
runs-on: ubuntu-latest
outputs:
new_release_created: ${{ steps.release.outputs.release_created }}
release_version: ${{ steps.release.outputs.version }}
release_git_tag: ${{ steps.release.outputs.tag_name }}
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Release Please
id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
config-file: release-please-config.json
target-branch: main
fork: false
# ==========================================================================
# Build Release Binaries - Cross-platform
# ==========================================================================
build-binaries:
name: Build (${{ matrix.target }})
needs: release-please
if: needs.release-please.outputs.new_release_created == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
archive: tar.gz
# macOS
- target: x86_64-apple-darwin
os: macos-latest
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
# Windows
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
- target: aarch64-pc-windows-msvc
os: windows-latest
archive: zip
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release-please.outputs.release_git_tag }}
- name: Install and configure gcc-12 (Linux)
if: runner.os == 'Linux'
run: |
for i in {1..5}; do
sudo apt-get update && \
sudo apt-get install -y gcc-12 g++-12 && break || sleep 5
if [ $i -eq 5 ]; then exit 1; fi
done
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60
gcc --version
g++ --version
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install cross-compilation tools (Linux)
if: runner.os == 'Linux'
run: |
if [[ "${{ matrix.target }}" == *"musl"* ]]; then
for i in {1..5}; do
sudo apt-get update && \
sudo apt-get install -y musl-tools && break || sleep 5
if [ $i -eq 5 ]; then exit 1; fi
done
fi
if [[ "${{ matrix.target }}" == "aarch64-"* ]]; then
for i in {1..5}; do
sudo apt-get update && \
sudo apt-get install -y gcc-aarch64-linux-gnu && break || sleep 5
if [ $i -eq 5 ]; then exit 1; fi
done
fi
- name: Install cross
if: runner.os == 'Linux'
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build binary (Linux native GNU)
if: runner.os == 'Linux' && matrix.target == 'x86_64-unknown-linux-gnu'
run: cargo build --release --target x86_64-unknown-linux-gnu
- name: Build binary (Linux with cross)
if: runner.os == 'Linux' && matrix.target != 'x86_64-unknown-linux-gnu'
run: cross build --release --target ${{ matrix.target }}
- name: Build binary (macOS/Windows)
if: runner.os != 'Linux'
run: cargo build --release --target ${{ matrix.target }}
- name: Prepare artifact (Unix)
if: runner.os != 'Windows'
run: |
cd target/${{ matrix.target }}/release
BINARY_NAME="agentsync"
ARCHIVE_NAME="agentsync-${{ needs.release-please.outputs.release_version }}-${{ matrix.target }}"
# Create archive directory
mkdir -p "$ARCHIVE_NAME"
cp "$BINARY_NAME" "$ARCHIVE_NAME/"
cp ../../../README.md "$ARCHIVE_NAME/" 2>/dev/null || true
cp ../../../LICENSE "$ARCHIVE_NAME/" 2>/dev/null || true
# Create tarball
tar -czvf "$ARCHIVE_NAME.tar.gz" "$ARCHIVE_NAME"
# Create checksum
shasum -a 256 "$ARCHIVE_NAME.tar.gz" > "$ARCHIVE_NAME.tar.gz.sha256"
# Move to workspace root for upload
mv "$ARCHIVE_NAME.tar.gz" ../../../
mv "$ARCHIVE_NAME.tar.gz.sha256" ../../../
- name: Prepare artifact (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cd target/${{ matrix.target }}/release
$BINARY_NAME = "agentsync.exe"
$ARCHIVE_NAME = "agentsync-${{ needs.release-please.outputs.release_version }}-${{ matrix.target }}"
# Create archive directory
New-Item -ItemType Directory -Force -Path $ARCHIVE_NAME
Copy-Item $BINARY_NAME -Destination "$ARCHIVE_NAME/"
Copy-Item ../../../README.md -Destination "$ARCHIVE_NAME/" -ErrorAction SilentlyContinue
Copy-Item ../../../LICENSE -Destination "$ARCHIVE_NAME/" -ErrorAction SilentlyContinue
# Create zip
Compress-Archive -Path $ARCHIVE_NAME -DestinationPath "$ARCHIVE_NAME.zip"
# Create checksum
(Get-FileHash "$ARCHIVE_NAME.zip" -Algorithm SHA256).Hash.ToLower() + " $ARCHIVE_NAME.zip" | Out-File -FilePath "$ARCHIVE_NAME.zip.sha256" -Encoding ascii
# Move to workspace root
Move-Item "$ARCHIVE_NAME.zip" ../../../
Move-Item "$ARCHIVE_NAME.zip.sha256" ../../..
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: agentsync-${{ matrix.target }}
path: |
agentsync-*.${{ matrix.archive }}
agentsync-*.${{ matrix.archive }}.sha256
if-no-files-found: error
# ==========================================================================
# Upload Release Assets
# ==========================================================================
upload-assets:
name: Upload Release Assets
needs: [release-please, build-binaries]
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: agentsync-*
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Upload to GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release-please.outputs.release_git_tag }}
files: artifacts/*
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
# ==========================================================================
# Publish NPM Platform-Specific Packages
# ==========================================================================
publish-npm-binaries:
name: Publish NPM (${{ matrix.config.name }})
needs: [release-please, build-binaries]
if: needs.release-please.outputs.new_release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
config:
- name: linux-x64
target: x86_64-unknown-linux-gnu
os: linux
arch: x64
- name: linux-arm64
target: aarch64-unknown-linux-gnu
os: linux
arch: arm64
- name: darwin-x64
target: x86_64-apple-darwin
os: darwin
arch: x64
- name: darwin-arm64
target: aarch64-apple-darwin
os: darwin
arch: arm64
- name: windows-x64
target: x86_64-pc-windows-msvc
os: win32
arch: x64
- name: windows-arm64
target: aarch64-pc-windows-msvc
os: win32
arch: arm64
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release-please.outputs.release_git_tag }}
- name: Install and configure gcc-12 (Linux)
if: runner.os == 'Linux'
run: |
for i in {1..5}; do
sudo apt-get update && \
sudo apt-get install -y gcc-12 g++-12 && break || sleep 5
if [ $i -eq 5 ]; then exit 1; fi
done
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60
gcc --version
g++ --version
- name: Download artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: agentsync-${{ matrix.config.target }}
path: artifacts
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.15"
registry-url: "https://registry.npmjs.org"
- name: Setup pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
with:
version: 10
- name: Verify npm authentication
run: |
echo "Checking npm authentication..."
npm whoami
npm config get registry
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish platform package to NPM
shell: bash
run: |
cd npm
# Set version from release-please
export node_version="${{ needs.release-please.outputs.release_version }}"
export node_os="${{ matrix.config.os }}"
export node_arch="${{ matrix.config.arch }}"
export node_pkg="agentsync-${{ matrix.config.name }}"
if [[ "${{ matrix.config.os }}" == "win32" ]]; then
export node_bin="agentsync.exe"
else
export node_bin="agentsync"
fi
# Create package directory
mkdir -p "${node_pkg}/bin"
# Generate package.json from template
envsubst < package.json.tmpl > "${node_pkg}/package.json"
# Extract binary from artifact
cd ../artifacts
# Find and validate the archive
if [[ "${{ matrix.config.os }}" == "win32" ]]; then
# Windows - look for .zip
matches=(agentsync-*.zip)
BINARY_NAME="agentsync.exe"
EXTRACT_CMD="unzip -o"
else
# Unix-like - look for .tar.gz
matches=(agentsync-*.tar.gz)
BINARY_NAME="agentsync"
EXTRACT_CMD="tar -xzvf"
fi
# Validate exactly one archive exists
if [ ! -e "${matches[0]}" ]; then
echo "Error: No matching archive found"
echo "Expected pattern: ${matches[0]}"
echo "Available files:"
ls -la
exit 1
fi
if [ ${#matches[@]} -gt 1 ]; then
echo "Error: Multiple matching archives found (ambiguous):"
printf '%s\n' "${matches[@]}"
exit 1
fi
ARCHIVE="${matches[0]}"
echo "Found archive: $ARCHIVE"
# Extract and verify success
if ! $EXTRACT_CMD "$ARCHIVE"; then
echo "Error: Failed to extract $ARCHIVE"
exit 1
fi
# Find the extracted binary
BINARY_PATH=$(find . -name "$BINARY_NAME" -type f | head -1)
if [ -z "$BINARY_PATH" ] || [ ! -f "$BINARY_PATH" ]; then
echo "Error: Could not find binary $BINARY_NAME after extraction"
echo "Archive used: $ARCHIVE"
echo "Working directory contents:"
ls -laR
exit 1
fi
echo "Found binary at: $BINARY_PATH"
# Copy binary to package
cp "$BINARY_PATH" "../npm/${node_pkg}/bin/"
# Ensure binary is executable before publishing
chmod +x "../npm/${node_pkg}/bin/$BINARY_NAME"
# Verify tarball will contain executable bit. Create a temporary package tgz and inspect.
# This acts as a safety check to avoid publishing packages without executable permissions.
pushd ../npm/${node_pkg} > /dev/null
# Create temp packed tarball
npm pack --pack-destination /tmp > /dev/null
PKG_TGZ=$(ls -1 /tmp/*.tgz | tail -n1)
echo "Created temporary package: $PKG_TGZ"
echo "Listing package contents to verify executable permissions for bin/$BINARY_NAME"
# Show the file entry for visibility (non-fatal). Prefer rg, fallback to grep.
if command -v rg >/dev/null 2>&1; then
tar -tzvf "$PKG_TGZ" | rg -F "bin/$BINARY_NAME" || true
else
tar -tzvf "$PKG_TGZ" | grep -F "bin/$BINARY_NAME" || true
fi
# Fail if the binary inside the tarball is not executable for owner
# Use a regex matching standard tar permission strings (10 chars) and require
# only the owner execute bit. Example line: -rwxr-xr-x 0/0 123 2026-01-22 00:00 bin/agentsync
# The pattern below matches: ^-.{2}x.{6}\s+.*bin/<name>$
# Use double quotes and escape the trailing $ so the shell redirect works correctly.
# Prefer rg if available, otherwise fall back to grep -E
PATTERN="^-..x.{6}[[:space:]]+.*bin/${BINARY_NAME}\$"
if command -v rg >/dev/null 2>&1; then
SEARCHER=(rg)
else
SEARCHER=(grep -E)
fi
if ! tar -tzvf "$PKG_TGZ" | "${SEARCHER[@]}" "$PATTERN" >/dev/null; then
echo "Error: packaged binary does not have executable bit set. Aborting publish."
tar -tzvf "$PKG_TGZ"
exit 1
fi
popd > /dev/null
# Publish the package
cd "../npm/${node_pkg}"
echo "Publishing ${node_pkg}@${node_version}..."
cat package.json
pnpm publish --provenance --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ==========================================================================
# Publish NPM Base Package
# ==========================================================================
publish-npm-base:
name: Publish NPM Base Package
needs: [release-please, publish-npm-binaries]
if: needs.publish-npm-binaries.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release-please.outputs.release_git_tag }}
- name: Install and configure gcc-12 (Linux)
if: runner.os == 'Linux'
run: |
for i in {1..5}; do
sudo apt-get update && \
sudo apt-get install -y gcc-12 g++-12 && break || sleep 5
if [ $i -eq 5 ]; then exit 1; fi
done
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60
gcc --version
g++ --version
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24.15"
registry-url: "https://registry.npmjs.org"
- name: Setup pnpm
uses: pnpm/action-setup@91ab88e2619ed1f46221f0ba42d1492c02baf788 # v6.0.6
with:
version: 10
- name: Verify npm authentication
run: |
echo "Checking npm authentication..."
npm whoami
npm config get registry
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Update package.json version
run: |
cd npm/agentsync
# Update version in package.json
VERSION="${{ needs.release-please.outputs.release_version }}"
# Update main package version and all optionalDependencies versions
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '${VERSION}';
for (const dep in pkg.optionalDependencies) {
pkg.optionalDependencies[dep] = '${VERSION}';
}
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "Updated package.json:"
cat package.json
- name: Install dependencies and build
run: |
cd npm/agentsync
pnpm install --no-frozen-lockfile
pnpm run build
- name: Publish base package to NPM
run: |
cd npm/agentsync
pnpm publish --provenance --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ==========================================================================
# Publish to crates.io
# ==========================================================================
publish-crates:
name: Publish to crates.io
needs: [release-please, build-binaries]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release-please.outputs.release_git_tag }}
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: stable
- name: Publish to crates.io
run: cargo publish --locked
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# ==========================================================================
# Publish Docker Image - Docker Hub & GitHub Container Registry
# ==========================================================================
publish-docker:
name: Publish Docker Image
needs: [release-please, build-binaries]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release-please.outputs.release_git_tag }}
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/agentsync
ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},value=${{ needs.release-please.outputs.release_version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.release_version }}
type=semver,pattern={{major}},value=${{ needs.release-please.outputs.release_version }}
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ==========================================================================
# Release Summary
# ==========================================================================
release-summary:
name: Release Summary
needs: [release-please, build-binaries, upload-assets, publish-crates, publish-docker, publish-npm-base]
runs-on: ubuntu-latest
if: always() && needs.release-please.outputs.new_release_created == 'true'
steps:
- name: Generate Summary
run: |
{
echo "# 🎉 AgentSync Release Summary"
echo ""
echo "## Version: ${{ needs.release-please.outputs.release_version }}"
echo ""
echo "### Build Status"
echo ""
echo "| Component | Status |"
echo "|-----------|--------|"
echo "| Release Please | ✅ ${{ needs.release-please.result }} |"
echo "| Build Binaries | ${{ needs.build-binaries.result == 'success' && '✅' || '❌' }} ${{ needs.build-binaries.result }} |"
echo "| Upload Assets | ${{ needs.upload-assets.result == 'success' && '✅' || '❌' }} ${{ needs.upload-assets.result }} |"
echo "| Publish to crates.io | ${{ needs.publish-crates.result == 'success' && '✅' || '❌' }} ${{ needs.publish-crates.result }} |"
echo "| Publish to NPM | ${{ needs.publish-npm-base.result == 'success' && '✅' || '❌' }} ${{ needs.publish-npm-base.result }} |"
echo "| Docker Image | ${{ needs.publish-docker.result == 'success' && '✅' || '❌' }} ${{ needs.publish-docker.result }} |"
echo ""
echo "### Installation"
echo ""
echo '```bash'
echo "# NPM / npx (easiest!)"
echo "npx agentsync@${{ needs.release-please.outputs.release_version }} --help"
echo ""
echo "# Or install globally"
echo "npm install -g agentsync@${{ needs.release-please.outputs.release_version }}"
echo ""
echo "# Cargo"
echo "cargo install agentsync"
echo ""
echo "# macOS (Apple Silicon)"
echo "curl -LO https://github.com/dallay/agentsync/releases/download/${{ needs.release-please.outputs.release_git_tag }}/agentsync-${{ needs.release-please.outputs.release_version }}-aarch64-apple-darwin.tar.gz"
echo ""
echo "# Linux (x86_64)"
echo "curl -LO https://github.com/dallay/agentsync/releases/download/${{ needs.release-please.outputs.release_git_tag }}/agentsync-${{ needs.release-please.outputs.release_version }}-x86_64-unknown-linux-gnu.tar.gz"
echo ""
echo "# Docker Hub"
echo "docker pull yacosta738/agentsync:${{ needs.release-please.outputs.release_version }}"
echo ""
echo "# GitHub Container Registry"
echo "docker pull ghcr.io/${{ github.repository }}:${{ needs.release-please.outputs.release_version }}"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"