fix(release): repair v0.27 integration contracts #5506
Workflow file for this run
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 | |
| env: | |
| PYTHON_VERSION: '3.12' | |
| on: | |
| pull_request: | |
| branches: [ main ] | |
| # Tier 1 also runs in merge queue context so the same unit + build checks | |
| # execute against the tentative merge commit that the queue creates. See | |
| # microsoft/apm#770 for the design. | |
| merge_group: | |
| branches: [ main ] | |
| types: [ checks_requested ] | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Fast lint gate -- runs in ~3s using Astral's ruff-action (no Python/uv setup needed). | |
| # Fails fast on style, import, and complexity violations before the heavier build-and-test job. | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: astral-sh/setup-uv@v6 | |
| - name: Ruff lint | |
| run: uv run --frozen --extra dev ruff check src/ tests/ | |
| - name: Ruff format check | |
| run: uv run --frozen --extra dev ruff format --check src/ tests/ | |
| - name: Check YAML encoding safety | |
| run: | | |
| # Ensure YAML file I/O goes through yaml_io helpers. | |
| # Catches yaml.dump/safe_dump writing to a file handle outside yaml_io.py. | |
| VIOLATIONS=$(grep -rn --include='*.py' -P \ | |
| 'yaml\.(safe_)?dump\(.+,\s*[a-zA-Z_]\w*\b' src/apm_cli/ \ | |
| | grep -v 'utils/yaml_io.py' \ | |
| | grep -v '# yaml-io-exempt' \ | |
| || true) | |
| if [ -n "$VIOLATIONS" ]; then | |
| echo "::error::Direct yaml.dump() to file handle detected. Use yaml_io.dump_yaml() instead:" | |
| echo "$VIOLATIONS" | |
| exit 1 | |
| fi | |
| - name: File length guardrail | |
| run: | | |
| # Ruff has no max-module-lines rule. This check prevents new files from | |
| # exceeding the current worst case. Tighten the threshold over time. | |
| MAX_LINES=2100 # Stage 1 (was 2450); target 1400 deferred to Stage 2 | |
| VIOLATIONS=$(find src/ -name '*.py' -print0 | xargs -0 -I{} awk -v max="$MAX_LINES" \ | |
| 'END { if (NR > max) printf "%s: %d lines (max %d)\n", FILENAME, NR, max }' {}) | |
| if [ -n "$VIOLATIONS" ]; then | |
| echo "::error::Source files exceed $MAX_LINES-line limit:" | |
| echo "$VIOLATIONS" | |
| exit 1 | |
| fi | |
| - name: Lint - no raw str(relative_to) patterns | |
| run: | | |
| # Fail if any code uses str(x.relative_to(y)) instead of portable_relpath() | |
| if grep -rn --include="*.py" -P 'str\([^)]*\.relative_to\(' src/apm_cli/ | grep -v portable_relpath | grep -v '\.pyc'; then | |
| echo "::error::Found raw str(path.relative_to()) calls. Use portable_relpath() from apm_cli.utils.paths instead." | |
| exit 1 | |
| fi | |
| - name: Code duplication guardrail (pylint R0801) | |
| run: | | |
| uv run --frozen --extra dev python -m pylint \ | |
| --disable=all --enable=R0801 \ | |
| --min-similarity-lines=10 \ | |
| --fail-on=R0801 \ | |
| src/apm_cli/ | |
| - name: Lint - auth-protocol boundary (#1212 anti-regression) | |
| run: bash scripts/lint-auth-signals.sh | |
| - name: Lint - architecture authority boundaries | |
| run: bash scripts/lint-architecture-boundaries.sh | |
| # Focused Windows compatibility gate -- runs at PR time (not just | |
| # post-merge in build-release.yml) so Windows-only path, encoding, | |
| # process, and junction regressions are caught before merge. Python | |
| # contracts are selected declaratively via the `windows_compat` | |
| # marker; the contributor dashboard's dependency-free Node suite | |
| # separately exercises its static-asset junction boundary. This is | |
| # intentionally narrower than either full suite. | |
| windows-compat-gate: | |
| name: Windows Compatibility Gate | |
| runs-on: windows-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| - name: Run contributor dashboard Node tests | |
| run: >- | |
| node --test | |
| packages/apm-contributor-dashboard/tests/logic.test.mjs | |
| packages/apm-contributor-dashboard/tests/server.test.mjs | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --frozen --extra dev | |
| - name: Diagnostics (platform, git, python) | |
| run: | | |
| python --version | |
| python -c "import sys; print('platform:', sys.platform)" | |
| git --version | |
| where git | |
| - name: Run cross-platform contract family | |
| run: >- | |
| uv run --extra dev pytest -p no:cacheprovider -v | |
| -m windows_compat | |
| tests/unit | |
| - name: Diagnostics on failure | |
| if: failure() | |
| run: | | |
| echo "::group::Environment" | |
| Get-ChildItem Env: | Sort-Object Name | |
| echo "::endgroup::" | |
| echo "::group::git config" | |
| git config --list --show-origin | |
| echo "::endgroup::" | |
| test-architecture: | |
| name: Test Architecture Ratchets | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --frozen --extra dev | |
| - name: Run ratchet contract tests | |
| env: | |
| APM_ALLOW_PROVISIONAL_BASELINES: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == true && '1' || '0' }} | |
| run: | | |
| uv run --frozen --extra dev pytest -p no:cacheprovider -q \ | |
| tests/unit/scripts/test_check_test_assertions.py \ | |
| tests/unit/scripts/test_check_exact_test_duplicates.py \ | |
| tests/quality/test_test_taxonomy.py \ | |
| tests/quality/test_quality_baselines.py \ | |
| tests/quality/test_ci_topology.py | |
| # Linux-only for PR feedback. Full platform matrix (incl. macOS + Windows) runs post-merge in build-release.yml. | |
| # | |
| # Two-way sharded with pytest-split, mirroring the pattern proven in | |
| # ci-integration.yml. Each shard is an independent runner; xdist inside | |
| # each shard parallelises within. | |
| # | |
| # The shard jobs themselves are the required checks at PR time (see | |
| # merge-gate.yml EXPECTED_CHECKS). An earlier iteration of this | |
| # workflow used a fan-in `Build & Test (Linux)` job as the required | |
| # check, but that re-added ~3 min of runner-allocation latency between | |
| # the matrix `needs:` resolution and the dependent job's runner pickup, | |
| # eating most of the sharding win. The fan-in survives as the | |
| # non-required `coverage-combine` job below: it still combines and | |
| # enforces the global 80% floor, just off the PR-time critical path, | |
| # and is escalated to required on `merge_group`. | |
| # | |
| # First runs use pytest-split's naive file-based split (no .test_durations | |
| # committed yet); commit a durations file in a follow-up PR after one | |
| # green run to balance shards better. | |
| build-and-test-shard: | |
| name: Build & Test Shard ${{ matrix.shard }} (Linux) | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| if: matrix.shard == 1 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| - name: Run contributor dashboard Node tests | |
| if: matrix.shard == 1 | |
| run: >- | |
| node --test | |
| packages/apm-contributor-dashboard/tests/logic.test.mjs | |
| packages/apm-contributor-dashboard/tests/server.test.mjs | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| # --extra build (PyInstaller + ~150MB of deps) is no longer pulled | |
| # into the test path. Binary packaging signal is preserved by the | |
| # parallel non-required `pr-binary-smoke` job below; the canonical | |
| # binary build still runs in ci-integration.yml on merge_group and | |
| # in build-release.yml on release. | |
| - name: Install dependencies | |
| run: uv sync --frozen --extra dev | |
| # --splits 2 --group N matches the layout used by ci-integration.yml. | |
| # -n 2 --dist worksteal parallelises within the shard. | |
| # | |
| # Per-shard floor of 60% catches regressions at PR time on roughly | |
| # half the codebase (observed per-shard coverage today is ~65%). | |
| # The global 80% floor declared in pyproject.toml runs after combine | |
| # in the non-required `coverage-combine` job below, and is also | |
| # enforced on `merge_group` via ci-integration.yml -- so the union | |
| # gate is preserved on the path that actually merges to main. | |
| - name: Run tests with coverage | |
| run: | | |
| uv run pytest tests/unit tests/test_console.py tests/red_team \ | |
| --splits 2 --group ${{ matrix.shard }} \ | |
| -n 2 --dist worksteal \ | |
| --cov --cov-report= --cov-fail-under=60 | |
| - name: Rename coverage data for shard | |
| if: always() | |
| run: | | |
| if [ -f .coverage ]; then | |
| mv .coverage .coverage.unit-shard-${{ matrix.shard }} | |
| fi | |
| - name: Upload coverage data | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: unit-coverage-shard-${{ matrix.shard }} | |
| path: .coverage.unit-shard-${{ matrix.shard }} | |
| include-hidden-files: true | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # Coverage combine + global 80% gate. | |
| # | |
| # NOT a required check. Earlier iterations of this workflow made the | |
| # combined job required, but doing so re-added ~3 min of GitHub Actions | |
| # runner-allocation latency between the matrix `needs:` resolution and | |
| # this job's runner pickup, eating most of the sharding win. The shard | |
| # jobs themselves are the required checks (see merge-gate.yml | |
| # EXPECTED_CHECKS); this job exists so the global 80% coverage floor | |
| # still surfaces as a red check on the PR if it drops, and the | |
| # canonical merge-time enforcement runs in ci-integration.yml on | |
| # `merge_group`. | |
| coverage-combine: | |
| name: Coverage Combine (Linux) | |
| needs: [build-and-test-shard] | |
| if: always() | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install coverage | |
| run: pip install "coverage[toml]" | |
| - name: Download shard coverage | |
| if: always() | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: unit-coverage-shard-* | |
| path: coverage-shards/ | |
| merge-multiple: true | |
| - name: Combine and summarise coverage | |
| if: always() | |
| run: | | |
| if find coverage-shards -type f -name '.coverage*' 2>/dev/null | grep -q .; then | |
| coverage combine coverage-shards/ | |
| coverage json -o coverage.json | |
| python3 scripts/coverage-summary.py coverage.json --title "Unit Test Coverage" | |
| else | |
| echo "No coverage shard files found; skipping unit coverage summary." | |
| fi | |
| # Same 80% floor pyproject.toml declares. Non-blocking at PR time | |
| # (this job is not in merge-gate.yml's EXPECTED_CHECKS), but the | |
| # red check is visible in the PR UI. ci-integration.yml enforces | |
| # the same floor on `merge_group`, which IS the merge gate. | |
| - name: Enforce unit coverage gate | |
| if: always() | |
| run: | | |
| if [ -f .coverage ]; then | |
| coverage report --fail-under=80 | |
| else | |
| echo "ERROR: No combined coverage data -- the 'Combine and summarise coverage' step must produce .coverage before the gate can run." | |
| exit 1 | |
| fi | |
| # Non-required parallel job that builds the PyInstaller binary at PR | |
| # time so packaging regressions surface in review without gating the | |
| # critical path. The canonical binary build still runs in | |
| # ci-integration.yml on merge_group (used by smoke + integration jobs) | |
| # and in build-release.yml on release. | |
| pr-binary-smoke: | |
| name: PR Binary Smoke (Linux) | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --frozen --extra dev --extra build | |
| - name: Install UPX | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y upx-ucl | |
| - name: Build binary | |
| run: | | |
| chmod +x scripts/build-binary.sh | |
| uv run ./scripts/build-binary.sh | |
| # Dogfood the audit-only CI gate we ship and document to users: | |
| # - `apm audit --ci` -- the full producer+consumer gate. Runs the | |
| # lockfile / content-integrity checks AND the install-replay drift | |
| # check. The drift replay is cache-free here because every dependency | |
| # in apm.lock.yaml is local (self ``.apm/`` + in-repo ``packages/*``, | |
| # resolved from the checkout via path anchoring), so no warm cache or | |
| # prior ``apm install`` is required. setup-only: the committed managed | |
| # files are left untouched so both content-integrity (SHA-256 tamper) | |
| # and drift (replay vs committed tree, incl. ``.agents/skills/``) can | |
| # detect divergence. See microsoft/apm#883 and #1716. Tier 1. | |
| apm-self-check: | |
| name: APM Self-Check | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Dogfood the repo's OWN apm (built from this branch), not a published | |
| # binary. A self-check that runs a stale released apm structurally | |
| # cannot validate the fixes the repo ships: this repo's new local-path | |
| # packages (./packages/*) depend on the local-dep source-tagging and | |
| # transitive-resolution fixes that land IN this tree but predate any | |
| # release. The published action (0.14.0 default; 0.16.1 too) false-flags | |
| # those deps on ref-consistency and no-orphaned-packages. `uv run` syncs | |
| # the project and runs the in-tree apm. No `apm install` is run, so the | |
| # committed managed files are preserved: content-integrity detects | |
| # tampered file hashes and the drift replay compares the committed tree | |
| # (including .agents/skills/) against a fresh cache-free replay. | |
| - uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| # Full producer+consumer gate. Verifies every file in | |
| # lockfile.deployed_files exists, ref consistency between apm.yml and | |
| # apm.lock.yaml, no orphan packages, content-integrity (SHA-256 hashes | |
| # against deployed_file_hashes in the lockfile) on deployed content, AND | |
| # drift: a cache-free install-replay compared against the committed tree | |
| # across every governed deploy root (.github AND .agents). The replay is | |
| # cache-free because all deps are local (see job comment); dropping | |
| # --no-drift is therefore safe and closes the gap where committed | |
| # .agents/skills/ content could silently diverge from source (#1716). | |
| - name: apm audit --ci | |
| run: uv run --frozen apm audit --ci | |
| # Promotes the smallest stable hermetic slice of the real Consume/Produce/ | |
| # Govern lifecycle contracts to a PR-time required check. Tier 1. | |
| # | |
| # Why this job exists | |
| # -------------------- | |
| # Every module under tests/integration/ -- including marker-classified | |
| # producer/consumer/govern contracts and the regression coverage added by | |
| # #2226 (ADO lock coordinate | |
| # preservation) and #2240 (virtual/manifestless lifecycle convergence) -- | |
| # only ran on merge_group (ci-integration.yml), never as a required PR | |
| # check. A revert of either fix could sit un-caught until a PR reached the | |
| # merge queue. This job closes that gap for the subset that is both | |
| # hermetic and fast enough for the PR-time critical path. | |
| # | |
| # Deterministic release selection | |
| # ------------------------------- | |
| # The bounded required release expression excludes contracts explicitly | |
| # marked `lifecycle_merge_group`; ci-integration.yml's full integration | |
| # shards still execute them from the unfiltered tests/integration root. | |
| # | |
| # Why this required partition, Consume/Produce/Govern coverage | |
| # ------------------------------------------------------------- | |
| # The marker family retains one static authority guard plus policy, | |
| # content-hash, hook, virtual-package, audit, auth, and installed-console | |
| # lifecycle contracts. Real subprocess rows use the uv-installed `apm` | |
| # console script and local Git for pack/install/compile/audit closure, | |
| # target widen/narrow, reinstall idempotency, prune/uninstall cascade, | |
| # tamper/repair, and mixed primitives. | |
| # This is installed-console-script coverage, not frozen PyInstaller parity; | |
| # the packaged-platform lane owns frozen-binary coverage. | |
| # | |
| # #2238 (bounded best-effort ref-lookup retries) is intentionally NOT | |
| # duplicated here: its regression coverage | |
| # (tests/unit/deps/test_download_strategies_phase3.py, | |
| # tests/unit/deps/test_git_reference_resolver.py) is unit-level and | |
| # already required today via build-and-test-shard. | |
| # | |
| # Why this stays hermetic, network-free, and credential-free | |
| # ------------------------------------------------------------ | |
| # Subprocess nodes carry requires_apm_binary and requires_e2e_mode. | |
| # `uv sync --extra dev` puts the installed console script on PATH, while the | |
| # pytest step's APM_E2E_TESTS=1 enables only the declarative E2E prerequisite. | |
| # APM_RUN_INTEGRATION_TESTS remains unset: no live network integration is | |
| # enabled. No node carries a token prerequisite. | |
| # tests/utils/isolated_apm_environment.py's socket guard denies ALL | |
| # AF_INET/AF_INET6 use (including loopback) inside the subprocess these | |
| # tests drive; git remotes are rewritten to local bare repos via | |
| # `git config url.<path>.insteadOf`. This job declares no `secrets:` and no | |
| # GITHUB_APM_PAT/ADO_APM_PAT env, so it is structurally unable to depend on | |
| # credentials. | |
| # | |
| # Why timeout-minutes: 5 | |
| # ------------------------ | |
| # Recent hosted runs of the current required selection complete in about | |
| # three minutes. The 5-minute job cap includes checkout/setup/sync and | |
| # still fails fast on accidental network retries or unreviewed marker growth. | |
| # Hosted job duration is the release authority. | |
| # | |
| # tests/quality/test_ci_topology.py independently collects the full, | |
| # merge-group-only, and required selections and guards their set partition, | |
| # this exact expression, the unfiltered merge-group integration path, | |
| # --strict-markers, required-check membership, E2E opt-in placement, and | |
| # credential/network prohibitions. | |
| lifecycle-smoke: | |
| name: Lifecycle Smoke (Linux) | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v6 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --frozen --extra dev | |
| - name: Run required lifecycle smoke subset | |
| env: | |
| APM_E2E_TESTS: "1" | |
| run: | | |
| uv run --extra dev pytest -p no:cacheprovider -q --strict-markers \ | |
| -m 'lifecycle_smoke and not lifecycle_merge_group' tests/integration |