diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index b7dfc6f23..72196f2f2 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -79,15 +79,10 @@ jobs: if: startsWith(matrix.os, 'macos') run: codesign --sign - --force build/c/codebase-memory-mcp - - name: Archive standard binary - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: | - cp LICENSE install.sh build/c/ - scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md - tar -czf "codebase-memory-mcp-${GOOS}-${GOARCH}.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md + # Archive layout/name live in the ONE canonical script the local + # artifact-flow smoke lane also runs (venue-parity contract). + - name: Archive standard binary (canonical package-release.sh) + run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }} - name: Attest standard binary provenance if: ${{ inputs.attest }} @@ -115,15 +110,8 @@ jobs: if: matrix.goos == 'linux' && matrix.goarch == 'amd64' run: scripts/security-ui.sh - - name: Archive UI binary - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - run: | - cp LICENSE install.sh build/c/ - scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md - tar -czf "codebase-memory-mcp-ui-${GOOS}-${GOARCH}.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md + - name: Archive UI binary (canonical package-release.sh) + run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }} --variant ui - name: Attest UI binary provenance if: ${{ inputs.attest }} @@ -167,18 +155,11 @@ jobs: scripts/build.sh CC=clang CXX=clang++ fi - - name: Archive standard binary + # Archive layout/name live in the ONE canonical script the local + # artifact-flow smoke lane also runs (venue-parity contract). + - name: Archive standard binary (canonical package-release.sh) shell: msys2 {0} - run: | - make -f Makefile.cbm build/c/codebase-memory-mcp-launcher.exe CC=clang CXX=clang++ - PAYLOAD=build/c/codebase-memory-mcp - [ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe" - cp build/c/codebase-memory-mcp-launcher.exe codebase-memory-mcp.exe - cp "$PAYLOAD" codebase-memory-mcp.payload.exe - scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md - zip codebase-memory-mcp-windows-amd64.zip \ - codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md + run: scripts/package-release.sh windows amd64 CC=clang CXX=clang++ - name: Attest standard binary provenance if: ${{ inputs.attest }} @@ -197,18 +178,9 @@ jobs: scripts/build.sh --with-ui CC=clang CXX=clang++ fi - - name: Archive UI binary + - name: Archive UI binary (canonical package-release.sh) shell: msys2 {0} - run: | - make -f Makefile.cbm build/c/codebase-memory-mcp-launcher.exe CC=clang CXX=clang++ - PAYLOAD=build/c/codebase-memory-mcp - [ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe" - cp build/c/codebase-memory-mcp-launcher.exe codebase-memory-mcp.exe - cp "$PAYLOAD" codebase-memory-mcp.payload.exe - scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md - zip codebase-memory-mcp-ui-windows-amd64.zip \ - codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md + run: scripts/package-release.sh windows amd64 --variant ui CC=clang CXX=clang++ - name: Attest UI binary provenance if: ${{ inputs.attest }} @@ -256,18 +228,9 @@ jobs: scripts/build.sh CC=clang CXX=clang++ fi - - name: Archive standard binary + - name: Archive standard binary (canonical package-release.sh) shell: msys2 {0} - run: | - make -f Makefile.cbm build/c/codebase-memory-mcp-launcher.exe CC=clang CXX=clang++ - PAYLOAD=build/c/codebase-memory-mcp - [ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe" - cp build/c/codebase-memory-mcp-launcher.exe codebase-memory-mcp.exe - cp "$PAYLOAD" codebase-memory-mcp.payload.exe - scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md - zip codebase-memory-mcp-windows-arm64.zip \ - codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md + run: scripts/package-release.sh windows arm64 CC=clang CXX=clang++ - name: Attest standard binary provenance if: ${{ inputs.attest }} @@ -286,18 +249,9 @@ jobs: scripts/build.sh --with-ui CC=clang CXX=clang++ fi - - name: Archive UI binary + - name: Archive UI binary (canonical package-release.sh) shell: msys2 {0} - run: | - make -f Makefile.cbm build/c/codebase-memory-mcp-launcher.exe CC=clang CXX=clang++ - PAYLOAD=build/c/codebase-memory-mcp - [ -f "${PAYLOAD}.exe" ] && PAYLOAD="${PAYLOAD}.exe" - cp build/c/codebase-memory-mcp-launcher.exe codebase-memory-mcp.exe - cp "$PAYLOAD" codebase-memory-mcp.payload.exe - scripts/gen-third-party-notices.sh THIRD_PARTY_NOTICES.md - zip codebase-memory-mcp-ui-windows-arm64.zip \ - codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md + run: scripts/package-release.sh windows arm64 --variant ui CC=clang CXX=clang++ - name: Attest UI binary provenance if: ${{ inputs.attest }} @@ -348,14 +302,8 @@ jobs: file build/c/codebase-memory-mcp ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked" - - name: Archive standard binary - env: - ARCH: ${{ matrix.arch }} - run: | - cp LICENSE install.sh build/c/ - scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md - tar -czf "codebase-memory-mcp-linux-${ARCH}-portable.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md + - name: Archive standard binary (canonical package-release.sh) + run: scripts/package-release.sh linux ${{ matrix.arch }}-portable - name: Attest standard binary provenance if: ${{ inputs.attest }} @@ -373,14 +321,8 @@ jobs: scripts/build.sh --with-ui CC=gcc CXX=g++ STATIC=1 fi - - name: Archive UI binary - env: - ARCH: ${{ matrix.arch }} - run: | - cp LICENSE install.sh build/c/ - scripts/gen-third-party-notices.sh build/c/THIRD_PARTY_NOTICES.md - tar -czf "codebase-memory-mcp-ui-linux-${ARCH}-portable.tar.gz" \ - -C build/c codebase-memory-mcp LICENSE install.sh THIRD_PARTY_NOTICES.md + - name: Archive UI binary (canonical package-release.sh) + run: scripts/package-release.sh linux ${{ matrix.arch }}-portable --variant ui - name: Attest UI binary provenance if: ${{ inputs.attest }} diff --git a/.github/workflows/_smoke.yml b/.github/workflows/_smoke.yml index c635ebd4a..98781f144 100644 --- a/.github/workflows/_smoke.yml +++ b/.github/workflows/_smoke.yml @@ -100,58 +100,40 @@ jobs: with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} - - name: Extract binary + - name: Extract release artifact run: | SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }} - tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz - chmod +x codebase-memory-mcp + mkdir -p "$RUNNER_TEMP/cbm-artifact" + tar -xzf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \ + -C "$RUNNER_TEMP/cbm-artifact" + chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" - - name: Start artifact server + # ONE harness for every venue. This is the same wrapper local CI and PR CI + # run, handed the DOWNLOADED artifact instead of a local build: it stages + # the release fixture, serves it on a kernel-assigned port, and runs + # smoke-test.sh under the isolated profile/TEMP the other venues use. + # It replaces an inline copy of that staging logic which had drifted from + # the wrapper (no env isolation, no user-PATH guard, fixed port). + - name: Smoke test (shared wrapper) run: | - mkdir -p /tmp/smoke-server - cp codebase-memory-mcp /tmp/smoke-server/ - OS=${{ matrix.goos }}; ARCH=${{ matrix.goarch }} - SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }} - tar -czf "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \ - -C /tmp/smoke-server codebase-memory-mcp - if [ -n "$SUFFIX" ]; then - cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \ - "/tmp/smoke-server/codebase-memory-mcp-${OS}-${ARCH}.tar.gz" - fi - # The linux binary self-updates from the fully-static "-portable" asset - # (build_update_url in src/cli/cli.c appends -portable on linux; _build.yml's - # build-linux-portable job ships it), so the smoke server must serve that name - # too -- otherwise `update` 404s and Phase 14 fails. Mirror the tarball(s) under - # the -portable name on linux only. - if [ "$OS" = "linux" ]; then - cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}.tar.gz" \ - "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz" - if [ -n "$SUFFIX" ]; then - cp "/tmp/smoke-server/codebase-memory-mcp${SUFFIX}-${OS}-${ARCH}-portable.tar.gz" \ - "/tmp/smoke-server/codebase-memory-mcp-${OS}-${ARCH}-portable.tar.gz" - fi - fi - cd /tmp/smoke-server - sha256sum *.tar.gz > checksums.txt 2>/dev/null || shasum -a 256 *.tar.gz > checksums.txt - python3 -m http.server 18080 -d /tmp/smoke-server & - - - name: Smoke test - run: scripts/smoke-test.sh ./codebase-memory-mcp + scripts/smoke-local.sh \ + "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" ${{ matrix.variant }} env: - SMOKE_DOWNLOAD_URL: http://localhost:18080 - SMOKE_UPDATE_FIXTURE_DIR: /tmp/smoke-server + CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact - name: Security audits run: | - scripts/security-strings.sh ./codebase-memory-mcp - scripts/security-install.sh ./codebase-memory-mcp - scripts/security-network.sh ./codebase-memory-mcp + BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" + scripts/security-strings.sh "$BIN" + scripts/security-install.sh "$BIN" + scripts/security-network.sh "$BIN" - name: MCP robustness test (linux-amd64 standard only) if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64' run: | - scripts/security-fuzz.sh ./codebase-memory-mcp - scripts/security-fuzz-random.sh ./codebase-memory-mcp 60 + BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" + scripts/security-fuzz.sh "$BIN" + scripts/security-fuzz-random.sh "$BIN" 60 - name: ClamAV scan (Linux) if: startsWith(matrix.os, 'ubuntu') @@ -165,7 +147,7 @@ jobs: grep -q "DatabaseMirror" /etc/clamav/freshclam.conf 2>/dev/null || \ echo "DatabaseMirror database.clamav.net" | sudo tee -a /etc/clamav/freshclam.conf > /dev/null sudo freshclam --quiet - clamscan --no-summary ./codebase-memory-mcp + clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" - name: ClamAV scan (macOS) if: startsWith(matrix.os, 'macos') @@ -178,7 +160,7 @@ jobs: echo "DatabaseMirror database.clamav.net" >> "$CLAMAV_ETC/freshclam.conf" fi freshclam --quiet --no-warnings 2>/dev/null || freshclam --quiet 2>/dev/null || echo "WARNING: freshclam update failed" - clamscan --no-summary ./codebase-memory-mcp + clamscan --no-summary "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" smoke-windows: needs: setup-matrix @@ -190,6 +172,10 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 + - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: ${{ matrix.arch == 'arm64' && 'CLANGARM64' || 'CLANG64' }} @@ -205,66 +191,53 @@ jobs: with: name: binaries-windows-${{ matrix.arch }} - - name: Extract binary + - name: Extract release artifact shell: msys2 {0} run: | SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }} ARCH=${{ matrix.arch }} - unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" - test -s codebase-memory-mcp.exe - test -s codebase-memory-mcp.payload.exe + ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact" + mkdir -p "$ARTIFACT_DIR" + unzip -o "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" -d "$ARTIFACT_DIR" + test -s "$ARTIFACT_DIR/codebase-memory-mcp.exe" + test -s "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" + # Pre-flight both images from a profile-rooted directory: the shared + # runner workspace is deliberately not a valid launcher bundle root. PROFILE_ROOT="$(cygpath -u "$USERPROFILE")" LAUNCH_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-version.XXXXXX")" trap 'rm -rf "$LAUNCH_DIR"' EXIT - cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$LAUNCH_DIR/" + cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \ + "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$LAUNCH_DIR/" "$LAUNCH_DIR/codebase-memory-mcp.payload.exe" --version "$LAUNCH_DIR/codebase-memory-mcp.exe" --version - - name: Start artifact server - shell: msys2 {0} - run: | - mkdir -p /tmp/smoke-server - cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md /tmp/smoke-server/ - SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }} - ARCH=${{ matrix.arch }} - cd /tmp/smoke-server - zip -q "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" \ - codebase-memory-mcp.exe codebase-memory-mcp.payload.exe \ - LICENSE install.ps1 THIRD_PARTY_NOTICES.md - if [ -n "$SUFFIX" ]; then - cp "codebase-memory-mcp${SUFFIX}-windows-${ARCH}.zip" "codebase-memory-mcp-windows-${ARCH}.zip" - fi - sha256sum *.zip > checksums.txt - # Pin to explicit IPv4: on windows-11-arm, `localhost` resolves to ::1 (IPv6) - # for msys2 curl while python's http.server is IPv4-only -> instant connect - # failure in smoke Phase 12a. --bind 127.0.0.1 + a 127.0.0.1 URL pin both ends. - python3 -m http.server 18080 --bind 127.0.0.1 -d /tmp/smoke-server & - - - name: Smoke test + # ONE harness for every venue: the same script win.sh smoke-install and + # pr.yml run, handed the DOWNLOADED artifact instead of a local build. It + # stages the release fixture under a disposable profile root, serves it on + # a kernel-assigned port, guards AND verifies the user-PATH registry + # mutation, and neutralizes every agent-config destination before running + # smoke-test.sh. It replaces an inline copy of that staging which had none + # of the isolation and never verified the PATH mutation it performed. + - name: Smoke test (shared wrapper) shell: msys2 {0} - run: | - PROFILE_ROOT="$(cygpath -u "$USERPROFILE")" - SMOKE_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-smoke.XXXXXX")" - trap 'rm -rf "$SMOKE_DIR"' EXIT - cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SMOKE_DIR/" - CBM_CACHE_DIR="$(cygpath -m "$SMOKE_DIR/cache")" \ - SMOKE_TEMP_ROOT="$SMOKE_DIR" \ - scripts/smoke-test.sh "$SMOKE_DIR/codebase-memory-mcp.exe" env: - SMOKE_DOWNLOAD_URL: http://127.0.0.1:18080 - SMOKE_UPDATE_FIXTURE_DIR: /tmp/smoke-server SMOKE_ARCH: ${{ matrix.arch }} + SMOKE_VARIANT: ${{ matrix.variant }} + run: | + export CBM_SMOKE_ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact" + bash test-infrastructure/vm/vm-smoke.sh - name: Security audits shell: msys2 {0} run: | - scripts/security-strings.sh ./codebase-memory-mcp.exe - scripts/security-strings.sh ./codebase-memory-mcp.payload.exe + ARTIFACT_DIR="$(cygpath -u "$RUNNER_TEMP")/cbm-artifact" + scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.exe" + scripts/security-strings.sh "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" PROFILE_ROOT="$(cygpath -u "$USERPROFILE")" SECURITY_DIR="$(mktemp -d "$PROFILE_ROOT/cbm-release-security.XXXXXX")" trap 'rm -rf "$SECURITY_DIR"' EXIT - cp codebase-memory-mcp.exe codebase-memory-mcp.payload.exe "$SECURITY_DIR/" + cp "$ARTIFACT_DIR/codebase-memory-mcp.exe" \ + "$ARTIFACT_DIR/codebase-memory-mcp.payload.exe" "$SECURITY_DIR/" TMPDIR="$SECURITY_DIR" \ scripts/security-install.sh "$SECURITY_DIR/codebase-memory-mcp.exe" @@ -272,8 +245,9 @@ jobs: shell: pwsh run: | & "C:\Program Files\Windows Defender\MpCmdRun.exe" -SignatureUpdate 2>$null + $artifactDir = Join-Path $env:RUNNER_TEMP "cbm-artifact" foreach ($binary in @("codebase-memory-mcp.exe", "codebase-memory-mcp.payload.exe")) { - $result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File "$PWD\$binary" -DisableRemediation + $result = & "C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File (Join-Path $artifactDir $binary) -DisableRemediation $code = $LASTEXITCODE Write-Host $result # MpCmdRun -Scan exit codes: 0 = clean, 2 = threat found. Any OTHER non-zero @@ -284,7 +258,9 @@ jobs: if ($code -eq 2) { Write-Host "BLOCKED: Windows Defender flagged $binary!"; exit 1 } elseif ($code -ne 0) { - Write-Host "::warning::Windows Defender scan could not run for $binary (exit $code) - skipping AV gate on this runner" + # ensure-defender gates this job earlier, so the engine MUST be + # available here: an engine failure is a red gate, never a skip. + Write-Host "BLOCKED: Defender scan engine could not run for $binary (exit $code) - Defender-ON parity violated"; exit 1 } else { Write-Host "=== Windows Defender: $binary clean ===" } @@ -304,25 +280,39 @@ jobs: with: name: binaries-linux-${{ matrix.arch }}-portable - - name: Extract and smoke test + - name: Extract release artifact run: | SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }} - tar -xzf codebase-memory-mcp${SUFFIX}-linux-${{ matrix.arch }}-portable.tar.gz - chmod +x codebase-memory-mcp - scripts/smoke-test.sh ./codebase-memory-mcp + mkdir -p "$RUNNER_TEMP/cbm-artifact" + tar -xzf codebase-memory-mcp${SUFFIX}-linux-${{ matrix.arch }}-portable.tar.gz \ + -C "$RUNNER_TEMP/cbm-artifact" + chmod +x "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" + + # Same wrapper as every other venue. This leg previously called + # smoke-test.sh with no fixture server, which silently SKIPPED the + # download/checksum/install-script phases (12-13) — the portable binary is + # what every linux install and update actually delivers, so those are + # exactly the phases it most needs to run. + - name: Smoke test (shared wrapper) + run: | + scripts/smoke-local.sh \ + "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" ${{ matrix.variant }} + env: + CBM_SMOKE_ARTIFACT_DIR: ${{ runner.temp }}/cbm-artifact # The -portable binary is what all linux install/update paths now deliver; # it MUST start on old glibc (Debian 11 / RHEL 8 / Ubuntu 20.04). Runs it # in debian:bullseye (glibc 2.31) — the standard dynamic binary would fail # here with `GLIBC_2.38 not found`. - name: Old-glibc compatibility - run: scripts/ci/check-glibc-compat.sh ./codebase-memory-mcp + run: scripts/ci/check-glibc-compat.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" - name: Security audits run: | - scripts/security-strings.sh ./codebase-memory-mcp - scripts/security-install.sh ./codebase-memory-mcp - scripts/security-network.sh ./codebase-memory-mcp + BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" + scripts/security-strings.sh "$BIN" + scripts/security-install.sh "$BIN" + scripts/security-network.sh "$BIN" # ── Packaging check (non-gating) ────────────────────────────────── # Builds pkg/glama/Dockerfile — the image Glama builds to score the diff --git a/.github/workflows/_soak.yml b/.github/workflows/_soak.yml index 7d7a1d4b5..5ce37ce44 100644 --- a/.github/workflows/_soak.yml +++ b/.github/workflows/_soak.yml @@ -5,9 +5,13 @@ on: workflow_call: inputs: duration_minutes: - description: 'Soak duration in minutes' + description: 'Soak duration in minutes (per leg)' type: number default: 10 + legs: + description: "Soak legs for the quick jobs, passed to scripts/soak-legs.sh --legs (e.g. 'quick,query-leak' or 'query-leak' for a long #581-only run)" + type: string + default: 'quick,query-leak' run_asan: description: 'Run ASan soak in addition to quick soak' type: boolean @@ -47,12 +51,13 @@ jobs: cc: cc cxx: c++ runs-on: ${{ matrix.os }} - # BUG FIX: this was hard-coded to 30, but the caller (nightly-soak.yml) - # passes duration_minutes: 240. GitHub killed the job at 30 min, so the - # "4h nightly soak" was SILENTLY TRUNCATED to 30 min and never once ran - # multi-hour. Budget must always exceed the passed duration; 300 covers - # the 240-min nightly with headroom (build + analysis + idle phases). - timeout-minutes: 300 + # Budget doctrine (this line has been silently violated TWICE): the job + # timeout must exceed the WORST-CASE run length, and duration_minutes is + # per LEG. Two 240-min legs (the nightly) need ~480 min + build + analysis; + # the previous 300 silently killed the second leg at the timeout — the same + # truncation class as the original hard-coded 30. Keep a literal (the + # inputs context is not reliably available at job-setup time): 560. + timeout-minutes: 560 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install deps (Linux) @@ -60,15 +65,11 @@ jobs: run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git - name: Build run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} - - name: Soak (${{ inputs.duration_minutes }} min) - run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} - # #581 guard: read-only soak (never reindex/mutate) so any memory growth is - # a query-path leak, not WAL/indexing. Reuses the build above. - - name: Query-leak soak (#581, read-only) - env: - CBM_SOAK_MODE: query-leak - RESULTS_DIR: soak-results-query-leak - run: scripts/soak-test.sh build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} --skip-crash-test + # The soak SEQUENCE and its guards live in the ONE canonical entry + # scripts/soak-legs.sh — never in workflow YAML. Leg selection is a + # workflow input so a long #581-only run needs no separate workflow. + - name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each) + run: scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} - name: Upload metrics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -86,12 +87,15 @@ jobs: soak-quick-windows: runs-on: windows-latest - # BUG FIX (same 30→240 mismatch as soak-quick above): the caller passes - # duration_minutes: 240, so a 30-min cap truncated the nightly soak here - # too. 300 covers the 240-min nightly with headroom. - timeout-minutes: 300 + # Budget doctrine (see soak-quick): duration_minutes is per LEG, so two + # 240-min legs need ~480 min + build; 300 silently killed the second leg. + timeout-minutes: 560 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: CLANG64 @@ -103,25 +107,28 @@ jobs: make git coreutils + # Same protected root the test legs and the VM soak use. Without it the + # soak ran under the runner's inherited LocalAppData\Temp ACLs while the + # local soak ran hardened — the two venues were soaking different shapes. + - name: Create protected per-user temp root + shell: pwsh + run: | + $root = & scripts/ci/new-protected-temp-root.ps1 ` + -Prefix 'cbm-ci-tmp-' ` + -ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c") + "CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build shell: msys2 {0} run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++ - - name: Soak (${{ inputs.duration_minutes }} min) + # Both release-gating legs via the ONE canonical soak entry; only the + # protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe. + - name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each) shell: msys2 {0} run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} - # #581 guard on Windows — the platform the bug is reported on. - - name: Query-leak soak (#581, read-only) - shell: msys2 {0} - env: - CBM_SOAK_MODE: query-leak - RESULTS_DIR: soak-results-query-leak - run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} --skip-crash-test + export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")" + export TMP="$TEMP" + export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")" + scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} - name: Upload metrics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -141,9 +148,14 @@ jobs: # Native ARM64 Windows soak (CLANGARM64, no sanitizer — ASan is unavailable # on native ARM64 Windows). Builds from source like soak-quick-windows. runs-on: windows-11-arm - timeout-minutes: 300 + # Budget doctrine (see soak-quick): per-leg duration, two legs, 560. + timeout-minutes: 560 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: CLANGARM64 @@ -155,25 +167,28 @@ jobs: make git coreutils + # Same protected root the test legs and the VM soak use. Without it the + # soak ran under the runner's inherited LocalAppData\Temp ACLs while the + # local soak ran hardened — the two venues were soaking different shapes. + - name: Create protected per-user temp root + shell: pwsh + run: | + $root = & scripts/ci/new-protected-temp-root.ps1 ` + -Prefix 'cbm-ci-tmp-' ` + -ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c") + "CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Build shell: msys2 {0} run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=clang CXX=clang++ - - name: Soak (${{ inputs.duration_minutes }} min) + # Both release-gating legs via the ONE canonical soak entry; only the + # protected-TEMP provisioning is venue text. soak-legs.sh resolves .exe. + - name: Soak legs (${{ inputs.legs }}, ${{ inputs.duration_minutes }} min each) shell: msys2 {0} run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} - # #581 guard on Windows — the platform the bug is reported on. - - name: Query-leak soak (#581, read-only) - shell: msys2 {0} - env: - CBM_SOAK_MODE: query-leak - RESULTS_DIR: soak-results-query-leak - run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" ${{ inputs.duration_minutes }} --skip-crash-test + export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")" + export TMP="$TEMP" + export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")" + scripts/soak-legs.sh --legs "${{ inputs.legs }}" build/c/codebase-memory-mcp ${{ inputs.duration_minutes }} - name: Upload metrics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -234,7 +249,10 @@ jobs: - name: ASan soak (15 min) env: ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:log_path=soak-results/asan" - run: scripts/soak-test.sh build/c/codebase-memory-mcp 15 + # Single-leg variant of the canonical soak entry: the ASan build is too + # slow for the two-leg sequence; leg selection is a documented flag, not + # a divergent inline procedure. + run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15 - name: Upload metrics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -252,6 +270,10 @@ jobs: timeout-minutes: 240 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: CLANG64 @@ -272,10 +294,8 @@ jobs: shell: msys2 {0} env: ASAN_OPTIONS: "detect_leaks=0:halt_on_error=0:log_path=soak-results/asan" - run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" 15 + # Single-leg variant of the canonical soak entry (see the unix ASan job). + run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15 - name: Upload metrics if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 70bd67bec..2cd90cfe4 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -222,8 +222,10 @@ jobs: export PATH=/usr/lib/ccache:$PATH sudo sysctl -w vm.mmap_rnd_bits=28 || true fi - export CCACHE_COMPILERCHECK=content - make -f Makefile.cbm test-tsan CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} + # The TSan leg lives in the canonical test entry (same file the + # compose test-tsan service runs); env.sh supplies the verified + # ccache masquerade. + scripts/test.sh --tsan CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} env: CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_MAXSIZE: 1500M @@ -245,6 +247,12 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # Defender parity: every Windows venue (local VM and runner alike) tests + # with real-time protection ACTIVE — enabled + verified, never skipped. + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 + - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: ${{ matrix.msystem }} @@ -259,41 +267,15 @@ jobs: git - name: Create protected per-user temp root - # The daemon suites fail closed on the MSYS-shared /tmp and on the - # runner's inherited LocalAppData\Temp ACLs. Give the whole harness a - # per-user root under the profile with an owner-stamped, protected - # current-SID DACL, matching the launcher/daemon trust policy. + # One implementation, shared with _soak.yml and the VM leg + # (test-infrastructure/vm/vm-run-tests.sh) — see the script's header for + # why the harness cannot run under a shared or ACL-inherited temp. shell: pwsh run: | - $root = Join-Path $env:USERPROFILE ("cbm-ci-tmp-" + [guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $root | Out-Null - $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User - $acl = [System.Security.AccessControl.DirectorySecurity]::new() - $acl.SetOwner($sid) - $acl.SetAccessRuleProtection($true, $false) - $rule = [System.Security.AccessControl.FileSystemAccessRule]::new( - $sid, - [System.Security.AccessControl.FileSystemRights]::FullControl, - ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor - [System.Security.AccessControl.InheritanceFlags]::ObjectInherit), - [System.Security.AccessControl.PropagationFlags]::None, - [System.Security.AccessControl.AccessControlType]::Allow) - $acl.AddAccessRule($rule) | Out-Null - Set-Acl -LiteralPath $root -AclObject $acl + $root = & scripts/ci/new-protected-temp-root.ps1 ` + -Prefix 'cbm-ci-tmp-' ` + -ProtectDir (Join-Path $env:GITHUB_WORKSPACE "build\c") "CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - # Pre-create the build directory with the same protected DACL so - # every build product inherits it. The workspace drive root grants - # Authenticated Users Modify by inheritance — a shape real user - # checkouts (under the profile) do not have — and the activation - # transaction's source-directory policy correctly refuses it, which - # would fail the install-flow tests on the environment, not the code. - $build = Join-Path $env:GITHUB_WORKSPACE "build\c" - New-Item -ItemType Directory -Path $build -Force | Out-Null - $bacl = [System.Security.AccessControl.DirectorySecurity]::new() - $bacl.SetOwner($sid) - $bacl.SetAccessRuleProtection($true, $false) - $bacl.AddAccessRule($rule) | Out-Null - Set-Acl -LiteralPath $build -AclObject $bacl - name: Compiler cache (content-verified) uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -324,7 +306,9 @@ jobs: export TEMP="$(cygpath -m "$CBM_CI_TEMP_ROOT")" export TMP="$TEMP" export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")" - scripts/test.sh CC=clang CXX=clang++ ${{ matrix.os == 'windows-11-arm' && '"SANITIZE=-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer"' || '' }} + # No per-leg flag logic here: test.sh itself applies the CLANGARM64 + # trap-UBSan default, so local and CI build identical test binaries. + scripts/test.sh CC=clang CXX=clang++ env: CBM_TEST_SHARD: ${{ matrix.shard }} CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.msystem }} @@ -369,51 +353,19 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: + # The union proof lives in the canonical scripts/ci entry, not inline + # YAML (venue-parity contract) — checkout (into repo/, so the manifest + # download below stays untouched) provides the script. + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: repo + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: shard-manifest-* path: manifests - - name: Verify cross-shard union per leg - run: | - set -eu - files=$(find manifests -name shard-manifest.txt | sort) - if [ -z "$files" ]; then - echo "FAIL: no shard manifests were uploaded" >&2 - exit 1 - fi - rc=0 - for leg in $(grep -h '^leg=' $files | sort -u | sed 's/^leg=//'); do - leg_files=$(grep -l "^leg=$leg\$" $files) - n=$(grep -h '^shard=' $leg_files | sed 's|.*/||' | sort -u) - if [ "$(printf '%s\n' "$n" | wc -l)" -ne 1 ]; then - echo "FAIL: $leg shards disagree on shard count: $n" >&2 - rc=1 - continue - fi - indices=$(grep -h '^shard=' $leg_files | sed 's/^shard=//;s|/.*||' | sort -n) - if [ "$indices" != "$(seq 1 "$n")" ]; then - echo "FAIL: $leg shard indices [$indices] != 1..$n" >&2 - rc=1 - continue - fi - list_sha=$(grep -h '^list_sha256=' $leg_files | sed 's/^list_sha256=//' | sort -u) - if [ "$(printf '%s\n' "$list_sha" | wc -l)" -ne 1 ]; then - echo "FAIL: $leg shards saw different suite lists" >&2 - rc=1 - continue - fi - union_sha=$(for f in $leg_files; do - sed -n '/^--- slice ---$/,$p' "$f" | tail -n +2 - done | sort | sha256sum | awk '{print $1}') - if [ "$union_sha" != "$list_sha" ]; then - echo "FAIL: $leg union of shard slices != full suite list (GATE-QUALITY LOSS)" >&2 - rc=1 - continue - fi - echo "OK: $leg — $n shard(s), union of slices == full suite list" - done - exit "$rc" + run: repo/scripts/ci/verify-shard-union.sh manifests # Windows product-surface regression guards. Distinct from test-windows above # (the sanitizer C suite): these drive a real product binary + embedded HTTP UI @@ -429,6 +381,10 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Ensure Windows Defender is active (venue parity) + shell: pwsh + run: scripts/ci/ensure-defender.ps1 + - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 with: msystem: CLANG64 diff --git a/.github/workflows/nightly-soak.yml b/.github/workflows/nightly-soak.yml index e3a83109b..4c8abeb28 100644 --- a/.github/workflows/nightly-soak.yml +++ b/.github/workflows/nightly-soak.yml @@ -1,4 +1,11 @@ -# Weekly soak test: 4h sustained load + ASan leak detection +# Soak dispatcher — the weekly scheduled soak AND every on-demand soak run. +# +# Absorbs the retired soak.yml (the standalone "multi-hour #581" workflow): its +# reasons to exist — a job budget that actually covers a 4h run, and the +# query-leak mode that never reindexes so the #581 leak can accumulate — are +# now _soak.yml facts (timeout 560, legs input). A long #581-only run is +# `legs: query-leak` here, not a separate workflow with its own copy of the +# soak procedure. NON-GATING: never a required check, never blocks a merge. name: Nightly Soak on: @@ -7,9 +14,22 @@ on: workflow_dispatch: inputs: duration_minutes: - description: 'Soak duration in minutes (default: 240 = 4h)' + description: 'Soak duration in minutes PER LEG (default: 240 = 4h)' type: number default: 240 + legs: + description: 'Soak legs (query-leak = the #581 detector alone)' + type: choice + options: ['quick,query-leak', 'quick', 'query-leak'] + default: 'quick,query-leak' + run_asan: + description: 'Also run the 15-min ASan soak legs' + type: boolean + default: true + # Iteration convenience carried over from soak.yml: pushing a qa/soak-** + # branch starts a real run from that branch's own copy of the workflows. + push: + branches: ['qa/soak-**'] permissions: contents: read @@ -18,5 +38,8 @@ jobs: soak: uses: ./.github/workflows/_soak.yml with: + # On schedule/push events the inputs context is empty — fall back to the + # weekly defaults (4h, both legs, ASan on). duration_minutes: ${{ inputs.duration_minutes || 240 }} - run_asan: true + legs: ${{ inputs.legs || 'quick,query-leak' }} + run_asan: ${{ github.event_name != 'workflow_dispatch' || inputs.run_asan }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3d49534b5..0fa95567b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -84,6 +84,11 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Ensure Windows Defender is active (venue parity) + if: matrix.os == 'windows-latest' + shell: pwsh + run: scripts/ci/ensure-defender.ps1 + - name: Install deps (Ubuntu) if: matrix.os == 'ubuntu-latest' run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache @@ -156,16 +161,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: All PR stages must have succeeded env: RESULTS: ${{ toJSON(needs) }} - run: | - echo "$RESULTS" | python3 -c " - import json, sys - needs = json.load(sys.stdin) - bad = {k: v['result'] for k, v in needs.items() if v['result'] not in ('success', 'skipped')} - if bad: - print('CI NOT OK:', bad) - sys.exit(1) - print('CI OK:', ', '.join(needs)) - " + run: scripts/ci/require-all-green.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4df5409c..60f705260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,39 +110,10 @@ jobs: with: subject-path: checksums.txt + # SBOM content lives in the canonical scripts/ci entry, not inline YAML + # (venue-parity contract): vendored versions are reviewable there. - name: Generate SBOM - run: | - python3 -c " - import json, glob, os - n_grammars = len([d for d in glob.glob('internal/cbm/vendored/grammars/*') if os.path.isdir(d)]) - sbom = { - 'spdxVersion': 'SPDX-2.3', - 'dataLicense': 'CC0-1.0', - 'SPDXID': 'SPDXRef-DOCUMENT', - 'name': 'codebase-memory-mcp-${{ inputs.version }}', - 'documentNamespace': 'https://github.com/DeusData/codebase-memory-mcp/releases/${{ inputs.version }}', - 'creationInfo': { - 'created': '$(date -u +%Y-%m-%dT%H:%M:%SZ)', - 'creators': ['Tool: codebase-memory-mcp-release-pipeline'] - }, - 'packages': [ - {'SPDXID': 'SPDXRef-Package-sqlite3', 'name': 'sqlite3', 'versionInfo': '3.51.3', 'licenseDeclared': 'blessing', 'downloadLocation': 'https://sqlite.org', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-yyjson', 'name': 'yyjson', 'versionInfo': '0.12.0', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/ibireme/yyjson', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-mimalloc', 'name': 'mimalloc', 'versionInfo': '3.3.2', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/microsoft/mimalloc', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-xxhash', 'name': 'xxhash', 'versionInfo': '0.8.3', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/Cyan4973/xxHash', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-tre', 'name': 'tre', 'versionInfo': '0.8.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/laurikari/tre', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-tree-sitter', 'name': 'tree-sitter', 'versionInfo': '0.24.4', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/tree-sitter/tree-sitter', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-lz4', 'name': 'lz4', 'versionInfo': '1.10.0', 'licenseDeclared': 'BSD-2-Clause', 'downloadLocation': 'https://github.com/lz4/lz4', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-zstd', 'name': 'zstd', 'versionInfo': '1.5.7', 'licenseDeclared': 'BSD-3-Clause', 'downloadLocation': 'https://github.com/facebook/zstd', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-simplecpp', 'name': 'simplecpp', 'versionInfo': '1.x', 'licenseDeclared': '0BSD', 'downloadLocation': 'https://github.com/danmar/simplecpp', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-verstable', 'name': 'verstable', 'versionInfo': '2.2.1', 'licenseDeclared': 'MIT', 'downloadLocation': 'https://github.com/JacksonAllan/Verstable', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-wyhash', 'name': 'wyhash', 'versionInfo': 'final-4.3', 'licenseDeclared': 'Unlicense', 'downloadLocation': 'https://github.com/wangyi-fudan/wyhash', 'filesAnalyzed': False}, - {'SPDXID': 'SPDXRef-Package-nomic-embed-code', 'name': 'nomic-embed-code-token-embeddings', 'versionInfo': '1.0', 'licenseDeclared': 'Apache-2.0', 'downloadLocation': 'https://huggingface.co/nomic-ai/nomic-embed-code', 'filesAnalyzed': False, 'comment': 'Derived int8 token embeddings; see vendored/nomic/NOTICE'}, - {'SPDXID': 'SPDXRef-Package-tree-sitter-grammars', 'name': 'tree-sitter-grammars-aggregate', 'versionInfo': f'{n_grammars}-grammars', 'licenseDeclared': 'MIT', 'downloadLocation': 'NOASSERTION', 'filesAnalyzed': False, 'comment': f'Aggregate of {n_grammars} vendored tree-sitter grammars, compiled statically. Predominantly MIT; non-MIT families present include CC0-1.0 (clojure, fennel), Apache-2.0 (elixir, erlang, gleam, hcl, ini, jinja2, just, pkl, sway, wit), ISC (pine, templ), Unlicense (fish); first-party grammars (c) DeusData, MIT. This summary is non-exhaustive; the authoritative complete per-grammar license list is internal/cbm/vendored/grammars/MANIFEST.md, and full license texts ship in THIRD_PARTY_NOTICES.md inside each archive.'} - ] - } - json.dump(sbom, open('sbom.json', 'w'), indent=2) - " + run: python3 scripts/ci/generate-sbom.py "${{ inputs.version }}" - name: Attest SBOM uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0 diff --git a/.github/workflows/soak.yml b/.github/workflows/soak.yml deleted file mode 100644 index 451968e0f..000000000 --- a/.github/workflows/soak.yml +++ /dev/null @@ -1,130 +0,0 @@ -# Real multi-hour soak — #581 query-only memory-leak reproducer. -# -# WHY THIS EXISTS (separate from _soak.yml / nightly-soak.yml): -# The nightly path was structurally incapable of running a real long soak: -# 1. nightly-soak.yml passes duration_minutes: 240, but _soak.yml's -# soak-quick / soak-asan jobs hard-cap `timeout-minutes: 30` (45 for -# ASan). GitHub kills the job at 30 min → the "4h" soak NEVER ran past -# 30 min. (Fixed in _soak.yml too, but this workflow guarantees the -# right budget for the long #581 run.) -# 2. scripts/soak-test.sh's default mode reindexes every 2 min; -# index_repository triggers cbm_mem_collect (mimalloc page return), -# which sweeps the query-only leak — masking #581 even on a long run. -# This workflow drives CBM_SOAK_MODE=query-leak, which never reindexes -# and never mutates files, so the leak can accumulate and be detected -# by soak-test.sh's RSS slope / ratio / ceiling analysis. -# -# NON-GATING: workflow_dispatch + push to qa/soak-** only. Never a required -# check, never blocks a merge. -# -# CRITICAL: timeout-minutes = duration + 60. A 240-min soak gets ~300 min. -name: Soak (multi-hour #581) - -on: - workflow_dispatch: - inputs: - duration_minutes: - description: 'Soak duration in minutes (default: 240 = 4h)' - type: number - default: 240 - mode: - description: 'Soak mode (query-leak = #581 detector, no reindex/mutate)' - type: choice - options: ['default', 'query-leak'] - default: 'query-leak' - # Iteration convenience: pushing a qa/soak-** branch starts a real run from - # that branch's own copy of this file (no main merge needed). Non-gating. - push: - branches: ['qa/soak-**'] - -permissions: - contents: read - -jobs: - # ── Unix: full matrix (linux amd64+arm64, darwin arm64+amd64) ────────────── - soak-unix: - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - cc: gcc - cxx: g++ - - os: ubuntu-24.04-arm - cc: gcc - cxx: g++ - - os: macos-14 - cc: cc - cxx: c++ - - os: macos-15-intel - cc: cc - cxx: c++ - runs-on: ${{ matrix.os }} - # Fixed budget (NOT the 30 min that silently truncated nightly). 320 min covers - # the 240-min default soak + build + analysis. `timeout-minutes` is evaluated at - # workflow setup where the `inputs` context is null on push events, so an - # inputs-based expression here is a startup failure — keep it a literal. - # (A workflow_dispatch run with duration > ~250 min should bump this.) - timeout-minutes: 320 - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Install deps (Linux) - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git - - - name: Build (prod binary) - run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} - - - name: Soak - env: - # On push events there are no inputs → fall back to shell defaults - # (240 min / query-leak) so a qa/soak-** push runs the real #581 soak. - CBM_SOAK_MODE: ${{ inputs.mode || 'query-leak' }} - DURATION_MINUTES: ${{ inputs.duration_minutes || '240' }} - run: scripts/soak-test.sh build/c/codebase-memory-mcp "${DURATION_MINUTES}" - - - name: Upload metrics - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: soak-${{ matrix.os }}-${{ inputs.mode || 'query-leak' }} - path: soak-results/ - retention-days: 14 - - # ── Windows: the platform #581 actually crashes on (50+ GB → crash) ─────── - soak-windows: - runs-on: windows-latest - timeout-minutes: 320 - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2 - 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 - make - git - coreutils - - name: Build (prod binary) - shell: msys2 {0} - run: scripts/build.sh CC=clang CXX=clang++ - - name: Soak - shell: msys2 {0} - env: - CBM_SOAK_MODE: ${{ inputs.mode || 'query-leak' }} - DURATION_MINUTES: ${{ inputs.duration_minutes || '240' }} - run: | - BIN=build/c/codebase-memory-mcp - [ -f "${BIN}.exe" ] && BIN="${BIN}.exe" - scripts/soak-test.sh "$BIN" "${DURATION_MINUTES}" - - name: Upload metrics - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: soak-windows-${{ inputs.mode || 'query-leak' }} - path: soak-results/ - retention-days: 14 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..9e9a5d42d --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,65 @@ +# scripts/ — the canonical leg entries + +**The doctrine (enforced, not advisory):** *a venue may provision a machine; +only a canonical leg script may exercise the product.* Local CI, PR CI, the +dry run and the release all call the SAME files below — venues differ only in +host specs, architecture and inputs, never in what a leg does. Platform +differences (arm64 sanitizer flags, the Windows launcher, the linux portable +binary) live INSIDE these scripts, once, never per-venue. +`tests/test_venue_parity_contract.sh` (run as Step 0j of every test leg) turns +any violation — inline harness logic in a workflow, a bypassed wrapper, a +missing `--help` — into a red build. + +Every entry answers `--help` (authoritative, agent-oriented: modes, env, exit +codes) and rejects unknown flags with exit 2 + `Please consult --help.` + +## The legs + +| leg | entry | what a run gives you | +|---|---|---| +| **test** | `test.sh` | DEFAULT = the venue leg: static contracts (Step 0a–0j) + CLEAN ASan+UBSan build + all suites via the parallel harness + prod-binary guards. `--suites a,b` = iteration mode (incremental rebuild, subset, seconds). `--tsan` = the ThreadSanitizer leg. CLANGARM64 gets CI's trap-UBSan flags automatically. | +| **build** | `build.sh` | CLEAN production build (+ launcher on Windows). ccache via `env.sh` makes repeats fast; `CCACHE_COMPILERCHECK=content` guarantees a hit is byte-identical to a cold compile — never stale. `--with-ui`, `--version`, `STATIC=1`, `BUILD_DIR=`. | +| **lint** | `lint.sh` | clang-tidy + cppcheck + clang-format (+ no-skips policy). `--ci` = the CI gate set (no clang-tidy). Drives the same make targets as `make lint`/`lint-ci`. | +| **smoke (unix)** | `smoke-local.sh` | Stages a full release fixture, serves it on a kernel-assigned port, runs `smoke-test.sh` (ALL phases incl. download/install/update E2E) inside a disposable HOME/XDG/TMP sandbox. `ui` variant makes a missing embedded UI a FAILURE. `CBM_SMOKE_ARTIFACT_DIR` = smoke an extracted release artifact verbatim (release mode). | +| **smoke (windows)** | `../test-infrastructure/vm/vm-smoke.sh` | Same contract for the launcher+payload pair, plus the user-PATH registry guard (prepare/verify/cleanup). | +| **smoke-invariants** | `smoke-invariants.sh` | The "shipped binary does not fail" battery (MCP handshake, all tools invocable, malformed-input resilience) — no fixture server, no install E2E. `smoke.yml` runs it on the WIDEST build-from-source matrix (incl. older-glibc legs the release artifacts cannot cover). | +| **soak** | `soak-legs.sh` | The release-gating soak SEQUENCE: `quick` then `query-leak` (the #581 detector — never reindexes, so RSS growth = query-path leak), each guarded by a completion-summary check. `--legs quick` for the ASan single-leg variant. Duration is per leg. | + +Internal harnesses — never called directly by a venue (the contract forbids +it): `smoke-test.sh` (phases; wrappers provide fixture server + sandbox), +`soak-test.sh` (one soak run; `soak-legs.sh` provides the sequence + guards), +`run-tests-parallel.sh` (reached through `test.sh`). + +## Conventions + +- **Exit codes:** 0 = pass · 2 = usage error · 90 = guard (a run died without + its completion summary — never counts as green) · anything else = the leg's + real failure. +- **Iteration is a flag, not a side-tool:** the fast paths (`--suites`, + `--legs`) are modes of the SAME entry the gates run, so a dev loop can never + drift from the venue behaviour. +- **Env sandboxing:** the smoke wrappers neutralize every agent-config + destination override; a smoke can never scribble on your real config. + +## Recommended workflows + +- **Iterating on a change:** `scripts/test.sh --suites ` (seconds, + incremental, same ASan+UBSan flags as the gate). List suites: + `build/c/test-runner --list-suites`. Debugging a Windows-on-ARM trap: + re-run with `SANITIZE=` for a plain build, or use the emulated + `win.sh ubsan-*` pair for full diagnostics. +- **Before any push (the 3-OS ladder):** `scripts/test.sh` (macOS, full) → + `./test-infrastructure/run.sh full` (Linux + TSan + smoke) → + `test-infrastructure/vm/win.sh test-par` + `guards` + `smoke-install` + (+ `soak` when the change touches memory/daemon paths). +- **Concurrency-touching change:** add `scripts/test.sh --tsan` early — the + same leg CI gates on. +- **Release-shaped verification:** `CBM_SMOKE_ARTIFACT_DIR= + scripts/smoke-local.sh [ui]` smokes exactly what would ship. +- **A leg is red in CI but green locally:** first suspect environment shape, + not code — the preflights (`win.sh` automatic; `scripts/ci/preflight-docker.sh`) + and `test-infrastructure/README.md`'s residuals list cover the knowable + differences. + +See `scripts/ci/README.md` for the CI plumbing and +`test-infrastructure/README.md` for the venue map. diff --git a/scripts/build.sh b/scripts/build.sh index 1b9003b28..575477a94 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -4,6 +4,7 @@ # Usage: # scripts/build.sh # Standard binary # scripts/build.sh --with-ui # Binary with embedded UI +# scripts/build.sh --help # Full usage # scripts/build.sh --version v0.8.0 # With version stamp # scripts/build.sh --arch x86_64 # Force x86_64 build # scripts/build.sh CC=gcc-14 CXX=g++-14 # Override compiler @@ -16,6 +17,45 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" +usage() { + cat <<'EOF' +Usage: scripts/build.sh [--with-ui] [--version V] [--arch ARCH] [VAR=VAL ...] + +The canonical production-build entry: identical in local CI, PR CI, dry run +and release. Always a CLEAN build of BUILD_DIR (build/c by default) — the +content-verified compiler cache (ccache via scripts/env.sh) makes repeat +builds fast without ever reusing a stale object: every object is re-derived +from current sources; a cache hit is byte-identical to a cold compile by +construction (CCACHE_COMPILERCHECK=content). + +Options: + --with-ui Embed the web UI (builds the frontend first; needs node). + --version V Stamp the version string (release venue passes the tag). + --arch ARCH Force target arch (arm64 | x86_64), e.g. under Rosetta. + -h, --help This text. + +Make passthrough (VAR=VAL, forwarded verbatim): + CC= CXX= Compiler override (venues pass their matrix compiler). + BUILD_DIR= Build in an isolated directory — REQUIRED when a clean + product build must not wipe build/c's test-runner (e.g. + build/smoke for the local ladder smoke). + STATIC=1 Fully static portable build (Alpine/musl leg). + EXTRA_CFLAGS= EXTRA_LDFLAGS= Sanitizer soak builds (see _soak.yml). + +Environment: + CBM_NO_CCACHE=1 Disable the compiler cache (build correctness is identical; + only speed changes). + +Callers: _build.yml (all release artifacts) · pr.yml pr-smoke · every +docker-compose build/smoke service · win.sh build (Windows VM ladder). +EOF +} +for arg in "$@"; do + case "$arg" in + -h|--help) usage; exit 0 ;; + esac +done + # Pre-parse --arch flag before sourcing env.sh for arg in "$@"; do case "$arg" in @@ -60,6 +100,12 @@ for arg in "$@"; do ;; --arch|--arch=*) ;; # already handled + -*) + # STRICT: an unknown flag never falls through into make where it + # would fail cryptically (or worse, be absorbed). + echo "build.sh: unknown option '$arg'. Please consult --help." >&2 + exit 2 + ;; BUILD_DIR=*) BUILD_DIR="${arg#BUILD_DIR=}" EXTRA_MAKE_ARGS+=("$arg") @@ -68,12 +114,17 @@ for arg in "$@"; do export "${arg}" EXTRA_MAKE_ARGS+=("$arg") ;; + *=*) + EXTRA_MAKE_ARGS+=("$arg") # VAR=VAL make passthrough + ;; *) - # Check if this is the value for --version + # Bare words are only ever the value of --version; anything else is + # a usage error, not a silent make argument. if [[ "${prev_arg:-}" == "--version" ]]; then VERSION="$arg" else - EXTRA_MAKE_ARGS+=("$arg") + echo "build.sh: unexpected argument '$arg'. Please consult --help." >&2 + exit 2 fi ;; esac diff --git a/scripts/ci/README.md b/scripts/ci/README.md new file mode 100644 index 000000000..2d482ebba --- /dev/null +++ b/scripts/ci/README.md @@ -0,0 +1,18 @@ +# scripts/ci/ — venue plumbing (single implementations) + +Support scripts that keep the venues in the SAME shape. Each exists because +the logic used to live inline in workflow YAML or was hand-duplicated between +CI and the local infrastructure — both of which the venue-parity contract +(`tests/test_venue_parity_contract.sh`) now forbids. Everything here answers +`--help` (PowerShell: comment-based help, `Get-Help