name: Release Binaries
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
version:
description: 'Version tag (e.g., v0.1.0)'
required: true
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
# Validation job runs first to catch version mismatches early
validate:
name: Validate Release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.VERSION }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get version from tag
id: get_version
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
else
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
- name: Validate Cargo.toml version matches tag
run: |
TAG_VERSION="${{ steps.get_version.outputs.VERSION }}"
TAG_VERSION_NUM="${TAG_VERSION#v}"
CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Tag version: ${TAG_VERSION_NUM}"
echo "Cargo.toml version: ${CARGO_VERSION}"
if [ "$CARGO_VERSION" != "$TAG_VERSION_NUM" ]; then
echo "::error::Version mismatch! Cargo.toml ($CARGO_VERSION) does not match tag ($TAG_VERSION_NUM)"
echo "::error::Please update Cargo.toml version to $TAG_VERSION_NUM before tagging"
exit 1
fi
echo "Version validation passed"
- name: Validate SDK version matches tag
run: |
TAG_VERSION="${{ steps.get_version.outputs.VERSION }}"
TAG_VERSION_NUM="${TAG_VERSION#v}"
SDK_VERSION=$(grep '"version"' sdk/package.json | head -1 | sed 's/.*"version": "\(.*\)".*/\1/')
echo "Tag version: ${TAG_VERSION_NUM}"
echo "SDK package.json version: ${SDK_VERSION}"
if [ "$SDK_VERSION" != "$TAG_VERSION_NUM" ]; then
echo "::error::Version mismatch! sdk/package.json ($SDK_VERSION) does not match tag ($TAG_VERSION_NUM)"
echo "::error::Please update sdk/package.json version to $TAG_VERSION_NUM before tagging"
exit 1
fi
echo "SDK version validation passed"
- name: Check release does not already exist
run: |
VERSION="${{ steps.get_version.outputs.VERSION }}"
if gh release view "$VERSION" > /dev/null 2>&1; then
echo "::error::Release $VERSION already exists! Delete it first or use a different version."
exit 1
fi
echo "No existing release found for $VERSION"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Create draft release before building (prevents race conditions)
# Note: Full test suite removed - CI workflow validates main branch with PostgreSQL
# before any release can be tagged. This eliminates duplicate ~45min test runs.
create-draft:
name: Create Draft Release
needs: [validate]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check if CHANGELOG exists
id: check_changelog
run: |
if [ -f "CHANGELOG.md" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Extract release notes from CHANGELOG
if: steps.check_changelog.outputs.exists == 'true'
run: |
VERSION="${{ needs.validate.outputs.version }}"
awk "/^## \[${VERSION#v}\]/ {flag=1; next} /^## \[/ {flag=0} flag" CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "No release notes found in CHANGELOG.md for version ${VERSION}"
echo "## What's New" > release-notes.md
echo "" >> release-notes.md
echo "See [CHANGELOG.md](CHANGELOG.md) for details." >> release-notes.md
fi
- name: Create default release notes
if: steps.check_changelog.outputs.exists == 'false'
run: |
VERSION="${{ needs.validate.outputs.version }}"
cat > release-notes.md <<EOF
## Pierre MCP Server ${VERSION}
Multi-protocol fitness data API for LLMs with MCP and A2A support.
### Installation
Download the appropriate binary for your platform below.
**Linux (Recommended):**
- \`pierre-mcp-server-${VERSION}-linux-x86_64-musl.tar.gz\` - Static binary, no dependencies
**macOS:**
- \`pierre-mcp-server-${VERSION}-macos-x86_64.tar.gz\` - Intel Macs
- \`pierre-mcp-server-${VERSION}-macos-aarch64.tar.gz\` - Apple Silicon (M1/M2/M3)
**Windows:**
- \`pierre-mcp-server-${VERSION}-windows-x86_64.zip\`
### Verification
Verify your download with SHA256 checksums:
\`\`\`bash
sha256sum -c SHA256SUMS.txt
\`\`\`
### Documentation
See [README.md](https://github.com/Async-IO/pierre_mcp_server) for configuration and usage.
EOF
- name: Create draft release
run: |
VERSION="${{ needs.validate.outputs.version }}"
gh release create "$VERSION" \
--draft \
--title "Pierre MCP Server $VERSION" \
--notes-file release-notes.md
echo "Draft release $VERSION created"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: Build ${{ matrix.target }}
needs: [validate, create-draft]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux GNU (glibc) - x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
archive: tar.gz
platform: linux
arch: x86_64
variant: gnu
# Linux musl (static) - x86_64 - RECOMMENDED
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
archive: tar.gz
platform: linux
arch: x86_64
variant: musl
# macOS x86_64 (Intel)
- os: macos-latest
target: x86_64-apple-darwin
archive: tar.gz
platform: macos
arch: x86_64
variant: ''
# macOS ARM64 (Apple Silicon)
- os: macos-latest
target: aarch64-apple-darwin
archive: tar.gz
platform: macos
arch: aarch64
variant: ''
# Windows x86_64 (MSVC)
- os: windows-latest
target: x86_64-pc-windows-msvc
archive: zip
platform: windows
arch: x86_64
variant: ''
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Free disk space (Ubuntu only)
if: runner.os == 'Linux'
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc || true
sudo apt-get clean || true
echo "Disk space after cleanup:"
df -h
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.92.0
with:
targets: ${{ matrix.target }}
- name: Install musl tools (Linux musl only)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-cargo-release-
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }} --locked
env:
# Minimal env for build (no secrets needed)
CARGO_INCREMENTAL: 0
- name: Strip binary (Unix only)
if: matrix.os != 'windows-latest'
run: |
strip target/${{ matrix.target }}/release/pierre-mcp-server || true
strip target/${{ matrix.target }}/release/admin-setup || true
- name: Create package directory structure (Unix)
if: matrix.os != 'windows-latest'
run: |
VERSION="${{ needs.validate.outputs.version }}"
PACKAGE_NAME="pierre-mcp-server-${VERSION}-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.variant && format('-{0}', matrix.variant) || '' }}"
mkdir -p "dist/${PACKAGE_NAME}/bin"
# Copy binaries
cp target/${{ matrix.target }}/release/pierre-mcp-server "dist/${PACKAGE_NAME}/bin/"
cp target/${{ matrix.target }}/release/admin-setup "dist/${PACKAGE_NAME}/bin/"
# Copy documentation
cp README.md "dist/${PACKAGE_NAME}/"
cp LICENSE-MIT "dist/${PACKAGE_NAME}/" || true
cp LICENSE-APACHE "dist/${PACKAGE_NAME}/" || true
# Create INSTALL.txt with quick start instructions
cat > "dist/${PACKAGE_NAME}/INSTALL.txt" <<EOF
Pierre MCP Server $VERSION - Installation Instructions
Quick Start:
1. Add the bin/ directory to your PATH:
export PATH="\$PWD/bin:\$PATH"
2. Verify installation:
pierre-mcp-server --version
3. Set up admin user:
admin-setup create-admin-user --email admin@example.com --password YourSecurePassword
4. Run the server:
pierre-mcp-server
For full documentation, see README.md or visit:
https://github.com/Async-IO/pierre_mcp_server
EOF
echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV
- name: Create package directory structure (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$VERSION = "${{ needs.validate.outputs.version }}"
$PACKAGE_NAME = "pierre-mcp-server-${VERSION}-${{ matrix.platform }}-${{ matrix.arch }}"
New-Item -ItemType Directory -Force -Path "dist\${PACKAGE_NAME}\bin"
# Copy binaries
Copy-Item "target\${{ matrix.target }}\release\pierre-mcp-server.exe" "dist\${PACKAGE_NAME}\bin\"
Copy-Item "target\${{ matrix.target }}\release\admin-setup.exe" "dist\${PACKAGE_NAME}\bin\"
# Copy documentation
Copy-Item "README.md" "dist\${PACKAGE_NAME}\"
Copy-Item "LICENSE-MIT" "dist\${PACKAGE_NAME}\" -ErrorAction SilentlyContinue
Copy-Item "LICENSE-APACHE" "dist\${PACKAGE_NAME}\" -ErrorAction SilentlyContinue
# Create INSTALL.txt
@"
Pierre MCP Server $VERSION - Installation Instructions
Quick Start:
1. Add the bin\ directory to your PATH or run from this directory
2. Verify installation:
bin\pierre-mcp-server.exe --version
3. Set up admin user:
bin\admin-setup.exe create-admin-user --email admin@example.com --password YourSecurePassword
4. Run the server:
bin\pierre-mcp-server.exe
For full documentation, see README.md or visit:
https://github.com/Async-IO/pierre_mcp_server
"@ | Out-File -FilePath "dist\${PACKAGE_NAME}\INSTALL.txt" -Encoding utf8
echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $env:GITHUB_ENV
- name: Create tar.gz archive (Unix)
if: matrix.archive == 'tar.gz'
run: |
cd dist
tar -czf "${PACKAGE_NAME}.tar.gz" "${PACKAGE_NAME}"
ls -lh "${PACKAGE_NAME}.tar.gz"
- name: Create zip archive (Windows)
if: matrix.archive == 'zip'
shell: pwsh
run: |
cd dist
Compress-Archive -Path "${env:PACKAGE_NAME}" -DestinationPath "${env:PACKAGE_NAME}.zip"
Get-ChildItem "${env:PACKAGE_NAME}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: |
dist/*.tar.gz
dist/*.zip
retention-days: 1
# Upload assets to draft and publish
publish-release:
name: Publish Release
needs: [validate, create-draft, build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts/
- name: Organize artifacts
run: |
mkdir -p release-assets
find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec cp {} release-assets/ \;
ls -lh release-assets/
- name: Generate SHA256 checksums
run: |
cd release-assets
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Upload assets to draft release
run: |
VERSION="${{ needs.validate.outputs.version }}"
echo "Uploading assets to draft release $VERSION..."
gh release upload "$VERSION" release-assets/* --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release (remove draft status)
run: |
VERSION="${{ needs.validate.outputs.version }}"
echo "Publishing release $VERSION..."
gh release edit "$VERSION" --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release summary
run: |
VERSION="${{ needs.validate.outputs.version }}"
echo "✅ Release ${VERSION} published successfully!"
echo ""
echo "📦 Released binaries:"
ls -lh release-assets/ | grep -v total
echo ""
echo "🔗 Release URL: https://github.com/${{ github.repository }}/releases/tag/${VERSION}"