Skip to content

fix(release): repair v0.27 integration contracts #5506

fix(release): repair v0.27 integration contracts

fix(release): repair v0.27 integration contracts #5506

Workflow file for this run

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