CI/CD Pipeline #479
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" | |
| } | |
| } |