Repository navigation
CI/CD Pipeline #2086
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 * * *' | |
| 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 |