Skip to content

CI/CD Pipeline

CI/CD Pipeline #2086

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 * * *'
repository_dispatch:
types: [manual-build-release]
# Restrict default GITHUB_TOKEN permissions for all jobs (principle of least privilege)
permissions:
contents: read
jobs:
# Unit tests + binary build combined (Linux + Windows).
# Merging test+build eliminates ~1.5m runner re-provisioning overhead per platform.
# macOS runners are scarce and have their own consolidated jobs below.
build-and-test:
name: Build & Test
strategy:
fail-fast: false
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: windows-latest
arch: x86_64
platform: windows
binary_name: apm-windows-x86_64
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install dependencies
run: uv sync --frozen --extra dev --extra build
# Keep the installer gate ahead of the broad unit suite so an unrelated
# platform test failure cannot hide installer-specific Windows evidence.
- name: Test install.ps1 end-to-end (Windows)
if: matrix.platform == 'windows'
shell: pwsh
env:
APM_E2E_TESTS: "1"
run: >-
uv run --frozen pytest
tests/integration/test_windows_installer_launchers.py
-vv -ra --tb=short
# Unit tests run on every push for fast platform-regression signal.
# Smoke is intentionally NOT included here: it duplicates ci-integration.yml's
# merge-time smoke gate and burns a real codex binary download per platform
# per push (~15 redundant runs/day). Smoke is gated to promotion boundaries
# below (tag/schedule/dispatch) where it actually serves as a pre-ship gate.
- name: Run unit tests
if: matrix.platform != 'windows'
env:
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: uv run pytest tests/unit tests/test_console.py -n auto --dist worksteal
# Named starts/outcomes identify unfinished tests without dumping captures.
# This bounds an opaque hang; it does not turn a timeout into success.
- name: Run unit tests (Windows diagnostics)
if: matrix.platform == 'windows'
timeout-minutes: 60
env:
PYTHONUNBUFFERED: "1"
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: >-
uv run pytest tests/unit tests/test_console.py -n auto --dist worksteal
-vv --tb=short --show-capture=no --no-showlocals
# Smoke runs only at promotion boundaries:
# - tags (pre-ship release gate; only place tag-cut releases get smoke validation)
# - schedule (nightly regression catch for upstream codex URL drift)
# - repository_dispatch (default-branch manual safety net)
- name: Run smoke tests
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch'
env:
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GH_MODELS_PAT }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: uv run pytest tests/integration/test_runtime_smoke.py -v
- name: Install UPX (Linux)
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y upx-ucl
- name: Build binary (Unix)
if: matrix.platform != 'windows'
run: |
chmod +x scripts/build-binary.sh
uv run ./scripts/build-binary.sh
- name: Build binary (Windows)
if: matrix.platform == 'windows'
shell: pwsh
run: |
uv run pwsh scripts/windows/build-binary.ps1
- name: Sign Windows binary (Authenticode)
if: matrix.platform == 'windows' && env.WINDOWS_CERT_PFX != ''
shell: pwsh
env:
WINDOWS_CERT_PFX: ${{ secrets.WINDOWS_CERT_PFX }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
BINARY_DIR: dist\apm-windows-x86_64
run: |
uv run pwsh scripts/windows/sign-binary.ps1
- name: Upload binary as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.binary_name }}
path: |
./dist/${{ matrix.binary_name }}
./dist/${{ matrix.binary_name }}.sha256
./scripts/test-release-validation.sh
./scripts/windows/test-release-validation.ps1
./scripts/test-dependency-integration.sh
./scripts/windows/test-dependency-integration.ps1
./scripts/github-token-helper.sh
./scripts/windows/github-token-helper.ps1
include-hidden-files: true # Required to include .apm directories
retention-days: 30
if-no-files-found: error
# Consolidated macOS Intel build + validation.
# Intel runners are plentiful (<1 min queue) so this runs on every push for
# early macOS build-regression feedback. Integration and release-validation
# phases are conditional on tag/schedule/dispatch.
# Root node - runs its own unit tests instead of waiting for build-and-test.
build-and-validate-macos-intel:
name: Build & Validate (macOS x86_64)
runs-on: macos-15-intel
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup build environment
run: |
sudo xcode-select --install 2>/dev/null || true
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
- name: Install UPX
run: brew install upx
- name: Install Unix shell startup test runtimes
run: |
brew list bash >/dev/null 2>&1 || brew install bash
brew list fish >/dev/null 2>&1 || brew install fish
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install dependencies
run: uv sync --frozen --extra dev --extra build
- name: Run unit tests
run: uv run pytest tests/unit tests/test_console.py -n auto --dist worksteal
# PHASE 1: BUILD BINARY
- name: Build binary
run: |
chmod +x scripts/build-binary.sh
uv run ./scripts/build-binary.sh
- name: Test macOS non-shell binary startup
env:
APM_E2E_TESTS: "1"
APM_BINARY_PATH: ${{ github.workspace }}/dist/apm-darwin-x86_64/apm
run: |
test -x "$APM_BINARY_PATH"
uv run --frozen pytest \
tests/integration/test_core_smoke.py::TestBinaryStartup::test_apm_version_runs \
tests/integration/test_core_smoke.py::TestBinaryStartup::test_apm_rich_table_runs \
-vv -ra --tb=short
- name: Upload binary as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: apm-darwin-x86_64
path: |
./dist/apm-darwin-x86_64
./dist/apm-darwin-x86_64.sha256
./scripts/test-release-validation.sh
./scripts/test-dependency-integration.sh
./scripts/github-token-helper.sh
include-hidden-files: true
retention-days: 30
if-no-files-found: error
# PHASE 2: FOCUSED INTEGRATION TESTS (tag/schedule/dispatch only)
- name: Run focused Intel integration tests
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch'
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
# Intel validates the promoted lifecycle subset against its native
# binary. Live credential acceptance belongs to auth-acceptance.yml.
# loadgroup preserves HOME-mutating test serialization.
PYTEST_MARK_EXPR: "lifecycle_smoke and not live"
PYTEST_EXTRA_ARGS: "-n 4 --dist loadgroup"
run: |
chmod +x scripts/test-integration.sh
uv run ./scripts/test-integration.sh
timeout-minutes: 30
# PHASE 3: RELEASE VALIDATION (tag/schedule/dispatch only)
- name: Prepare isolated release-validation environment
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch'
run: |
mkdir -p /tmp/apm-isolated-test/dist
cp -R ./dist/apm-darwin-x86_64 /tmp/apm-isolated-test/dist/
cp ./dist/apm-darwin-x86_64.sha256 /tmp/apm-isolated-test/dist/ 2>/dev/null || true
cp ./scripts/test-release-validation.sh /tmp/apm-isolated-test/
cp ./scripts/test-dependency-integration.sh /tmp/apm-isolated-test/ 2>/dev/null || true
cp ./scripts/github-token-helper.sh /tmp/apm-isolated-test/ 2>/dev/null || true
cd /tmp/apm-isolated-test
chmod +x ./dist/apm-darwin-x86_64/apm
ln -s "$(pwd)/dist/apm-darwin-x86_64/apm" "$(pwd)/apm"
echo "/tmp/apm-isolated-test" >> "$GITHUB_PATH"
- name: Run release validation tests
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch'
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: |
cd /tmp/apm-isolated-test
chmod +x test-release-validation.sh
./test-release-validation.sh
timeout-minutes: 20
# Consolidated macOS ARM build + validation.
# ARM runners (macos-latest) are extremely scarce - 2-4+ hour queue waits are
# common. This job is gated to tag/schedule/dispatch only so push-to-main never
# blocks on ARM availability. All phases run unconditionally since the job itself
# is already release-gated.
# Root node - runs its own unit tests instead of waiting for build-and-test.
build-and-validate-macos-arm:
name: Build & Validate (macOS arm64)
if: github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch'
runs-on: macos-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install UPX
run: brew install upx
- name: Install Unix shell startup test runtimes
run: |
brew list bash >/dev/null 2>&1 || brew install bash
brew list fish >/dev/null 2>&1 || brew install fish
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install dependencies
run: uv sync --frozen --extra dev --extra build
- name: Run unit tests
run: uv run pytest tests/unit tests/test_console.py -n auto --dist worksteal
# PHASE 1: BUILD BINARY
- name: Build binary
run: |
chmod +x scripts/build-binary.sh
uv run ./scripts/build-binary.sh
- name: Test macOS non-shell binary startup
env:
APM_E2E_TESTS: "1"
APM_BINARY_PATH: ${{ github.workspace }}/dist/apm-darwin-arm64/apm
run: |
test -x "$APM_BINARY_PATH"
uv run --frozen pytest \
tests/integration/test_core_smoke.py::TestBinaryStartup::test_apm_version_runs \
tests/integration/test_core_smoke.py::TestBinaryStartup::test_apm_rich_table_runs \
-vv -ra --tb=short
- name: Upload binary as workflow artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: apm-darwin-arm64
path: |
./dist/apm-darwin-arm64
./dist/apm-darwin-arm64.sha256
./scripts/test-release-validation.sh
./scripts/test-dependency-integration.sh
./scripts/github-token-helper.sh
include-hidden-files: true
retention-days: 30
if-no-files-found: error
# PHASE 2: INTEGRATION TESTS
- name: Run integration tests
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
# Scarce ARM runners force the full suite onto one runner, so
# parallelise the non-live corpus in-process with xdist.
# --dist loadgroup honors the
# home_env xdist_group markers that keep HOME-mutating tests
# serialized on a single worker.
PYTEST_MARK_EXPR: "not live"
PYTEST_EXTRA_ARGS: "-n 4 --dist loadgroup"
run: |
chmod +x scripts/test-integration.sh
uv run ./scripts/test-integration.sh
timeout-minutes: 60
# PHASE 3: RELEASE VALIDATION
- name: Prepare isolated release-validation environment
run: |
mkdir -p /tmp/apm-isolated-test/dist
cp -R ./dist/apm-darwin-arm64 /tmp/apm-isolated-test/dist/
cp ./dist/apm-darwin-arm64.sha256 /tmp/apm-isolated-test/dist/ 2>/dev/null || true
cp ./scripts/test-release-validation.sh /tmp/apm-isolated-test/
cp ./scripts/test-dependency-integration.sh /tmp/apm-isolated-test/ 2>/dev/null || true
cp ./scripts/github-token-helper.sh /tmp/apm-isolated-test/ 2>/dev/null || true
cd /tmp/apm-isolated-test
chmod +x ./dist/apm-darwin-arm64/apm
ln -s "$(pwd)/dist/apm-darwin-arm64/apm" "$(pwd)/apm"
echo "/tmp/apm-isolated-test" >> "$GITHUB_PATH"
- name: Run release validation tests
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: |
cd /tmp/apm-isolated-test
chmod +x test-release-validation.sh
./test-release-validation.sh
timeout-minutes: 20
# Integration tests with full source code access (Linux + Windows).
# macOS integration tests run inside the consolidated macOS jobs to avoid re-queuing scarce runners.
# 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: >
always() &&
(github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch') &&
(github.event_name == 'schedule' || needs.build-and-test.result == 'success')
needs: [build-and-test]
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: windows-latest
arch: x86_64
platform: windows
binary_name: apm-windows-x86_64
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download APM binary from build artifacts
continue-on-error: ${{ github.event_name == 'schedule' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.binary_name }}
path: ./
- name: Check downloaded binary artifact
id: artifact
if: always()
shell: bash
run: |
if [ -d "./dist/${{ matrix.binary_name }}" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::warning::No ${{ matrix.binary_name }} artifact; skipping scheduled integration validation for this platform."
fi
- name: Set up Node.js
if: github.event_name != 'schedule' || steps.artifact.outputs.available == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
- name: Set up Python
if: github.event_name != 'schedule' || steps.artifact.outputs.available == 'true'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
if: github.event_name != 'schedule' || steps.artifact.outputs.available == 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
- name: Install Unix shell startup test runtimes
if: matrix.platform != 'windows' && (github.event_name != 'schedule' || steps.artifact.outputs.available == 'true')
run: |
sudo apt-get update
sudo apt-get install -y fish zsh
- name: Install test dependencies
if: github.event_name != 'schedule' || steps.artifact.outputs.available == 'true'
run: uv sync --frozen --extra dev
- name: Run integration tests (Unix)
if: matrix.platform != 'windows' && (github.event_name != 'schedule' || steps.artifact.outputs.available == 'true')
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }} # Primary: APM module access
# The tag gate runs the non-live corpus on one runner. Four workers stay
# below the merge gate's aggregate width while loadgroup preserves
# xdist_group serialization for HOME-mutating tests.
PYTEST_MARK_EXPR: "not live"
PYTEST_EXTRA_ARGS: "-n 4 --dist loadgroup"
run: |
chmod +x scripts/test-integration.sh
uv run ./scripts/test-integration.sh
timeout-minutes: 60
- name: Run integration tests (Windows)
if: matrix.platform == 'windows' && (github.event_name != 'schedule' || steps.artifact.outputs.available == 'true')
shell: pwsh
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: |
uv run pwsh scripts/windows/test-integration.ps1 -SkipBuild
timeout-minutes: 20
# Release validation tests - Final pre-release validation of shipped binary (Linux + Windows).
# macOS release validation runs inside the consolidated macOS jobs.
release-validation:
name: Release Validation
if: >
always() &&
(github.ref_type == 'tag' || github.event_name == 'schedule' || github.event_name == 'repository_dispatch') &&
(github.event_name == 'schedule' ||
(needs.build-and-test.result == 'success' && needs.integration-tests.result == 'success'))
needs: [build-and-test, 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: windows-latest
arch: x86_64
platform: windows
binary_name: apm-windows-x86_64
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Download APM binary from build artifacts
continue-on-error: ${{ github.event_name == 'schedule' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.binary_name }}
path: ${{ matrix.platform == 'windows' && 'D:\apm-isolated-test' || '/tmp/apm-isolated-test/' }}
- name: Check downloaded binary artifact (Unix)
id: artifact_unix
if: always() && matrix.platform != 'windows'
run: |
if [ -d "/tmp/apm-isolated-test/dist/${{ matrix.binary_name }}" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::warning::No ${{ matrix.binary_name }} artifact; skipping scheduled release validation for this platform."
fi
- name: Check downloaded binary artifact (Windows)
id: artifact_windows
if: always() && matrix.platform == 'windows'
shell: pwsh
run: |
if (Test-Path "D:\apm-isolated-test\dist\${{ matrix.binary_name }}") {
"available=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
} else {
"available=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
Write-Warning "No ${{ matrix.binary_name }} artifact; skipping scheduled release validation for this platform."
}
- name: Make binary executable and verify isolation (Unix)
if: matrix.platform != 'windows' && (github.event_name != 'schedule' || steps.artifact_unix.outputs.available == 'true')
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 (Unix)
if: matrix.platform != 'windows' && (github.event_name != 'schedule' || steps.artifact_unix.outputs.available == 'true')
run: |
cd /tmp/apm-isolated-test
ln -s "$(pwd)/dist/${{ matrix.binary_name }}/apm" "$(pwd)/apm"
echo "/tmp/apm-isolated-test" >> "$GITHUB_PATH"
- name: Verify binary and add to PATH (Windows)
if: matrix.platform == 'windows' && (github.event_name != 'schedule' || steps.artifact_windows.outputs.available == 'true')
shell: pwsh
run: |
cd D:\apm-isolated-test
# Debug: List the downloaded structure
Write-Host "Downloaded structure:"
Get-ChildItem -Recurse -Filter "apm.exe"
Get-ChildItem .\dist\
# Add binary directory to PATH
$binDir = "D:\apm-isolated-test\dist\${{ matrix.binary_name }}"
echo $binDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Run release validation tests (Unix)
if: matrix.platform != 'windows' && (github.event_name != 'schedule' || steps.artifact_unix.outputs.available == 'true')
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: |
cd /tmp/apm-isolated-test
chmod +x scripts/test-release-validation.sh
./scripts/test-release-validation.sh
timeout-minutes: 20
- name: Run release validation tests (Windows)
if: matrix.platform == 'windows' && (github.event_name != 'schedule' || steps.artifact_windows.outputs.available == 'true')
shell: pwsh
env:
APM_E2E_TESTS: "1"
GITHUB_API_TOKEN: ${{ github.token }}
GITHUB_APM_PAT: ${{ secrets.GH_CLI_PAT }}
run: |
cd D:\apm-isolated-test
.\scripts\windows\test-release-validation.ps1
timeout-minutes: 20
daily-release-smoke-signal:
name: Daily Release Smoke Signal
needs:
- build-and-test
- build-and-validate-macos-intel
- build-and-validate-macos-arm
- integration-tests
- release-validation
if: always() && github.event_name == 'schedule'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: File or recover daily smoke issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { main } = require('./scripts/daily-release-smoke-signal.cjs');
await main({ github, context, core });
create-release:
name: Create GitHub Release
needs: [build-and-test, build-and-validate-macos-intel, build-and-validate-macos-arm, integration-tests, release-validation]
if: github.event_name == 'push' && github.ref_type == 'tag' # Only pushed 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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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
binary="apm-windows-x86_64"
artifact_dir="${binary}"
binary_dir="${artifact_dir}/dist/${binary}"
if [ -d "$binary_dir" ] && [ -f "$binary_dir/apm.exe" ]; then
echo "Processing $binary_dir directory..."
(
cd "${artifact_dir}/dist"
zip -qr "../../${binary}.zip" "${binary}"
)
if command -v sha256sum &> /dev/null; then
sha256sum "${binary}.zip" > "${binary}.zip.sha256"
elif command -v shasum &> /dev/null; then
shasum -a 256 "${binary}.zip" > "${binary}.zip.sha256"
fi
echo "Created ${binary}.zip"
else
echo "ERROR: Binary directory $binary_dir not found or $binary_dir/apm.exe 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
- 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@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
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
./dist/apm-windows-x86_64.zip
./dist/apm-windows-x86_64.zip.sha256
# Deploy the docs site after the release is published.
# Invoked here (rather than via the docs workflow's `release: published`
# trigger) because release events created by GITHUB_TOKEN do not fire
# downstream workflow runs -- a documented Actions safeguard against
# recursion. Calling docs.yml directly preserves the standard release ->
# docs deploy chain without needing a PAT or GitHub App token.
deploy-docs:
name: Deploy Docs
needs: [create-release]
if: github.event_name == 'push' && github.ref_type == 'tag' && needs.create-release.outputs.is_prerelease != 'true'
uses: ./.github/workflows/docs.yml
permissions:
contents: read
pages: write
id-token: write
with:
is_prerelease: false
# GH-AW Compatibility - validates the released binary works in the
# exact flow GitHub Agentic Workflows uses (isolated install + pack, no token).
# Informational: runs as continue-on-error because it depends on external services
# (GitHub API, npm, microsoft/apm-action, microsoft/apm-sample-package) that can
# fail independently of APM code quality (~4% false-negative rate per execution).
gh-aw-compat:
name: GH-AW Compatibility
needs: [create-release]
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-24.04
continue-on-error: true
steps:
- name: Install and pack with apm-action
uses: microsoft/apm-action@d723bb64ed70c135bbaf87d126b721dd2dae0439 # v1.10.0
id: pack
with:
dependencies: |
- microsoft/apm-sample-package
isolated: 'true'
pack: 'true'
archive: 'true'
target: claude
apm-version: ${{ github.ref_name }}
working-directory: /tmp/gh-aw-compat-test
- name: Verify bundle
run: |
test -f "${{ steps.pack.outputs.bundle-path }}"
echo "[+] GH-AW compatibility test passed"
- name: Warn on failure
if: failure()
run: |
echo "::warning::GH-AW compatibility test failed. This is informational - external dependency may be unavailable. Investigate before next release."
build-pypi-distributions:
name: Build PyPI Distributions
runs-on: ubuntu-latest
needs: [create-release]
if: github.event_name == 'push' && github.ref_type == 'tag' && needs.create-release.outputs.is_private_repo != 'true' && needs.create-release.outputs.is_prerelease != 'true'
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
- name: Install locked build dependencies
run: uv sync --frozen --extra dev
- name: Build Python package
run: uv build --no-build-isolation --no-sources
- name: Upload Python distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-distributions
path: |
dist/*.whl
dist/*.tar.gz
if-no-files-found: error
# Publish to PyPI (only stable releases from public repo)
publish-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [build-pypi-distributions]
environment:
name: pypi
url: https://pypi.org/p/apm-cli
permissions:
actions: read
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- name: Download Python distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pypi-distributions
path: dist
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
with:
verbose: true