Skip to content

Merge pull request #108 from foutoucour/patch-1 #469

Merge pull request #108 from foutoucour/patch-1

Merge pull request #108 from foutoucour/patch-1 #469

Workflow file for this run

name: CI/CD Pipeline
env:
PYTHON_VERSION: '3.12' # Standardize on 3.12 for Ubuntu 24.04 LTS compatibility
on:
push:
branches: [ main ]
tags: [ "v*" ]
paths-ignore:
- 'docs/**'
- '.gitignore'
- 'LICENSE'
schedule:
# Run daily at 04:00 UTC (6 AM CEST)
- cron: '0 4 * * *'
workflow_dispatch:
inputs:
test_build:
description: 'Test build (uploads as workflow artifacts instead of release assets)'
required: false
default: true
type: boolean
# Restrict default GITHUB_TOKEN permissions for all jobs (principle of least privilege)
permissions:
contents: read
jobs:
# Always run tests first with matrix strategy
test:
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
arch: x86_64
platform: linux
- os: ubuntu-24.04-arm
arch: arm64
platform: linux
- os: macos-15-intel
arch: x86_64
platform: darwin
- os: macos-latest
arch: arm64
platform: darwin
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup build environment (macOS-15 Intel)
if: matrix.os == 'macos-15-intel'
run: |
# Install Xcode Command Line Tools
sudo xcode-select --install 2>/dev/null || true
# Wait for installation to complete
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Cache uv environments
uses: actions/cache@v3
with:
path: |
~/.cache/uv
~/.local/share/uv
key: ${{ runner.os }}-uv-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-uv-
- name: Install dependencies
run: uv sync --extra dev
- name: Test with pytest
run: uv run pytest tests/unit tests/test_console.py
- name: Run smoke tests
env:
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: uv run pytest tests/integration/test_runtime_smoke.py -v
# Build binaries
build:
name: Build APM Binary
needs: [test]
strategy:
matrix:
include:
- os: ubuntu-24.04
platform: linux
arch: x86_64
binary_name: apm-linux-x86_64
- os: ubuntu-24.04-arm
platform: linux
arch: arm64
binary_name: apm-linux-arm64
- os: macos-15-intel
platform: darwin
arch: x86_64
binary_name: apm-darwin-x86_64
- os: macos-latest
platform: darwin
arch: arm64
binary_name: apm-darwin-arm64
runs-on: ${{ matrix.os }}
permissions:
contents: read # Checkout code; upload-artifact uses separate Actions API
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install UPX (Linux)
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y upx-ucl
- name: Install UPX and XCode (macOS)
if: matrix.platform == 'darwin'
run: |
# Install Xcode Command Line Tools
sudo xcode-select --install 2>/dev/null || true
# Wait for installation to complete
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
brew install upx
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install Python dependencies
run: |
uv sync --extra dev --extra build
- name: Build binary
run: |
chmod +x scripts/build-binary.sh
uv run ./scripts/build-binary.sh
- name: Upload binary as workflow artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: |
./dist/${{ matrix.binary_name }}
./dist/${{ matrix.binary_name }}.sha256
./scripts/test-release-validation.sh
./scripts/github-token-helper.sh
include-hidden-files: true # Required to include .apm directories
retention-days: 30
if-no-files-found: error
# Integration tests with full source code access
# Skip on push-to-main: already validated by ci-integration.yml during the PR.
# Run on tags (release gate), schedule (regression), and dispatch (manual).
integration-tests:
name: Integration Tests
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs: [test, build]
strategy:
matrix:
include:
- os: ubuntu-24.04
arch: x86_64
platform: linux
binary_name: apm-linux-x86_64
- os: ubuntu-24.04-arm
arch: arm64
platform: linux
binary_name: apm-linux-arm64
- os: macos-15-intel
arch: x86_64
platform: darwin
binary_name: apm-darwin-x86_64
- os: macos-latest
arch: arm64
platform: darwin
binary_name: apm-darwin-arm64
runs-on: ${{ matrix.os }}
permissions:
contents: read
models: read # Required for GitHub Models API access
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download APM binary from build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: ./
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup build environment (macOS-15-intel)
if: matrix.os == 'macos-15-intel'
run: |
# Install Xcode Command Line Tools
sudo xcode-select --install 2>/dev/null || true
# Wait for installation to complete
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install test dependencies
run: uv sync --extra dev
- name: Run integration tests
env:
APM_E2E_TESTS: "1"
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }} # Models access
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }} # Primary: APM module access
ADO_APM_PAT: ${{ secrets.ADO_APM_PAT }} # Azure DevOps module access
run: |
chmod +x scripts/test-integration.sh
uv run ./scripts/test-integration.sh
timeout-minutes: 20
# Release validation tests - Final pre-release validation of shipped binary
release-validation:
name: Release Validation
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs: [test, build, integration-tests]
strategy:
matrix:
include:
- os: ubuntu-24.04
arch: x86_64
platform: linux
binary_name: apm-linux-x86_64
- os: ubuntu-24.04-arm
arch: arm64
platform: linux
binary_name: apm-linux-arm64
- os: macos-15-intel
arch: x86_64
platform: darwin
binary_name: apm-darwin-x86_64
- os: macos-latest
arch: arm64
platform: darwin
binary_name: apm-darwin-arm64
runs-on: ${{ matrix.os }}
permissions:
contents: read
models: read # Required for GitHub Models API access
steps:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup build environment (macOS-15-intel)
if: matrix.os == 'macos-15-intel'
run: |
# Install Xcode Command Line Tools
sudo xcode-select --install 2>/dev/null || true
# Wait for installation to complete
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
- name: Download APM binary from build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ matrix.binary_name }}
path: /tmp/apm-isolated-test/
- name: Make binary executable and verify isolation
run: |
cd /tmp/apm-isolated-test
# Debug: List the downloaded structure
echo "Downloaded structure:"
find . -name "apm" -type f
ls -la ./dist/
# Make the binary executable
chmod +x ./dist/${{ matrix.binary_name }}/apm
- name: Create APM symlink for testing
run: |
cd /tmp/apm-isolated-test
ln -s "$(pwd)/dist/${{ matrix.binary_name }}/apm" "$(pwd)/apm"
echo "/tmp/apm-isolated-test" >> $GITHUB_PATH
- name: Run release validation tests
env:
APM_E2E_TESTS: "1" # Avoids interactive prompts for MCP env values with apm install
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }} # Primary: APM module access
ADO_APM_PAT: ${{ secrets.ADO_APM_PAT }} # Azure DevOps module access
run: |
cd /tmp/apm-isolated-test
chmod +x scripts/test-release-validation.sh
./scripts/test-release-validation.sh
timeout-minutes: 20
create-release:
name: Create GitHub Release
needs: [test, build, integration-tests, release-validation]
if: github.ref_type == 'tag' # All tags create GitHub releases
runs-on: ubuntu-latest
permissions:
contents: write # Required for release creation
outputs:
is_prerelease: ${{ steps.release_type.outputs.is_prerelease }}
is_private_repo: ${{ github.event.repository.private }}
steps:
- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
path: ./dist
- name: Prepare release binaries
run: |
# Debug: Show the actual downloaded structure
echo "Directory listing:"
ls -la ./dist
echo ""
# Create tar.gz archives from directory structure for release and Homebrew
cd dist
for binary in apm-linux-x86_64 apm-linux-arm64 apm-darwin-x86_64 apm-darwin-arm64; do
# With artifacts containing both scripts and dist/, the binary is in artifact/dist/binary/
artifact_dir="${binary}"
binary_dir="${artifact_dir}/dist/${binary}"
if [ -d "$binary_dir" ] && [ -f "$binary_dir/apm" ]; then
echo "Processing $binary_dir directory..."
# Ensure the binary is executable before archiving
chmod +x "${binary_dir}/apm"
# Create tar.gz with preserved permissions, archiving ONLY the binary directory for release
tar -czf "${binary}.tar.gz" -C "${artifact_dir}/dist" "${binary}"
if command -v sha256sum &> /dev/null; then
sha256sum "${binary}.tar.gz" > "${binary}.tar.gz.sha256"
elif command -v shasum &> /dev/null; then
shasum -a 256 "${binary}.tar.gz" > "${binary}.tar.gz.sha256"
fi
echo "Created ${binary}.tar.gz"
else
echo "ERROR: Binary directory $binary_dir not found or $binary_dir/apm missing"
echo "Artifact directory contents:"
ls -la "$artifact_dir/" || echo "Directory $artifact_dir does not exist"
if [ -d "$artifact_dir/dist" ]; then
echo "Dist directory contents:"
ls -la "$artifact_dir/dist/"
fi
exit 1
fi
done
- name: Determine release type
id: release_type
run: |
TAG_NAME="${{ github.ref_name }}"
# Check if tag matches stable semver pattern (e.g., v1.2.3, v0.2.0)
# Stable: starts with v, followed by digits.digits.digits, then end of string
if [[ "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "Detected stable release: $TAG_NAME"
# Check PEP 440 prerelease patterns: v1.2.3a1, v1.2.3b1, v1.2.3rc1, etc.
elif [[ "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(a|b|rc)[0-9]+$ ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "Detected PEP 440 prerelease: $TAG_NAME"
else
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "Detected other prerelease: $TAG_NAME"
fi
- name: Create GitHub Release
id: release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
prerelease: ${{ steps.release_type.outputs.is_prerelease == 'true' }}
files: |
./dist/apm-linux-x86_64.tar.gz
./dist/apm-linux-x86_64.tar.gz.sha256
./dist/apm-linux-arm64.tar.gz
./dist/apm-linux-arm64.tar.gz.sha256
./dist/apm-darwin-x86_64.tar.gz
./dist/apm-darwin-x86_64.tar.gz.sha256
./dist/apm-darwin-arm64.tar.gz
./dist/apm-darwin-arm64.tar.gz.sha256
# Publish to PyPI (only stable releases from public repo)
publish-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [test, build, integration-tests, release-validation, create-release]
if: github.ref_type == 'tag' && needs.create-release.outputs.is_private_repo != 'true' && needs.create-release.outputs.is_prerelease != 'true'
environment:
name: pypi
url: https://pypi.org/p/apm-cli
permissions:
contents: read # Required for actions/checkout
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build Python package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true
# Update Homebrew formula (only stable releases from public repo)
update-homebrew:
name: Update Homebrew Formula
runs-on: ubuntu-latest
needs: [test, build, integration-tests, release-validation, create-release, publish-pypi]
if: github.ref_type == 'tag' && needs.create-release.outputs.is_private_repo != 'true' && needs.create-release.outputs.is_prerelease != 'true'
permissions:
contents: read
steps:
- name: Extract SHA256 checksums from GitHub release
id: checksums
run: |
# Download the SHA256 checksum files from the GitHub release
RELEASE_TAG="${{ github.ref_name }}"
echo "Downloading checksums for release $RELEASE_TAG"
# Download checksum files directly from the release
curl -L -o apm-darwin-arm64.tar.gz.sha256 \
"https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-darwin-arm64.tar.gz.sha256"
curl -L -o apm-darwin-x86_64.tar.gz.sha256 \
"https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-darwin-x86_64.tar.gz.sha256"
curl -L -o apm-linux-x86_64.tar.gz.sha256 \
"https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-linux-x86_64.tar.gz.sha256"
curl -L -o apm-linux-arm64.tar.gz.sha256 \
"https://github.com/${{ github.repository }}/releases/download/$RELEASE_TAG/apm-linux-arm64.tar.gz.sha256"
# Extract SHA256 checksums
DARWIN_ARM64_SHA=$(cat apm-darwin-arm64.tar.gz.sha256 | cut -d' ' -f1)
DARWIN_X86_64_SHA=$(cat apm-darwin-x86_64.tar.gz.sha256 | cut -d' ' -f1)
LINUX_X86_64_SHA=$(cat apm-linux-x86_64.tar.gz.sha256 | cut -d' ' -f1)
LINUX_ARM64_SHA=$(cat apm-linux-arm64.tar.gz.sha256 | cut -d' ' -f1)
echo "darwin-arm64-sha=$DARWIN_ARM64_SHA" >> $GITHUB_OUTPUT
echo "darwin-x86_64-sha=$DARWIN_X86_64_SHA" >> $GITHUB_OUTPUT
echo "linux-x86_64-sha=$LINUX_X86_64_SHA" >> $GITHUB_OUTPUT
echo "linux-arm64-sha=$LINUX_ARM64_SHA" >> $GITHUB_OUTPUT
echo "Darwin ARM64 SHA: $DARWIN_ARM64_SHA"
echo "Darwin x86_64 SHA: $DARWIN_X86_64_SHA"
echo "Linux x86_64 SHA: $LINUX_X86_64_SHA"
echo "Linux ARM64 SHA: $LINUX_ARM64_SHA"
- name: Trigger Homebrew tap repository update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.GH_PKG_PAT }}
repository: microsoft/homebrew-apm
event-type: formula-update
client-payload: |
{
"release": {
"version": "${{ github.ref_name }}",
"tag": "${{ github.ref_name }}",
"repository": "${{ github.repository }}"
},
"checksums": {
"darwin_arm64": "${{ steps.checksums.outputs.darwin-arm64-sha }}",
"darwin_x86_64": "${{ steps.checksums.outputs.darwin-x86_64-sha }}",
"linux_x86_64": "${{ steps.checksums.outputs.linux-x86_64-sha }}",
"linux_arm64": "${{ steps.checksums.outputs.linux-arm64-sha }}"
}
}