mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
The v0.12.1 release smoke failed on every leg in Phase 13w, the package-wrapper install gate added in #2664: - macOS: the runner images have no Go on PATH, and SMOKE_REQUIRE_WRAPPERS=1 rightly fails without one. pr-smoke and the three release smoke jobs now set up Go from pkg/go/go.mod, with the same pinned step the wrapper test job uses. - Windows: `mkdir -m 700` changes the mode in a second step, which MSYS2 cannot do under the smoke profile's owner-only DACL. mktemp -d sets the mode at creation. - Linux: the runner's older Go downloaded the 1.26.1 toolchain into the smoke profile's HOME. Go writes its module cache read-only, so the harness could not delete its profile afterwards. All Go state now stays in the wrapper's own work tree, and its cleanup restores write permission before removing it. npm and PyPI passed on every leg that reached them; the gate itself is unchanged. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
384 lines
18 KiB
YAML
384 lines
18 KiB
YAML
# 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:
|
|
# Full history: the engine certifies the history it diffs and refuses
|
|
# a shallow clone, so a depth-2 checkout always answered `all`.
|
|
fetch-depth: 0
|
|
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
|
|
|
|
# SMOKE_REQUIRE_WRAPPERS=1 needs a Go toolchain for the Go wrapper; the
|
|
# macOS runner images have none on PATH.
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
with:
|
|
go-version-file: pkg/go/go.mod
|
|
cache: false
|
|
|
|
- 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
|
|
SMOKE_REQUIRE_WRAPPERS: "1"
|
|
|
|
- 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
|
|
SMOKE_REQUIRE_WRAPPERS: "1"
|
|
|
|
- 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
|
|
SMOKE_REQUIRE_WRAPPERS: "1"
|
|
|
|
# ── 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:
|
|
# Full history: the engine (test-impact select) certifies the history
|
|
# it diffs and refuses a shallow clone ("Graft or shallow state
|
|
# prevents history certification"), which made every shadow answer
|
|
# run_all (GIT_UNAVAILABLE). About 400 MiB, this job only.
|
|
fetch-depth: 0
|
|
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
|