Repository navigation
fix(pipeline): expose unresolved call coverage #3123
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
| # PR validation: security gates + lint + the test legs this PR's changes need. | |
| # `changes` runs scripts/ci/select-lanes.sh on the PR's file list; every lane | |
| # job below runs only when selected (unknown paths, workflows, the harness, | |
| # Makefile.cbm and vendored code select everything). Builds, smoke and soak at | |
| # full breadth stay maintainer-driven via the dry-run workflow_dispatch. Branch | |
| # protection requires `dco` + `ci-ok` — a single stable summary context that | |
| # fails unless every PR stage succeeded and every selected lane ran green. | |
| name: PR | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| # A new push to the PR supersedes the running validation — cancel it | |
| # instead of stacking zombie pipelines. | |
| concurrency: | |
| group: pr-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| jobs: | |
| security: | |
| needs: [changes] | |
| uses: ./.github/workflows/_security.yml | |
| # The called job requests security-events:read to count code-scanning | |
| # alerts; a reusable workflow cannot request more than its caller grants, | |
| # so withholding this here fails the whole run at startup. | |
| permissions: | |
| contents: read | |
| security-events: read | |
| actions: read | |
| secrets: inherit | |
| with: | |
| lanes: ${{ needs.changes.outputs.lanes }} | |
| lint: | |
| needs: [changes] | |
| if: ${{ contains(fromJSON(needs.changes.outputs.lanes), 'lint') || contains(fromJSON(needs.changes.outputs.lanes), 'lint-mem') }} | |
| uses: ./.github/workflows/_lint.yml | |
| with: | |
| lanes: ${{ needs.changes.outputs.lanes }} | |
| # ── Smart CI, PR CI only: run only the tests this change can reach. | |
| # Active only while the repository variable TEST_IMPACT_MODE is `gate`; | |
| # unset (shadow) every test runs and test-impact-shadow only reports. | |
| # test-impact-select.sh --gate narrows only on a well-formed answer: a | |
| # failure of this job, a run-all answer or a lane that selects nothing | |
| # leaves `test` without a selection, and every test runs. ci-ok does not | |
| # need this job: when it breaks, PRs cost more minutes, never coverage. | |
| # Dry runs and releases never pass a selection. | |
| select-tests: | |
| needs: [changes] | |
| if: ${{ vars.TEST_IMPACT_MODE == 'gate' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml) | |
| outputs: | |
| selection: ${{ steps.select.outputs.selection }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # The PR merge commit and its base parent: the change to select for. | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - name: Build this checkout's engine | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y zlib1g-dev | |
| scripts/build.sh CC=gcc CXX=g++ | |
| - name: Select the tests | |
| id: select | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection" | |
| - name: Upload the selection | |
| if: ${{ steps.select.outputs.selection == 'narrowed' }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: test-selection | |
| path: ${{ runner.temp }}/test-selection | |
| if-no-files-found: error | |
| overwrite: true | |
| test: | |
| needs: [changes, lint, select-tests] | |
| if: ${{ !cancelled() && needs.lint.result == 'success' }} | |
| uses: ./.github/workflows/_test.yml | |
| with: | |
| lanes: ${{ needs.changes.outputs.lanes }} | |
| # Smart CI: only a successful select-tests run that narrowed passes a | |
| # selection; skipped (shadow), failed, run-all and nothing-selected | |
| # answers all run every test. | |
| test_selection: ${{ needs.select-tests.result == 'success' && needs.select-tests.outputs.selection == 'narrowed' && 'test-selection' || '' }} | |
| # Perf assertions are timing-sensitive on shared runners; they stay in | |
| # dry runs and releases where a flaky red does not block a merge. | |
| skip_perf: true | |
| # Iteration speed: split C-suite legs across parallel shard jobs | |
| # (coverage re-proven every run by the shard-completeness job). The PR | |
| # profile shards only arm64 (x2); x86 and Windows run whole, since each | |
| # shard re-pays its setup. Dry runs and releases keep x3 / x2. | |
| shard_suites: true | |
| shard_profile: pr | |
| # ── Which lanes does this PR need? The changed files go through | |
| # scripts/ci/select-lanes.sh (tier, lanes, full, reasons); docs / CI / | |
| # test-only PRs stay fast, anything it cannot place runs everything. ── | |
| changes: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| tier: ${{ steps.select.outputs.tier }} | |
| full: ${{ steps.select.outputs.full }} | |
| lanes: ${{ steps.select.outputs.lanes }} | |
| smoke: ${{ steps.select.outputs.smoke }} | |
| steps: | |
| - name: List the changed files | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| # The full .diff endpoint rejects large-but-valid PRs at 20k lines. | |
| # The paginated files endpoint remains filename-only for this gate. | |
| # A rename lists its old path too, as the fallback's --no-renames | |
| # diff does: moving a file away is a change to where it was. | |
| if FILES=$(gh api --paginate "repos/$REPO/pulls/$PR/files?per_page=100" --jq '.[] | .filename, (.previous_filename // empty)'); then | |
| echo "file list: pulls/files API" | |
| else | |
| # The files endpoint renders the diff server-side too, and answers | |
| # 422 "this diff is taking too long to generate" for a PR that | |
| # regenerates a vendored parser (#2246: a 42 MB sql/parser.c) -- | |
| # so does the compare endpoint. The PR merge ref's first parent is | |
| # the base, so a name-only diff across it is the same file list. | |
| # depth 2 + blob:none fetches commits and trees only (well under | |
| # 1 MB, under a second); nothing from the PR is checked out or run. | |
| echo "::notice::pulls/files API failed; file list taken from refs/pull/$PR/merge" | |
| CHANGES_GIT="$RUNNER_TEMP/changes.git" | |
| git init -q --bare "$CHANGES_GIT" | |
| git --git-dir="$CHANGES_GIT" fetch -q --depth=2 --filter=blob:none \ | |
| "https://github.com/$REPO" "refs/pull/$PR/merge" | |
| FILES=$(git --git-dir="$CHANGES_GIT" diff --name-only --no-renames FETCH_HEAD^1 FETCH_HEAD) | |
| fi | |
| printf '%s\n' "$FILES" > "$RUNNER_TEMP/changed-files.txt" | |
| cat "$RUNNER_TEMP/changed-files.txt" | |
| # The selector is PR code: it runs in its own step, after the | |
| # token-bearing one, from a checkout that keeps no credentials. | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # The pulls/files API lists at most 3000 files: the PR's own count lets | |
| # the selector notice a cut list and select everything instead. | |
| - name: Select lanes | |
| id: select | |
| env: | |
| CHANGED_FILES: ${{ github.event.pull_request.changed_files }} | |
| run: | | |
| scripts/ci/select-lanes.sh --format github-output --expect-files "$CHANGED_FILES" "$RUNNER_TEMP/changed-files.txt" > "$RUNNER_TEMP/selection.txt" | |
| cat "$RUNNER_TEMP/selection.txt" | |
| cat "$RUNNER_TEMP/selection.txt" >> "$GITHUB_OUTPUT" | |
| # pr-smoke's legs, each tagged with its lane (kept in the job name, | |
| # where ci-ok reads it back); the job gets the selected ones. | |
| SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-14","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]' | |
| LANES=$(sed -n 's/^lanes=//p' "$RUNNER_TEMP/selection.txt") | |
| echo "smoke={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$SMOKE")}" >> "$GITHUB_OUTPUT" | |
| # ── Light smoke: build the PRODUCTION binary and run the core smoke on the | |
| # three RELIABLE NATIVE platforms. Catches built-binary regressions at PR | |
| # time (e.g. the Windows CreateProcess argv-quoting class that previously | |
| # only surfaced in the release dry run). The full broad/emulated smoke stays | |
| # in the dry run. Only the selected legs run (ubuntu from the extraction | |
| # tier up, all three for platform/packaging changes); every leg gates. | |
| # The maintained local wrappers add a race-free release fixture so the | |
| # download/checksum/install/update phases run here as well. ── | |
| pr-smoke: | |
| needs: [changes] | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && (contains(fromJSON(needs.changes.outputs.lanes), 'smoke-ubuntu') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-mac') || contains(fromJSON(needs.changes.outputs.lanes), 'smoke-win')) }} | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJSON(needs.changes.outputs.smoke) }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install deps (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache | |
| - name: Install ccache (macOS) | |
| if: matrix.os == 'macos-14' | |
| run: command -v ccache >/dev/null 2>&1 || brew install ccache | |
| - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 | |
| if: matrix.os == 'windows-latest' | |
| with: | |
| msystem: CLANG64 | |
| path-type: inherit | |
| install: >- | |
| mingw-w64-clang-x86_64-clang | |
| mingw-w64-clang-x86_64-zlib | |
| mingw-w64-clang-x86_64-python3 | |
| mingw-w64-clang-x86_64-ccache | |
| make | |
| coreutils | |
| curl | |
| zip | |
| unzip | |
| # Verified compiler cache — content-keyed, stale hits impossible by | |
| # construction (see scripts/env.sh). | |
| - name: Compiler cache (content-verified) | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ${{ github.workspace }}/.ccache | |
| key: ccache-smoke-${{ matrix.os }}-${{ github.ref }}-${{ github.sha }} | |
| restore-keys: | | |
| ccache-smoke-${{ matrix.os }}-${{ github.ref }}- | |
| - name: Build prod + smoke (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| scripts/build.sh CC=gcc CXX=g++ | |
| scripts/smoke-local.sh "$(pwd)/build/c/codebase-memory-mcp" | |
| env: | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| CCACHE_MAXSIZE: 1000M | |
| - name: Build prod + smoke (macOS) | |
| if: matrix.os == 'macos-14' | |
| run: | | |
| scripts/build.sh CC=cc CXX=c++ | |
| codesign --sign - --force build/c/codebase-memory-mcp | |
| scripts/smoke-local.sh "$(pwd)/build/c/codebase-memory-mcp" | |
| env: | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| CCACHE_MAXSIZE: 1000M | |
| - name: Build prod + smoke (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: msys2 {0} | |
| run: | | |
| scripts/build.sh CC=clang CXX=clang++ | |
| SMOKE_ARCH=amd64 bash test-infrastructure/vm/vm-smoke.sh | |
| env: | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| CCACHE_MAXSIZE: 1000M | |
| # ── Waste sanitizer, REPORT-ONLY: memory + CPU waste on this repository and | |
| # the per-function scaling lane (Linux). Deliberately NOT in ci-ok's needs: | |
| # it uploads evidence and prints the ratchet, it never blocks a merge. Only | |
| # PRs whose selection holds the memwaste lane (memory core, memwaste | |
| # tooling, full runs) pay for it. ── | |
| memwaste: | |
| needs: [changes] | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'memwaste') }} | |
| uses: ./.github/workflows/_memwaste.yml | |
| with: | |
| mode: pr | |
| # ── Test-impact SHADOW, REPORT-ONLY: predicts from the code graph which C | |
| # suites this change can affect (the latest released binary indexes the | |
| # PR checkout; detect_changes at depth 15 against the base, mapped to | |
| # suites, with the selector's run-all triggers) and uploads the | |
| # prediction, so graph-based selection can be judged against what really | |
| # failed before anything gates on it. continue-on-error and NOT in | |
| # ci-ok's needs: it can never block or pass a merge. ── | |
| test-impact-shadow: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| actions: read # the merge base's team-artifact bundle (test-impact-artifact.yml) | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # The PR merge commit and its base parent: the diff to predict for. | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - name: Fetch the latest released binary | |
| id: release | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: scripts/ci/test-impact-shadow.sh --fetch-release "$RUNNER_TEMP/cbm-bin" | |
| - name: Build the binary from this checkout (download failed) | |
| if: ${{ steps.release.outcome == 'failure' }} | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y zlib1g-dev | |
| scripts/build.sh CC=gcc CXX=g++ | |
| mkdir -p "$RUNNER_TEMP/cbm-bin" | |
| cp build/c/codebase-memory-mcp "$RUNNER_TEMP/cbm-bin/" | |
| - name: Predict the impacted suites | |
| run: scripts/ci/test-impact-shadow.sh --binary "$RUNNER_TEMP/cbm-bin/codebase-memory-mcp" --out "$RUNNER_TEMP/test-impact" | |
| # The engine (test-impact select) is this checkout's own code: the | |
| # released binary predates it, so the checkout is built either way. | |
| - name: Build this checkout's engine | |
| if: ${{ steps.release.outcome != 'failure' }} | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y zlib1g-dev | |
| scripts/build.sh CC=gcc CXX=g++ | |
| - name: Select with the team artifact (engine shadow) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact" | |
| - name: Upload the prediction | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: test-impact-prediction | |
| path: | | |
| ${{ runner.temp }}/test-impact/prediction.json | |
| ${{ runner.temp }}/test-impact/selection.json | |
| ${{ runner.temp }}/test-impact/changed-files.txt | |
| ${{ runner.temp }}/test-impact/selection-engine.json | |
| ${{ runner.temp }}/test-impact/summary-engine.md | |
| if-no-files-found: warn | |
| # ── Docs-only and non-product PRs (tiers T0a, T0b) run the static contract | |
| # steps of the canonical test leg on their own: they police docs | |
| # surfaces (language counts, version metadata), test-infrastructure/ and | |
| # lint config too. Every test leg runs the same steps. ── | |
| contracts: | |
| needs: [changes] | |
| if: ${{ !cancelled() && needs.changes.result == 'success' && contains(fromJSON(needs.changes.outputs.lanes), 'contracts') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Contract steps only | |
| run: scripts/test.sh --contracts-only | |
| ci-ok: | |
| # The one required context (besides dco) — fails unless every PR stage | |
| # succeeded, so matrix renames can never silently deadlock merges, and | |
| # every lane the selector chose ran green: `skipped` is OK only for a lane | |
| # nobody selected (a selected lane that skipped is a silent gate loss). | |
| # memwaste and the test-impact shadow are report-only and not needed here. | |
| needs: [changes, security, lint, test, pr-smoke, contracts] | |
| if: ${{ always() }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # actions:read lists this run's jobs, the lane-level truth behind `needs`. | |
| permissions: | |
| contents: read | |
| actions: read | |
| steps: | |
| # Needs a checkout (unlike before): the gate logic lives in the canonical | |
| # scripts/ci entry, not inline YAML (venue-parity contract). | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: List this run's jobs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api --paginate "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=latest&per_page=100" \ | |
| --jq '.jobs[] | {name, status, conclusion}' > "$RUNNER_TEMP/jobs.jsonl" | |
| - name: All PR stages and every selected lane must have succeeded | |
| env: | |
| RESULTS: ${{ toJSON(needs) }} | |
| LANES: ${{ needs.changes.outputs.lanes }} | |
| JOBS: ${{ runner.temp }}/jobs.jsonl | |
| run: scripts/ci/require-all-green.sh |