ci(codeql): PR scan per language inside PR CI, full scan on main

Bring CodeQL back to pull requests as jobs of PR CI itself (needed by
ci-ok, no polling), narrowed to first-party code, while push to main,
the weekly run, the dry run and the release keep the full scan.

- codeql.yml: one leg per language (c-cpp, javascript-typescript,
  python, actions), scope in .github/codeql/codeql-config.yml. The full
  scan traces the whole build, vendored C included. The PR scan
  (light: true) analyses each selected language's whole first-party
  scope; c-cpp compiles the third-party objects before tracing
  (scripts/build.sh --vendored-only) and then traces the first-party
  build plus the vendored files the PR changes (--reuse-vendored
  --retrace-from, make -W). Diff-informed analysis (codeql-action
  v4.38.1, forced on for PR calls) only restricts reported alerts.
  The alert check moves into each leg and reads the analysed ref.
  Push and PR runs cancel superseded runs per language; dispatched
  runs never cancel. Compiler cache off so every compile is traced.
- pr.yml: a codeql job runs the legs of the selected codeql-* lanes;
  ci-ok needs it.
- select-lanes.sh: codeql-cpp/-js/-py/-actions lanes, selected by a
  change in their scope (any C/C++ incl. vendored; JS/TS, Python and
  workflows inside the config's paths minus paths-ignore) and by every
  full run (undeterminable diff, workflows, build, CodeQL machinery).
  --codeql-scope LANGUAGE prints a list's in-scope paths.
- codeql-retrace-vendored.sh: the vendored C files a PR changes (empty,
  never narrowing, when the diff cannot be determined).
- Makefile.cbm: cbm-third-party builds the vendored prod objects only.
- tests: the gate contract pins the new layout; the selector contract
  pins that a C change always selects the C scan, an undeterminable
  diff selects every language, and a vendored-only change retraces
  exactly the changed vendored files. Selector fixtures, ci-ok's job
  map, the replay's codeql-action failures (lane codeql-cpp) and the
  interface probes follow.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-20 23:09:09 +02:00
1 parent 5afdfcaac9
commit 453b21e680
18 files changed
+694 -158

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# CodeQL scope: the repository's first-party source.
#
# Read by .github/workflows/codeql.yml (every scan, full and PR) and by
# scripts/ci/select-lanes.sh (which changed paths select which language's PR
# scan). Keep the lists plain: one path prefix per line, no globs.
#
# paths / paths-ignore narrow what the INTERPRETED languages extract
# (javascript-typescript, python, actions). For c-cpp they have no effect:
# CodeQL extracts exactly what the traced build compiles. A PR's c-cpp scan
# keeps vendored C out by compiling it before tracing starts (except vendored
# files the PR changes); the full scan on main, the dry run and the release
# traces everything, vendored C included.
name: cbm first-party source
paths:
- src
- internal
- graph-ui
- pkg
- scripts
- tests
- .github
paths-ignore:
- internal/cbm/vendored
- graph-ui/vendor
- graph-ui/fixtures
- graph-ui/verification
- tests/fixtures
+105 -52
View File
@@ -1,24 +1,43 @@
name: CodeQL SAST
# CodeQL runs on main, on a schedule and inside the dry run and the release --
# never per pull request. A per-PR analysis (40-120 min each) plus the PR-CI
# job that waited up to 150 min for it held runners that the PR legs needed:
# with ~30 PRs pushed at once, PR CI starved for hours.
# One analysis per language: c-cpp, javascript-typescript (graph-ui, pkg/npm),
# python, actions. Scope: .github/codeql/codeql-config.yml (first-party source).
#
# push to main every merge is analysed; the `alerts` job fails the run
# while any code-scanning alert is open (alert policy).
# schedule weekly, so a CodeQL query update that finds something in
# unchanged code is noticed without a push.
# workflow_call dry-run.yml and release.yml analyse the exact commit they
# test; their `codeql` job (analyze + alerts) must pass.
# workflow_dispatch a manual re-run on any ref.
# FULL scan -- push to main, weekly, dry run, release: every language; c-cpp
# traces the whole production build, vendored C included.
# PR scan -- pr.yml's `codeql` job (light: true), needed by ci-ok. One leg per
# language whose scope the PR changes (codeql-* lanes of
# scripts/ci/select-lanes.sh; an undeterminable diff or a change to the
# workflows, the build or the CodeQL machinery selects every language).
# Each leg analyses its language's WHOLE first-party scope, so cross-file
# data flow is kept; c-cpp compiles the vendored objects before tracing
# starts, except vendored files the PR changes. Diff-informed analysis then
# restricts the REPORTED alerts to the changed lines; what is extracted and
# analysed is never narrowed by the diff.
# There is no pull_request trigger and no job anywhere that polls for a CodeQL
# run: the former PR gate waited up to 150 min per PR for a full analysis and,
# with ~30 PRs pushed at once, starved PR CI for hours.
on:
push:
branches: [main]
schedule:
- cron: "23 5 * * 1"
workflow_dispatch:
inputs:
light:
description: "PR-style c-cpp build (vendored objects compiled untraced) -- to time it"
type: boolean
default: false
workflow_call:
inputs:
light:
description: "PR scan: vendored C compiled untraced except changed files; diff-informed results"
type: boolean
default: false
matrix:
description: 'The legs to run, {"include":[...]} (pr.yml: the selected codeql-* lanes); empty = every language'
type: string
default: ""
permissions:
contents: read
@@ -26,17 +45,25 @@ permissions:
jobs:
analyze:
runs-on: ubuntu-latest
# A newer push to main cancels the analysis of an older main commit; only
# the newest commit's results matter for main. Every other trigger gets a
# group of its own and is never cancelled, so a dry run and a release
# dispatched together cannot cancel each other's gate. In a called workflow
# `github` is the CALLER's context: github.workflow names the dry run or
# the release, github.run_id is the caller's run.
strategy:
fail-fast: false
# The same legs (and keys) everywhere, so a PR's analysis and main's
# share one code-scanning configuration per language.
matrix: ${{ fromJSON(inputs.matrix || '{"include":[{"language":"c-cpp","build-mode":"manual","lane":"codeql-cpp"},{"language":"javascript-typescript","build-mode":"none","lane":"codeql-js"},{"language":"python","build-mode":"none","lane":"codeql-py"},{"language":"actions","build-mode":"none","lane":"codeql-actions"}]}') }}
# Superseded runs are always cancelled: a newer push to main cancels the
# analysis of an older main commit (batched merges -> only the newest main
# commit is scanned), a newer push to a PR cancels that PR's scan. Every
# other trigger gets a group of its own and is never cancelled: a dry run
# or a release tests ONE commit, and two dispatched together must not
# cancel each other's gate. Per language, so legs never cancel each other.
# In a called workflow `github` is the CALLER's context: event_name is
# pull_request for pr.yml, github.workflow names the dry run or release.
concurrency:
group: codeql-${{ github.event_name == 'push' && github.ref || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'push' }}
# CodeQL needs security-events: write to upload results; scoped to this job so
# the workflow's top-level token stays read-only (Scorecard TokenPermissions).
group: codeql-${{ matrix.language }}-${{ github.event_name == 'push' && github.ref || github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
# CodeQL needs security-events: write to upload results (and read for the
# alert check); scoped to this job so the workflow's top-level token stays
# read-only (Scorecard TokenPermissions).
permissions:
security-events: write
actions: read
@@ -44,54 +71,80 @@ jobs:
env:
# One code-scanning configuration for every trigger. The action derives
# the key from the RUN's workflow file, which for a called workflow is
# the caller: dry-run.yml / release.yml would each open a configuration
# of their own on main, and an alert fixed by a later push would stay
# open in the stale one until the next dry run. The value is the key the
# push-to-main analysis has always used (the action documents this
# variable as settable for non-standard setups).
# the caller: pr.yml / dry-run.yml / release.yml would each open a
# configuration of their own, and on main an alert fixed by a later push
# would stay open in a stale one. The value is the key the push-to-main
# analysis has always used (the action documents this variable as
# settable for non-standard setups).
CODEQL_ACTION_ANALYSIS_KEY: .github/workflows/codeql.yml:analyze
# Diff-informed analysis: codeql-action v4.38.1 (CLI 2.27.0) reports a
# pull request's alerts only on its changed lines when the
# diff_informed_queries feature is on (default on; this variable forces
# it, src/feature-flags.ts). It applies only when the event payload is a
# pull request -- pr.yml's call -- and is inert on main, dry run, release.
CODEQL_ACTION_DIFF_INFORMED_QUERIES: ${{ inputs.light && 'true' || '' }}
# No compiler cache: a cache hit skips the compiler, and CodeQL extracts
# only what a traced compiler invocation actually compiles.
CBM_NO_CCACHE: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The PR scan reads the merge commit's first parent (the base) to
# find the vendored files the PR changes.
fetch-depth: ${{ inputs.light && 2 || 1 }}
- name: Install build dependencies
if: ${{ matrix.language == 'c-cpp' }}
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
# PR c-cpp: compile the third-party objects (tree-sitter grammars,
# sqlite, mimalloc, lz4, zstd, the tree-sitter runtime: ~1.3 GB of
# vendored C against ~45 MB first-party) BEFORE tracing starts, so the
# traced build below compiles first-party code only ...
- name: Build third-party objects (PR scan, untraced)
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
run: scripts/build.sh --vendored-only
# ... except the vendored files this PR changes: those are rebuilt
# under tracing (an undeterminable diff leaves the list empty; the scan
# still covers all first-party C).
- name: Vendored files this PR changes (traced too)
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
run: scripts/ci/codeql-retrace-vendored.sh "$RUNNER_TEMP/codeql-retrace.txt"
- name: Initialize CodeQL
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: c-cpp
build-mode: manual
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yml
- name: Build for CodeQL analysis
- name: Build for CodeQL analysis (full)
if: ${{ matrix.language == 'c-cpp' && !inputs.light }}
run: scripts/build.sh
- name: Build for CodeQL analysis (PR scan, first-party + changed vendored)
if: ${{ matrix.language == 'c-cpp' && inputs.light }}
run: scripts/build.sh --reuse-vendored --retrace-from "$RUNNER_TEMP/codeql-retrace.txt"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
category: "/language:c-cpp"
category: "/language:${{ matrix.language }}"
# Alert policy: the default branch carries ZERO open code-scanning alerts.
# Any open alert -- a new finding from a merge, from the weekly run, or one
# this commit adds -- fails this job: the main commit turns red, GitHub
# notifies whoever triggered the run, and the dry run and the release (which
# call this workflow) cannot pass until the alert is fixed or dismissed with
# a reason in the Security tab. With a zero baseline, "any open alert" is
# exactly "a new alert". Moved here unchanged from _security.yml's former
# PR-CI codeql-gate job; it reads the default branch's alerts, as it did there.
alerts:
needs: [analyze]
runs-on: ubuntu-latest
timeout-minutes: 10
# Without security-events:read this job inherits the workflow's
# contents:read, the code-scanning alert API answers 403, and the former
# gate reported "0 alerts" on every run it ever made.
permissions:
contents: read
security-events: read
steps:
# Alert policy: the default branch carries ZERO open code-scanning
# alerts. Any open alert on the analysed ref fails this leg: on main the
# commit turns red and GitHub notifies whoever triggered the run; the
# dry run and the release cannot pass; a PR (refs/pull/<n>/merge: what
# its changed lines introduce) fails ci-ok. With a zero baseline "any
# open alert" is exactly "a new alert"; a dismissal (with a reason, in
# the Security tab) holds on every ref. Every leg checks after its own
# upload, so each language's alerts are seen by at least its own leg.
# Moved from _security.yml's former PR-CI codeql-gate job.
- name: Check for open code scanning alerts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ALERT_REF: ${{ github.ref }}
run: |
# FAIL CLOSED. The previous form was `gh api ... 2>/dev/null || echo "0"`,
# which looks defensive and is the opposite. gh writes the API error
@@ -108,7 +161,7 @@ jobs:
# replaces. The callers below check the status explicitly.
count_open_alerts() {
local body status
body=$(gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
body=$(gh api "repos/${{ github.repository }}/code-scanning/alerts?state=open&ref=$ALERT_REF" \
--jq 'length' 2>/dev/null)
status=$?
if [ $status -ne 0 ]; then
@@ -132,9 +185,9 @@ jobs:
ALERTS2=$(count_open_alerts) || exit 1
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
if [ "$ALERTS" -gt 0 ]; then
echo "::error title=Open code scanning alerts::$ALERTS open alert(s) on the default branch -- fix, or dismiss with a reason in the Security tab"
echo "::error title=Open code scanning alerts::$ALERTS open alert(s) on $ALERT_REF -- fix, or dismiss with a reason in the Security tab"
echo "BLOCKED: $ALERTS open alert(s)"
gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
gh api "repos/${{ github.repository }}/code-scanning/alerts?state=open&ref=$ALERT_REF" \
--jq '.[] | " #\(.number) [\(.rule.security_severity_level // .rule.severity)] \(.rule.id) — \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line)"' 2>/dev/null || true
exit 1
fi
+6 -6
View File
@@ -3,7 +3,7 @@
#
# Pipeline: lint → test → build + candidate scan/select → smoke/soak
# Security: security-static + license-gate (independent island)
# CodeQL: codeql.yml on this commit (analyze + alerts) (independent island)
# CodeQL: codeql.yml full scan of this commit + alert check (independent island)
#
# Security and CodeQL do NOT block lint/test/build/smoke/soak. All jobs must pass
# for the overall workflow to be green.
@@ -69,11 +69,11 @@ jobs:
contents: read
secrets: inherit
# ── CodeQL: analysis of this exact commit + the open-alert check ──
# PRs do not run CodeQL; the dry run and the release are where a CodeQL pass
# is required. codeql.yml's analyze job uploads results and its alerts job
# reads them: a called workflow cannot request more than its caller grants,
# so withholding either permission fails the whole run at startup.
# ── CodeQL: full scan of this exact commit + the open-alert check ──
# Every language, vendored C included (PRs run the narrower PR scan).
# codeql.yml uploads results and reads the alerts: a called workflow
# cannot request more than its caller grants, so withholding either
# permission fails the whole run at startup.
codeql:
uses: ./.github/workflows/codeql.yml
permissions:
+27 -1
View File
@@ -125,6 +125,7 @@ jobs:
full: ${{ steps.select.outputs.full }}
lanes: ${{ steps.select.outputs.lanes }}
smoke: ${{ steps.select.outputs.smoke }}
codeql: ${{ steps.select.outputs.codeql }}
steps:
- name: List the changed files
env:
@@ -177,6 +178,10 @@ jobs:
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"
# The CodeQL legs, tagged the same way (codeql.yml's full matrix has
# the same entries, so PR and main share a configuration).
CODEQL='[{"language":"c-cpp","build-mode":"manual","lane":"codeql-cpp"},{"language":"javascript-typescript","build-mode":"none","lane":"codeql-js"},{"language":"python","build-mode":"none","lane":"codeql-py"},{"language":"actions","build-mode":"none","lane":"codeql-actions"}]'
echo "codeql={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$CODEQL")}" >> "$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
@@ -358,13 +363,34 @@ jobs:
- name: Contract steps only
run: scripts/test.sh --contracts-only
# ── CodeQL PR scan (lanes codeql-cpp / -js / -py / -actions): one leg per
# language whose first-party scope this PR changes; each analyses that
# language's whole first-party scope, c-cpp with the vendored objects
# compiled before tracing (except vendored files the PR changes), and
# reports alerts on the changed lines (diff-informed). A job of this run,
# needed by ci-ok -- nothing waits for a separate CodeQL run. Push to
# main, dry run and release run the full scan (codeql.yml). ──
codeql:
needs: [changes]
if: ${{ !cancelled() && needs.changes.result == 'success' && (contains(fromJSON(needs.changes.outputs.lanes), 'codeql-cpp') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-js') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-py') || contains(fromJSON(needs.changes.outputs.lanes), 'codeql-actions')) }}
uses: ./.github/workflows/codeql.yml
# codeql.yml uploads results and reads alerts: a called workflow cannot
# request more than its caller grants (startup failure otherwise).
permissions:
contents: read
security-events: write
actions: read
with:
light: true
matrix: ${{ needs.changes.outputs.codeql }}
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]
needs: [changes, security, lint, test, pr-smoke, contracts, codeql]
if: ${{ always() }}
runs-on: ubuntu-latest
timeout-minutes: 5
+5 -6
View File
@@ -1,6 +1,6 @@
# Release pipeline: lint → test → build → smoke/soak → draft → verify → publish
#
# Security (security-static + license-gate) and CodeQL (codeql.yml: analysis
# Security (security-static + license-gate) and CodeQL (codeql.yml: full scan
# of this commit + the open-alert check) start immediately and run in
# parallel with lint/test/build/smoke/soak. They only block the final verify
# step — everything else proceeds independently.
@@ -52,11 +52,10 @@ jobs:
secrets: inherit
# ── CodeQL (starts immediately, blocks only verify) ──────────────
# The analysis of this exact commit + the open-alert check.
# PRs do not run CodeQL; the dry run and the release are where a CodeQL pass
# is required. codeql.yml's analyze job uploads results and its alerts job
# reads them: a called workflow cannot request more than its caller grants,
# so withholding either permission fails the whole run at startup.
# Full scan of this exact commit (every language, vendored C included) +
# the open-alert check. codeql.yml uploads results and reads the alerts:
# a called workflow cannot request more than its caller grants, so
# withholding either permission fails the whole run at startup.
codeql:
uses: ./.github/workflows/codeql.yml
permissions:
+1 -1
View File
@@ -52,7 +52,7 @@ Operational authority is stricter than code review authority.
| Operation | Current authority | Project rule |
| --- | --- | --- |
| PR validation (`pr.yml`, DCO) | Automatic | Anyone may trigger it by opening or updating a PR. Required checks must pass. |
| PR validation (`pr.yml`, DCO, PR CodeQL scan) | Automatic | Anyone may trigger it by opening or updating a PR. Required checks must pass. |
| Dry run (`dry-run.yml`) | `@DeusData` | Delegated release operators may run dry runs after promotion. Dry-run delegation does not imply release authority. |
| Smoke/soak/repro manual runs | `@DeusData` | Area reviewers may operate these runs when delegated for diagnosis. Results are advisory. |
| Release workflow (`release.yml`) | `@DeusData` only | Owner-only until a release operator is explicitly promoted. Publishing, replacing releases, and tag movement remain owner-gated. |
+10
View File
@@ -1299,6 +1299,16 @@ $(BUILD_DIR)/prod_zstd.o: $(CBM_DIR)/vendored/zstd/zstd.c $(BUILD_DIR)/.build-co
OBJS_VENDORED_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LSP_OBJ_PROD) $(PP_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
# The part of OBJS_VENDORED_PROD that holds no first-party code: everything but
# prod_lsp_all.o (first-party LSP unity) and prod_preprocessor.o (first-party
# code around vendored simplecpp). CodeQL's light PR scan builds these BEFORE
# it starts tracing (scripts/build.sh --vendored-only), so the traced build
# (--reuse-vendored) compiles first-party code only: the grammars alone are
# most of the 1.3 GB of vendored C, against ~45 MB of first-party code.
OBJS_THIRD_PARTY_PROD = $(MIMALLOC_OBJ_PROD) $(SQLITE3_OBJ_PROD) $(TRE_OBJ_PROD) $(GRAMMAR_OBJS_PROD) $(TS_RUNTIME_OBJ_PROD) $(LZ4_OBJ_PROD) $(ZSTD_OBJ_PROD) $(UNIXCODER_OBJ)
.PHONY: cbm-third-party
cbm-third-party: $(OBJS_THIRD_PARTY_PROD)
MAIN_SRC = src/main.c
# Rebuild when the build CONFIGURATION changes, not only when sources do. The
+1 -1
View File
@@ -111,7 +111,7 @@ This project implements multiple layers of security verification. Every release
- **All dangerous function calls** require a reviewed entry in `scripts/security-allowlist.txt`
- **Time-bomb pattern detection** — scans for `time()`/`sleep()` near dangerous calls (could indicate delayed activation)
- **MCP tool handler file read audit** — tracks file read count in `mcp.c` against an expected maximum (detects added file reads that could exfiltrate data through tool responses)
- **CodeQL SAST** — static application security testing on every push to main, weekly, and in every dry run and release (taint analysis, CWE detection, data flow tracking). Any open alert fails the main run and blocks the release.
- **CodeQL SAST** — static application security testing of the C/C++, JavaScript/TypeScript, Python and GitHub Actions code (taint analysis, CWE detection, data flow tracking): a full scan on every push to main, weekly, and in every dry run and release; a first-party scan of the languages a pull request changes, with alerts reported on its changed lines. Any open alert fails the run and blocks the release.
- **Fuzz testing** — random/mutated inputs to MCP server and Cypher parser (60 seconds per build). Catches crashes, segfaults, and memory errors that structured tests miss.
- **Native antivirus scanning** on every platform (any detection fails the build):
- **Windows**: Windows Defender with ML heuristics — the same engine end users run
+55 -5
View File
@@ -32,6 +32,15 @@ Options:
--with-ui Build the web UI as a content-addressed sidecar (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.
--vendored-only Build only the third-party objects (make cbm-third-party)
into a clean BUILD_DIR, no binary.
--reuse-vendored Do NOT clean BUILD_DIR: build the binary on top of a
--vendored-only run with the same arguments, compiling
first-party code only. The pair exists for CodeQL's light
PR scan (codeql.yml): the first runs before tracing starts.
--retrace-from F With --reuse-vendored: F lists vendored paths (one per
line) whose objects must be rebuilt anyway (make -W), so a
vendored file the PR changes is compiled under tracing too.
-h, --help This text.
Make passthrough (VAR=VAL, forwarded verbatim):
@@ -79,6 +88,8 @@ source "$ROOT/scripts/path-safety.sh"
# containerized legs can build in their own directory instead of deleting and
# clobbering the host's native build/c artifacts.
WITH_UI=false
PHASE=all
RETRACE_FROM=""
VERSION=""
BUILD_DIR="build/c"
EXTRA_MAKE_ARGS=()
@@ -94,7 +105,13 @@ for arg in "$@"; do
--with-ui)
WITH_UI=true
;;
--version)
--vendored-only)
PHASE=vendored
;;
--reuse-vendored)
PHASE=reuse
;;
--version|--retrace-from)
prev_arg="$arg"
continue
;;
@@ -122,6 +139,8 @@ for arg in "$@"; do
# a usage error, not a silent make argument.
if [[ "${prev_arg:-}" == "--version" ]]; then
VERSION="$arg"
elif [[ "${prev_arg:-}" == "--retrace-from" ]]; then
RETRACE_FROM="$arg"
else
echo "build.sh: unexpected argument '$arg'. Please consult --help." >&2
exit 2
@@ -144,15 +163,46 @@ echo " ui=$WITH_UI version=${VERSION:-dev}"
# Verify compiler supports target arch
verify_compiler "$CC"
# Step 1: Clean C build artifacts only (not node_modules — npm ci handles that)
cbm_remove_build_dir "$ROOT" "$BUILD_DIR"
if [[ "$PHASE" != all ]] && $WITH_UI; then
echo "build.sh: --vendored-only / --reuse-vendored do not combine with --with-ui. Please consult --help." >&2
exit 2
fi
# --retrace-from: each listed path becomes `make -W <path>` (treated as changed
# now -- make's own dependency graph picks the objects; no timestamp race).
RETRACE_ARGS=()
if [[ -n "$RETRACE_FROM" ]]; then
if [[ "$PHASE" != reuse || ! -f "$RETRACE_FROM" ]]; then
echo "build.sh: --retrace-from needs --reuse-vendored and an existing list file. Please consult --help." >&2
exit 2
fi
while IFS= read -r retrace_path || [[ -n "$retrace_path" ]]; do
[[ -n "$retrace_path" ]] && RETRACE_ARGS+=(-W "$retrace_path")
done < "$RETRACE_FROM"
fi
# Step 1: Clean C build artifacts only (not node_modules — npm ci handles that).
# --reuse-vendored keeps the third-party objects of the --vendored-only run.
if [[ "$PHASE" == reuse ]]; then
if [[ ! -d "$BUILD_DIR" ]]; then
echo "build.sh: --reuse-vendored needs a --vendored-only run in $BUILD_DIR first." >&2
exit 2
fi
else
cbm_remove_build_dir "$ROOT" "$BUILD_DIR"
fi
# Step 2: Build (Makefile applies $ARCHFLAGS for the target arch on macOS)
if $WITH_UI; then
if [[ "$PHASE" == vendored ]]; then
make -j"$NPROC" -f Makefile.cbm cbm-third-party \
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
echo "=== Third-party objects built in ${BUILD_DIR} (no binary) ==="
exit 0
elif $WITH_UI; then
make -j"$NPROC" -f Makefile.cbm cbm-with-ui \
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
else
make -j"$NPROC" -f Makefile.cbm cbm \
make -j"$NPROC" -f Makefile.cbm cbm "${RETRACE_ARGS[@]+"${RETRACE_ARGS[@]}"}" \
CFLAGS_EXTRA="$CFLAGS_EXTRA" "${EXTRA_MAKE_ARGS[@]+"${EXTRA_MAKE_ARGS[@]}"}"
fi
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# codeql-retrace-vendored.sh — the vendored C/C++ files a pull request changes,
# for the PR c-cpp CodeQL scan (.github/workflows/codeql.yml).
#
# A PR's c-cpp scan compiles the third-party objects BEFORE CodeQL starts
# tracing (scripts/build.sh --vendored-only), so vendored C stays out of the
# analysis, while ALL first-party C is traced -- except the vendored files the
# PR itself changes. This lists those; scripts/build.sh --reuse-vendored
# --retrace-from <list> then rebuilds their objects under tracing (make -W, so
# make's own dependency graph picks the objects).
#
# The changed files come from the PR merge commit (HEAD^1 is the base it was
# merged onto, as in pr.yml's file-list fallback; needs fetch-depth 2) unless
# --changed FILE gives them. An undeterminable diff (no pull request, no
# parent, git failing, an unreadable FILE) is NOT an error and never narrows
# the scan: the list is empty and the scan still covers all first-party C.
#
# Usage: scripts/ci/codeql-retrace-vendored.sh [--changed FILE] OUT
# OUT is written with the vendored paths to retrace, one per line (may be
# empty). The scope test is scripts/ci/select-lanes.sh --codeql-scope c-cpp.
# Exit: 0 = list written · 2 = usage error.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
CHANGED_FILE=""
OUT=""
while [ $# -gt 0 ]; do
case "$1" in
-h | --help)
sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
--changed)
CHANGED_FILE="${2:-}"
[ -n "$CHANGED_FILE" ] || {
echo "codeql-retrace-vendored.sh: --changed needs a file. Please consult --help." >&2
exit 2
}
shift
;;
-*)
echo "codeql-retrace-vendored.sh: unknown option '$1'. Please consult --help." >&2
exit 2
;;
*)
if [ -n "$OUT" ]; then
echo "codeql-retrace-vendored.sh: more than one output file. Please consult --help." >&2
exit 2
fi
OUT="$1"
;;
esac
shift
done
if [ -z "$OUT" ]; then
echo "codeql-retrace-vendored.sh: no output file. Please consult --help." >&2
exit 2
fi
changed=""
determined=false
if [ -n "$CHANGED_FILE" ]; then
if [ -r "$CHANGED_FILE" ]; then
changed=$(cat "$CHANGED_FILE")
determined=true
fi
elif [ "${GITHUB_EVENT_NAME:-}" = pull_request ]; then
if changed=$(git -C "$ROOT" diff --name-only --no-renames HEAD^1 HEAD 2>/dev/null); then
determined=true
fi
fi
: >"$OUT"
if ! $determined; then
echo "diff undetermined: no vendored file to retrace; the scan covers all first-party C"
exit 0
fi
printf '%s\n' "$changed" |
bash "$ROOT/scripts/ci/select-lanes.sh" --codeql-scope c-cpp |
while IFS= read -r path; do
case "$path" in
internal/cbm/vendored/* | vendored/*)
if [ -f "$ROOT/$path" ]; then printf '%s\n' "$path"; fi
;;
esac
done >"$OUT"
count=$(wc -l <"$OUT" | tr -d ' ')
echo "vendored files to retrace: $count"
cat "$OUT"
+2 -1
View File
@@ -71,7 +71,8 @@ if 'LANES' in os.environ:
'test / test-lsan-macos': 'lsan-macos', 'test / test-windows-guards': 'windows-guards',
'test / shard-completeness': 'shard-completeness', 'contracts': 'contracts',
}
matrix = ('test / test-unix (', 'test / test-windows (', 'test / test-tsan (', 'pr-smoke (')
matrix = ('test / test-unix (', 'test / test-windows (', 'test / test-tsan (', 'pr-smoke (',
'codeql / analyze (')
def lane_of(name):
if name in single:
return single[name]
+109 -5
View File
@@ -30,13 +30,19 @@
# replay's single process).
# scripts/ci/select-lanes.sh --list-lanes
# Every lane name, one per line (the universe ci-ok checks against).
# scripts/ci/select-lanes.sh --codeql-scope LANGUAGE [FILE]
# The paths of FILE (default stdin) in that CodeQL language's PR
# scope (c-cpp, javascript-typescript, python, actions), one per line.
# The codeql-* lanes are selected by these scopes; the interpreted
# ones follow .github/codeql/codeql-config.yml (paths/paths-ignore).
# Exit: 0 = decided · 2 = usage error.
set -euo pipefail
usage() { sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//'; }
usage() { sed -n '2,38p' "$0" | sed 's/^# \{0,1\}//'; }
FORMAT=json
MODE="select"
CODEQL_LANGUAGE=""
INPUT=""
EXPECT=""
while [ $# -gt 0 ]; do
@@ -56,6 +62,11 @@ while [ $# -gt 0 ]; do
[ $# -gt 1 ] && shift
;;
--list-lanes) MODE="list" ;;
--codeql-scope)
MODE="codeql-scope"
CODEQL_LANGUAGE="${2:-}"
[ $# -gt 1 ] && shift
;;
--expect-files)
# An empty value (a workflow whose count went missing) must fail
# loudly, never quietly skip the truncation check.
@@ -101,20 +112,29 @@ fi
# The heredoc below is the program, so stdin input is staged in a file first.
STAGED=""
if [ "$MODE" = select ] && [ -z "$INPUT" ]; then
case "$MODE:$CODEQL_LANGUAGE" in
codeql-scope:c-cpp | codeql-scope:javascript-typescript | codeql-scope:python | codeql-scope:actions | select:* | batch:* | list:*) ;;
*)
echo "select-lanes.sh: --codeql-scope takes c-cpp, javascript-typescript, python or actions. Please consult --help." >&2
exit 2
;;
esac
if { [ "$MODE" = select ] || [ "$MODE" = codeql-scope ]; } && [ -z "$INPUT" ]; then
STAGED=$(mktemp "${TMPDIR:-/tmp}/cbm-select-lanes.XXXXXX")
trap 'rm -f "$STAGED"' EXIT
cat >"$STAGED"
INPUT="$STAGED"
fi
python3 - "$MODE" "$FORMAT" "$INPUT" "$EXPECT" <<'PY'
CODEQL_CONFIG="$(cd "$(dirname "$0")/../.." && pwd)/.github/codeql/codeql-config.yml"
python3 - "$MODE" "$FORMAT" "$INPUT" "$EXPECT" "$CODEQL_CONFIG" "$CODEQL_LANGUAGE" <<'PY'
import json
import re
import sys
mode, fmt, source = sys.argv[1:4]
expect = int(sys.argv[4]) if sys.argv[4] else None
codeql_config, codeql_language = sys.argv[5:7]
# ── Path -> category (first matching rule wins; order is part of the rule) ──
GATE_WORKFLOWS = {"pr.yml", "_test.yml", "_lint.yml", "_security.yml",
@@ -192,6 +212,8 @@ def category(path):
return "grammar"
if path.startswith(".github/workflows/"):
return "ci-gate" if base in GATE_WORKFLOWS else "ci-other"
if path.startswith(".github/codeql/"):
return "ci-gate"
if path.startswith(".github/"):
if path.endswith(".md") or ("ISSUE_TEMPLATE" in path and path.endswith((".yml", ".yaml"))):
return "docs"
@@ -293,11 +315,16 @@ def tier_of(cats):
# ── Tier -> lanes (R1) ──
# The PR CodeQL scans, one lane per language (pr.yml's `codeql` matrix). Each
# is selected by a change in its scope (codeql_scope below) and, like every
# lane, by any full run: an undeterminable diff scans every language.
CODEQL_LANES = {"c-cpp": "codeql-cpp", "javascript-typescript": "codeql-js",
"python": "codeql-py", "actions": "codeql-actions"}
FULL = {"diag", "license-gate", "lint", "lint-mem", "lsan-macos",
"memwaste", "msan", "pkg-wrappers", "security-static", "shard-completeness",
"smoke-mac", "smoke-ubuntu", "smoke-win", "tsan-arm", "tsan-mac", "tsan-x86",
"unix-arm64", "unix-macos-intel", "unix-macos14", "unix-x86", "windows",
"windows-guards"}
"windows-guards"} | set(CODEQL_LANES.values())
UNIVERSE = FULL | {"contracts"} # contracts: the T0a/T0b job; every test leg runs them too
LINT = {"lint", "lint-mem"}
CORE_LEGS = {"unix-x86", "unix-macos14", "windows", "shard-completeness"}
@@ -310,7 +337,7 @@ R1 = {
"T1-tests": LINT | CORE_LEGS,
"T2-extract": LINT | CORE_LEGS | {"tsan-x86", "smoke-ubuntu"},
"T3-mid": LINT | CORE_LEGS | {"tsan-x86", "smoke-ubuntu"},
"T4-platform": FULL - {"msan", "diag", "memwaste"},
"T4-platform": FULL - {"msan", "diag", "memwaste"} - set(CODEQL_LANES.values()),
"T5-core": FULL,
}
# Category add-ons: (categories, lanes). pkg adds nothing to a docs-only set
@@ -328,6 +355,73 @@ HARNESS = {"scripts/test.sh", "scripts/run-tests-parallel.sh", "scripts/run-test
"tests/test_main.c", "tests/test_framework.h"}
# ── CodeQL PR scope (the codeql-* lanes) ──
# c-cpp: any C/C++ the production build can compile -- first-party AND
# vendored (a vendored-only change is scanned: codeql.yml traces the vendored
# files a PR changes). Tests are not in the analysed build.
# Interpreted languages: their extensions inside the CodeQL config's
# paths minus paths-ignore (.github/codeql/codeql-config.yml, read below).
# A change to the CodeQL machinery itself scans every language (fail closed);
# those paths are full runs anyway, this keeps that true if a tier moves.
CODEQL_C_EXT = (".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx", ".inc")
CODEQL_C_ROOTS = ("src/", "internal/", "vendored/")
CODEQL_EXT = {"javascript-typescript": (".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx",
".mts", ".cts", ".vue", ".html", ".htm"),
"python": (".py",)}
CODEQL_FAIL_CLOSED_PREFIXES = (".github/workflows/", ".github/codeql/")
CODEQL_FAIL_CLOSED = {"Makefile.cbm", "scripts/build.sh", "scripts/ci/select-lanes.sh",
"scripts/ci/codeql-retrace-vendored.sh"}
def config_lists(path):
"""paths / paths-ignore of the CodeQL config: `key:` then ` - item` lines.
None when the file is unreadable -- every interpreted scope then matches
by extension alone (fail closed)."""
try:
lines = open(path, encoding="utf-8").read().splitlines()
except OSError:
return None
lists, key = {"paths": [], "paths-ignore": []}, None
for line in lines:
if not line.strip() or line.lstrip().startswith("#"):
continue
m = re.match(r"^([A-Za-z-]+):\s*$", line)
if m:
key = m.group(1) if m.group(1) in lists else None
continue
m = re.match(r"^\s+-\s+['\"]?([^'\"#]+?)['\"]?\s*$", line)
if m and key:
lists[key].append(m.group(1).rstrip("/"))
elif not line.startswith((" ", "\t")):
key = None
return lists
CODEQL_PATHS = config_lists(codeql_config)
def under(path, prefixes):
return any(path == p or path.startswith(p + "/") for p in prefixes)
def codeql_fail_closed(path):
return path in CODEQL_FAIL_CLOSED or path.startswith(CODEQL_FAIL_CLOSED_PREFIXES)
def codeql_scope(language, path):
if codeql_fail_closed(path):
return True
if language == "c-cpp":
return path.startswith(CODEQL_C_ROOTS) and path.endswith(CODEQL_C_EXT)
if CODEQL_PATHS is not None and (not under(path, CODEQL_PATHS["paths"])
or under(path, CODEQL_PATHS["paths-ignore"])):
return False
if language == "actions":
return path.startswith(".github/workflows/") or \
path.rsplit("/", 1)[-1] in ("action.yml", "action.yaml")
return path.endswith(CODEQL_EXT[language])
def triggers(path, cat):
found = []
if path == "scripts/ci/select-lanes.sh":
@@ -380,6 +474,12 @@ def select(paths, expect=None):
lanes = lanes | extra
if lanes & TEST_LEGS:
lanes = lanes | {"shard-completeness"}
for language, lane in sorted(CODEQL_LANES.items()):
hits = [p for p in paths if codeql_scope(language, p)]
if hits and lane not in lanes:
more = f" (+{len(hits) - 1} more)" if len(hits) > 1 else ""
reasons.append(f"codeql {language}: {hits[0]}{more}")
lanes = lanes | {lane}
for trigger, hits in by_trigger.items():
more = f" (+{len(hits) - 1} more)" if len(hits) > 1 else ""
reasons.append(f"full: {trigger}: {hits[0]}{more}")
@@ -407,6 +507,10 @@ def read_paths(path):
if mode == "list":
print("\n".join(sorted(UNIVERSE)))
elif mode == "codeql-scope":
scoped = sorted({p for p in read_paths(source) if p and codeql_scope(codeql_language, p)})
if scoped:
print("\n".join(scoped))
elif mode == "batch":
with open(source, encoding="utf-8") as fh:
for line in fh:
@@ -2,12 +2,14 @@
# pushes, from the lane analysis ground truth: which lane caught each. The
# replay asserts the selector selects that lane for that push.
# Columns (TAB): run_id, attempt, pr, head_sha, lane, classification.
# The three codeql-action-bump failures were caught by the codeql-gate lane;
# CodeQL now runs inside PR CI as the codeql-* lanes, so their lane is codeql-cpp.
34263688699 1 2115 013e1f7ec7d4b981a1cd28bf35d5767c195f8447 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
34288397854 1 2115 92cb3b9a4bebbc79bd283e447104669ca068ea64 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
34649406520 1 2115 78a6d19f1eaaa73054639ab0f14c372c53cd1bb3 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
34649406520 2 2115 78a6d19f1eaaa73054639ab0f14c372c53cd1bb3 unix-arm64 real: cli suite hang on slow runners (arm64, then intel)
34676336284 1 2185 eb5e34cfc141454e849daf413f648e73f74c455a codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
34676429093 1 2187 cea35fcc0520a0ba95139bd0210052f09b9cfa20 codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
34676336284 1 2185 eb5e34cfc141454e849daf413f648e73f74c455a codeql-cpp real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
34676429093 1 2187 cea35fcc0520a0ba95139bd0210052f09b9cfa20 codeql-cpp real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
34696991657 1 2115 9a460b389ce3002ac164605c3627bb86aa3b9be3 unix-macos-intel real: cli suite hang on slow runners (arm64, then intel)
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 lsan-macos real: macOS-only LSan leaks (memory-core)
34805101784 1 2202 7b77fd4834afe355cc4c7326ac3a55f0502f7264 msan real?: MSan stack overflow (memory-core, branch-specific)
@@ -23,4 +25,4 @@
35483768695 1 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a smoke-mac real: cache CLI broken on macOS/Windows
35483768695 1 2236 d2dd3b347bbdcc695f911e80b1b7f7a32d790e5a smoke-win real: cache CLI broken on macOS/Windows
35493947102 1 2236 2caafc0c41bd083092afeb692824e718a8e1e007 smoke-mac real: cache CLI broken on macOS/Windows
36242675954 1 2363 363575af920b05f911fc76ca964dcfa7ef1c62bb codeql-gate real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
36242675954 1 2363 363575af920b05f911fc76ca964dcfa7ef1c62bb codeql-cpp real: codeql-action bump broke CodeQL (still caught by requiring CodeQL/analyze)
1 # The 22 real (non-flake, non-infra) job failures of the September 2026 PR
2 # pushes, from the lane analysis ground truth: which lane caught each. The
3 # replay asserts the selector selects that lane for that push.
4 # Columns (TAB): run_id, attempt, pr, head_sha, lane, classification.
5 # The three codeql-action-bump failures were caught by the codeql-gate lane;
6 # CodeQL now runs inside PR CI as the codeql-* lanes, so their lane is codeql-cpp.
7 34263688699
8 34288397854
9 34649406520
10 34649406520
11 34676336284
12 34676429093
13 34696991657
14 34805101784
15 34805101784
25 35483768695
26 35483768695
27 35493947102
28 36242675954
+20 -16
View File
@@ -10,7 +10,7 @@ npm-readme-runs-the-test-legs pkg/npm/README.md T1-tests false lint lint-mem sec
issue-template-is-docs .github/ISSUE_TEMPLATE/bug.yml T0a-docs false contracts security-static
local-infra test-infrastructure/run.sh T0b-nonproduct false contracts lint lint-mem security-static
lint-config scripts/memory-core-baseline.txt T0b-nonproduct false contracts lint lint-mem security-static
graph-ui-adds-guards graph-ui/src/App.tsx T0b-nonproduct false contracts lint lint-mem security-static smoke-ubuntu windows-guards
graph-ui-adds-guards graph-ui/src/App.tsx T0b-nonproduct false contracts lint lint-mem security-static smoke-ubuntu windows-guards codeql-js
memwaste-tools-add-memwaste scripts/memwaste.sh T0b-nonproduct false contracts lint lint-mem memwaste security-static
non-gating-script-runs-contracts-leg scripts/benchmark-index.sh T0b-nonproduct false contracts lint lint-mem security-static shard-completeness unix-x86
test-impact-tooling scripts/test-impact/replay-selector.sh T0b-nonproduct false contracts lint lint-mem security-static shard-completeness unix-x86
@@ -19,19 +19,19 @@ test-fixture tests/fixtures/cpp_include/a.h T1-tests false lint lint-mem securit
contract-test tests/test_version_metadata_contract.sh T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
fuzz-harness tests/fuzz/fuzz_cypher.c T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
docs-plus-tests README.md tests/test_store.c T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
lsp-language internal/cbm/lsp/go_lsp.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
lsp-core internal/cbm/lsp/type_registry.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
grammar-is-extract-not-full internal/cbm/vendored/grammars/go/parser.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
extraction internal/cbm/extract_calls.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
pipeline src/pipeline/pass_calls.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
mcp-plus-tests src/mcp/mcp.c tests/test_mcp.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
mid-plus-memwaste-tools src/store/store.c scripts/memwaste-report.py T3-mid false lint lint-mem memwaste security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows
daemon src/daemon/application.c T4-platform false FULL -msan -diag -memwaste
main-c-is-cli src/main.c T4-platform false FULL -msan -diag -memwaste
foundation-non-memory src/foundation/log.c T4-platform false FULL -msan -diag -memwaste
windows-guard-tests tests/windows/guard.ps1 T4-platform false FULL -msan -diag -memwaste
smoke-harness scripts/smoke-local.sh T4-platform false FULL -msan -diag -memwaste
package-wrappers pkg/npm/package.json T4-platform false FULL -msan -diag -memwaste
lsp-language internal/cbm/lsp/go_lsp.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
lsp-core internal/cbm/lsp/type_registry.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
grammar-is-extract-not-full internal/cbm/vendored/grammars/go/parser.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
extraction internal/cbm/extract_calls.c T2-extract false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
pipeline src/pipeline/pass_calls.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mcp-plus-tests src/mcp/mcp.c tests/test_mcp.c T3-mid false lint lint-mem security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp
mid-plus-memwaste-tools src/store/store.c scripts/memwaste-report.py T3-mid false lint lint-mem memwaste security-static shard-completeness smoke-ubuntu tsan-x86 unix-macos14 unix-x86 windows codeql-cpp codeql-py
daemon src/daemon/application.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
main-c-is-cli src/main.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
foundation-non-memory src/foundation/log.c T4-platform false FULL -msan -diag -memwaste -codeql-js -codeql-py -codeql-actions
windows-guard-tests tests/windows/guard.ps1 T4-platform false FULL -msan -diag -memwaste -codeql-cpp -codeql-js -codeql-py -codeql-actions
smoke-harness scripts/smoke-local.sh T4-platform false FULL -msan -diag -memwaste -codeql-cpp -codeql-js -codeql-py -codeql-actions
package-wrappers pkg/npm/package.json T4-platform false FULL -msan -diag -memwaste -codeql-cpp -codeql-js -codeql-py -codeql-actions
memory-core src/foundation/mem_core.c T5-core true FULL
gate-script scripts/ci/require-all-green.sh T5-core true FULL
the-selector-itself scripts/ci/select-lanes.sh T5-core true FULL
@@ -50,8 +50,8 @@ lint-config-file .cppcheck T0b-nonproduct false contracts lint lint-mem security
script-c-tests-read scripts/security-vendored.sh T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
engine-config-c-tests-read .codebase-memory.json T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
test-failure-report scripts/suite-failure-report.sh T1-tests false lint lint-mem security-static shard-completeness unix-macos14 unix-x86 windows
smoke-security-script scripts/security-strings.sh T4-platform false FULL -msan -diag -memwaste
shipped-notices-are-pkg scripts/gen-third-party-notices.sh T4-platform false FULL -msan -diag -memwaste
smoke-security-script scripts/security-strings.sh T4-platform false FULL -msan -diag -memwaste -codeql-cpp -codeql-js -codeql-py -codeql-actions
shipped-notices-are-pkg scripts/gen-third-party-notices.sh T4-platform false FULL -msan -diag -memwaste -codeql-cpp -codeql-js -codeql-py -codeql-actions
non-gate-workflow-forces-full .github/workflows/stale.yml T0b-nonproduct true FULL
harness-main-forces-full tests/test_main.c T1-tests true FULL
harness-framework-forces-full tests/test_framework.h T1-tests true FULL
@@ -59,3 +59,7 @@ harness-driver-forces-full scripts/test.sh T5-core true FULL
harness-scheduler-forces-full scripts/run-test-wave.py T5-core true FULL
lower-tier-plus-trigger-is-full docs/x.md src/pipeline/a.c tests/test_main.c T3-mid true FULL
empty-list-fails-safe - T0a-docs true FULL
graph-ui-python-agent graph-ui/agents/agent.py T0b-nonproduct false codeql-py contracts lint lint-mem security-static smoke-ubuntu windows-guards
vendored-js-is-not-scanned graph-ui/vendor/lib.js T0b-nonproduct false contracts lint lint-mem security-static smoke-ubuntu windows-guards
vendored-only-c-scans-c vendored/sqlite3/sqlite3.c T5-core true FULL
codeql-config-is-full .github/codeql/codeql-config.yml T5-core true FULL
Can't render this file because it contains an unexpected character in line 3 and column 58.
+2 -1
View File
@@ -156,7 +156,8 @@ require("test_selection: ${{ needs.select-tests.result == 'success' && "
MATRIX = {"unix-x86": "_test.yml", "unix-arm64": "_test.yml", "unix-macos14": "_test.yml",
"unix-macos-intel": "_test.yml", "windows": "_test.yml", "tsan-x86": "_test.yml",
"tsan-arm": "_test.yml", "tsan-mac": "_test.yml", "smoke-ubuntu": "pr.yml",
"smoke-mac": "pr.yml", "smoke-win": "pr.yml"}
"smoke-mac": "pr.yml", "smoke-win": "pr.yml", "codeql-cpp": "pr.yml",
"codeql-js": "pr.yml", "codeql-py": "pr.yml", "codeql-actions": "pr.yml"}
SINGLE = {"lint": ("_lint.yml", "lint"), "lint-mem": ("_lint.yml", "lint-mem"),
"license-gate": ("_security.yml", "license-gate"),
"pkg-wrappers": ("_test.yml", "test-package-wrappers"),
+146 -60
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# The code-scanning gate must FAIL CLOSED.
# The code-scanning gate must FAIL CLOSED -- and CodeQL must run where it gates.
#
# History. `codeql-gate` in .github/workflows/_security.yml (since moved, as
# the `alerts` job, to .github/workflows/codeql.yml) counted open alerts with:
# History. `codeql-gate` in .github/workflows/_security.yml (since moved into
# .github/workflows/codeql.yml as the analyze job's alert check) counted open
# alerts with:
#
# ALERTS=$(gh api '.../code-scanning/alerts?state=open' --jq 'length' 2>/dev/null || echo "0")
#
@@ -20,9 +21,19 @@
#
# A security gate that cannot read its input must stop the build, not wave it
# through. This test pins both halves of that: the permission that makes the
# read possible, and the fail-closed handling for when it still is not. It also
# pins where the gate runs: never in PR CI (it starved PR CI of runners), on
# push to main, and inside the dry run and the release, whose verify needs it.
# read possible, and the fail-closed handling for when it still is not.
#
# It also pins where CodeQL runs. The former PR gate polled up to 150 min per
# PR for a separate CodeQL run and starved PR CI of runners; now:
# - no pull_request trigger and no job that polls for a CodeQL run;
# - PR CI calls codeql.yml as its own job (needed by ci-ok), one leg per
# selected codeql-* lane; each leg analyses its language's whole
# first-party scope (never narrowed by the diff); c-cpp compiles the
# vendored objects before tracing, then traces the first-party build plus
# the vendored files the PR changes;
# - push to main, the dry run and the release run the full scan, and the
# release's verify needs it;
# - superseded push and PR runs are cancelled, dispatched ones never.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@@ -30,6 +41,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
python3 - "$ROOT" <<'PY'
from __future__ import annotations
import json
import pathlib
import re
import sys
@@ -44,7 +56,10 @@ def require(condition: bool, message: str) -> None:
workflow_dir = root / ".github" / "workflows"
workflow = (workflow_dir / "codeql.yml").read_text(encoding="utf-8")
def text(name: str) -> str:
return (workflow_dir / name).read_text(encoding="utf-8")
def job_body(source: str, job: str) -> str | None:
@@ -52,28 +67,36 @@ def job_body(source: str, job: str) -> str | None:
return m.group("body") if m else None
# Isolate the alerts job so a permission granted to some OTHER job cannot
# satisfy this test. (It was _security.yml's codeql-gate job until CodeQL
# left PR CI; the logic moved unchanged.)
gate = job_body(workflow, "alerts")
require(gate is not None, "codeql.yml no longer defines an alerts job")
def code_only(source: str) -> str:
return "\n".join(line for line in source.splitlines() if not line.lstrip().startswith("#"))
def steps(body: str) -> list[tuple[str, str]]:
"""(name, step text) per `- name:`/`- uses:` step of a job body."""
found = []
for m in re.finditer(r"(?ms)^ - (?P<step>.*?)(?=^ - |\Z)", body):
step = m.group("step")
name = re.search(r"(?m)^(?:name: |\s+name: )(.*)$", step)
found.append((name.group(1).strip() if name else step.splitlines()[0], step))
return found
workflow = text("codeql.yml")
gate = job_body(workflow, "analyze")
require(gate is not None, "codeql.yml no longer defines an analyze job")
body = gate or ""
require(
re.search(r"^ needs: \[analyze\]\s*$", body, re.M) is not None,
"codeql.yml alerts must need analyze -- it counts the alerts that analysis uploaded",
)
# 1. The read has to be possible at all.
require(
re.search(r"^ permissions:\s*$", body, re.M) is not None,
"alerts must declare its own permissions block -- inheriting the "
"analyze must declare its own permissions block -- inheriting the "
"workflow default leaves it without security-events access and the alert "
"API answers 403",
)
require(
re.search(r"^ security-events:\s*read\s*$", body, re.M) is not None,
"alerts must request security-events: read, or the code-scanning "
"alert query 403s and the gate cannot see any alert",
re.search(r"^ security-events:\s*(read|write)\s*$", body, re.M) is not None,
"analyze must hold security-events (write implies read), or the "
"code-scanning alert query 403s and the gate cannot see any alert",
)
# 2. The failure direction. `|| echo "0"` on the alert query is the specific
@@ -85,11 +108,11 @@ require(
)
require(
"BLOCKED: cannot read code scanning alerts" in body,
"alerts must fail with a named error when the alert API cannot be read",
"the alert check must fail with a named error when the alert API cannot be read",
)
require(
re.search(r"alert count was not a number", body) is not None,
"alerts must reject a non-numeric alert count instead of comparing it",
"the alert check must reject a non-numeric alert count instead of comparing it",
)
# 3. The count is produced in a command substitution, so a bare `exit` inside
@@ -106,60 +129,123 @@ require(
"inside a command substitution does not stop the caller on its own",
)
# 4. Where CodeQL runs. Never per pull request (a per-PR analysis plus a PR-CI
# job waiting up to 150 min for it starved PR CI of runners); on push to
# main, and callable so the dry run and the release can require it.
# 4. Triggers: never per pull request (PR CI calls the workflow instead), on
# push to main, callable by PR CI, the dry run and the release.
on = re.search(r"(?ms)^on:\n(?P<body>.*?)(?=^\S)", workflow)
triggers = set(re.findall(r"(?m)^ ([a-z_]+):", on.group("body"))) if on else set()
require("pull_request" not in triggers and "pull_request_target" not in triggers,
"codeql.yml must not run per pull request")
"codeql.yml must not trigger on pull requests (pr.yml calls it as a job)")
require({"push", "workflow_call"} <= triggers,
f"codeql.yml must run on push to main and be callable, has {sorted(triggers)}")
require(re.search(r"(?m)^ push:\n branches: \[main\]$", workflow) is not None,
"codeql.yml's push trigger must be main")
for name in ("pr.yml", "_security.yml"):
code = "\n".join(
line for line in (workflow_dir / name).read_text(encoding="utf-8").splitlines()
if not line.lstrip().startswith("#")
)
require("codeql" not in code.lower(),
f"{name} must neither run nor wait for CodeQL (PR CI)")
# 5. Every caller must GRANT what codeql.yml's jobs request. A reusable
# workflow cannot request more permission than its caller holds: asking for
# security-events from a caller that grants only contents:read does not
# downgrade, it fails the entire run with a startup_failure before a single
# job executes. The caller list is DERIVED, never hardcoded: an earlier
# revision listed callers by hand and stayed green while a third caller
# nobody had listed failed every run at startup.
# 5. Nothing waits for a separate CodeQL run, and _security.yml has no CodeQL.
for path in sorted(workflow_dir.glob("*.yml")):
require("workflows/codeql.yml/runs" not in code_only(path.read_text(encoding="utf-8")),
f"{path.name} polls for a CodeQL run -- CodeQL runs as a job of the caller")
require("codeql" not in code_only(text("_security.yml")).lower(),
"_security.yml must not run or wait for CodeQL")
# 6. Superseded runs are cancelled (push to main, a PR), dispatched never;
# legs of one run never cancel each other.
concurrency = re.search(r"(?ms)^ concurrency:\n(.*?)(?=^ \S)", body)
conc = concurrency.group(1) if concurrency else ""
require("matrix.language" in conc, "the concurrency group must be per language")
require("github.event_name == 'push' && github.ref" in conc,
"push runs must share a ref-scoped group (only the newest main commit is scanned)")
require("github.event.pull_request.number" in conc,
"PR scans must share a per-PR group")
require("cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}" in conc,
"only push and PR scans may be cancelled -- a dry run or release tests one commit")
# 7. The PR scan: analysis never narrowed by the diff; c-cpp prebuilds the
# vendored objects untraced and traces first-party + changed vendored.
names = [name for name, _ in steps(body)]
by_name = dict(steps(body))
def index(fragment: str) -> int:
hits = [i for i, n in enumerate(names) if fragment in n]
return hits[0] if hits else -1
init, analyze = index("Initialize CodeQL"), index("Perform CodeQL Analysis")
prebuild, retrace = index("Build third-party objects"), index("Vendored files this PR changes")
light_build, full_build = index("PR scan, first-party"), index("Build for CodeQL analysis (full)")
require(min(init, analyze, prebuild, retrace, light_build, full_build) >= 0,
f"codeql.yml analyze lost a step, has {names}")
require(-1 < prebuild < init and -1 < retrace < init,
"the vendored prebuild and the retrace list must run BEFORE Initialize CodeQL (untraced)")
require(init < light_build < analyze and init < full_build < analyze,
"the traced builds must run between Initialize CodeQL and the analysis")
for name in names:
if "Initialize CodeQL" in name or "Perform CodeQL Analysis" in name or "open code scanning alerts" in name:
require(not re.search(r"(?m)^\s+if:", by_name[name]),
f"step '{name}' must never be skipped -- a selected leg always analyses")
light_step = by_name.get(names[light_build], "") if light_build >= 0 else ""
require("scripts/build.sh --reuse-vendored --retrace-from" in light_step,
"the PR c-cpp build must reuse the untraced vendored objects and retrace the changed ones")
require("scripts/ci/codeql-retrace-vendored.sh" in (by_name.get(names[retrace], "") if retrace >= 0 else ""),
"the retrace list must come from scripts/ci/codeql-retrace-vendored.sh")
require("scripts/build.sh --vendored-only" in (by_name.get(names[prebuild], "") if prebuild >= 0 else ""),
"the PR c-cpp prebuild must be scripts/build.sh --vendored-only")
require(re.search(r"(?m)^\s+run: scripts/build\.sh\s*$", by_name.get(names[full_build], "") if full_build >= 0 else "") is not None,
"the full c-cpp scan must trace the whole production build (vendored C included)")
require(re.search(r'(?m)^ CBM_NO_CCACHE: "1"$', body) is not None,
"the CodeQL job must disable the compiler cache -- a cache hit compiles nothing to trace")
require("config-file: ./.github/codeql/codeql-config.yml" in body,
"every scan must use the first-party scope in .github/codeql/codeql-config.yml")
# 8. One configuration per language: the full matrix and PR CI's legs agree.
default = re.search(r"fromJSON\(inputs\.matrix \|\| '(?P<json>\{.*?\})'\)", body)
pr = text("pr.yml")
pr_legs = re.search(r"CODEQL='(?P<json>\[.*?\])'", pr)
full_legs = json.loads(default.group("json"))["include"] if default else []
require({leg.get("language") for leg in full_legs} ==
{"c-cpp", "javascript-typescript", "python", "actions"},
"the full scan must cover c-cpp, javascript-typescript, python and actions")
require(pr_legs is not None and json.loads(pr_legs.group("json")) == full_legs,
"pr.yml's CodeQL legs must equal codeql.yml's full matrix (one configuration per language)")
# 9. Every caller GRANTS what codeql.yml requests (a called workflow cannot
# request more than its caller holds: startup failure, not a downgrade).
# The caller list is DERIVED, never hardcoded: an earlier revision listed
# callers by hand and stayed green while a third caller nobody had listed
# failed every run at startup.
callers = sorted(
path.name
for path in workflow_dir.glob("*.yml")
if "uses: ./.github/workflows/codeql.yml" in path.read_text(encoding="utf-8")
)
require(
{"dry-run.yml", "release.yml"} <= set(callers),
"the dry run and the release must call codeql.yml (their CodeQL pass), callers: "
+ (", ".join(callers) or "none"),
)
require({"pr.yml", "dry-run.yml", "release.yml"} <= set(callers),
"PR CI, the dry run and the release must call codeql.yml, callers: "
+ (", ".join(callers) or "none"))
for caller_name in callers:
caller = (workflow_dir / caller_name).read_text(encoding="utf-8")
call_body = job_body(caller, "codeql") or ""
require(
"uses: ./.github/workflows/codeql.yml" in call_body,
f"{caller_name} must call codeql.yml from a job named codeql",
)
call_body = job_body(text(caller_name), "codeql") or ""
require("uses: ./.github/workflows/codeql.yml" in call_body,
f"{caller_name} must call codeql.yml from a job named codeql")
for perm, level in (("security-events", "write"), ("actions", "read"), ("contents", "read")):
require(
re.search(rf"^ {perm}:\s*{level}\s*$", call_body, re.M) is not None,
f"{caller_name} must grant {perm}: {level} to the codeql job -- a "
"called workflow cannot request more than its caller grants, and the "
"run fails at startup rather than degrading",
)
require(re.search(rf"^ {perm}:\s*{level}\s*$", call_body, re.M) is not None,
f"{caller_name} must grant {perm}: {level} to the codeql job")
light = re.search(r"(?m)^ light: true$", call_body) is not None
require(light == (caller_name == "pr.yml"),
f"{caller_name}: only PR CI runs the PR scan (light: true); "
"the dry run and the release run the full scan")
# 6. The release publishes nothing without the CodeQL pass.
release = (workflow_dir / "release.yml").read_text(encoding="utf-8")
verify = job_body(release, "verify") or ""
# 10. PR CI: the codeql job runs for any selected codeql-* lane and ci-ok
# needs it; the release publishes nothing without the CodeQL pass.
pr_codeql = job_body(pr, "codeql") or ""
cond = re.search(r"(?m)^ if: (.*)$", pr_codeql)
for lane in ("codeql-cpp", "codeql-js", "codeql-py", "codeql-actions"):
require(cond is not None and f"'{lane}'" in cond.group(1),
f"pr.yml codeql must run when lane {lane} is selected")
require("matrix: ${{ needs.changes.outputs.codeql }}" in pr_codeql,
"pr.yml codeql must run the legs the changes job selected")
ci_ok = job_body(pr, "ci-ok") or ""
require(re.search(r"(?m)^ needs: \[[^\]]*\bcodeql\b[^\]]*\]$", ci_ok) is not None,
"ci-ok must need the codeql job")
verify = job_body(text("release.yml"), "verify") or ""
require(re.search(r"(?m)^ needs: \[[^\]]*\bcodeql\b[^\]]*\]$", verify) is not None,
"release.yml verify must need the codeql job")
require("needs.codeql.result == 'success'" in verify,
+82
View File
@@ -35,6 +35,7 @@ trap 'rm -rf "$WORK"' EXIT
# explicit path): a bare "bash" argv can resolve to the WSL alias on Windows.
python3 - "$BASH" "$SELECTOR" "$CASES" "$WORK" <<'PY'
import json
import os
import pathlib
import random
import subprocess
@@ -52,7 +53,9 @@ FULL = {
"shard-completeness", "smoke-mac", "smoke-ubuntu", "smoke-win",
"tsan-arm", "tsan-mac", "tsan-x86", "unix-arm64", "unix-macos-intel",
"unix-macos14", "unix-x86", "windows", "windows-guards",
"codeql-cpp", "codeql-js", "codeql-py", "codeql-actions",
}
CODEQL = {"codeql-cpp", "codeql-js", "codeql-py", "codeql-actions"}
UNIVERSE = FULL | {"contracts"}
@@ -201,6 +204,85 @@ for bad in ("many", ""): # "" = a workflow whose count went missing
failures.append(f"--expect-files {bad!r} must be a usage error (exit 2), never "
f"a silently skipped truncation check")
# 8. The CodeQL PR scans (codeql-* lanes) fail closed:
# (a) a C/C++ change -- first-party or vendored -- always selects the C scan;
# (b) an undeterminable diff (an empty or truncated list here; a diff the
# retrace helper cannot compute below) selects every language's full
# first-party scan, and so does a change to the workflows, the build or
# the CodeQL machinery;
# (c) a vendored-only change scans the changed vendored files: the C lane
# is selected and the retrace helper lists exactly those files, which
# codeql.yml then compiles under tracing.
# Tests, docs and vendored non-C select no CodeQL lane.
def lanes_for(files, args=()):
rc, out, _ = run(list(args), "".join(f + "\n" for f in files))
return set(json.loads(out)["lanes"]) if rc == 0 else None
for c_file in ("src/pipeline/pass_calls.c", "src/foundation/mem_core.c",
"internal/cbm/extract_calls.c", "internal/cbm/lsp/go_lsp.c",
"internal/cbm/cbm.h", "src/newdir/x.c",
"internal/cbm/vendored/grammars/go/parser.c",
"internal/cbm/vendored/lz4/lz4.c", "vendored/sqlite3/sqlite3.c",
"vendored/mimalloc/src/options.c"):
got = lanes_for([c_file])
if got is None or "codeql-cpp" not in got:
failures.append(f"codeql (a): a C/C++ change ({c_file}) must select codeql-cpp, "
f"got {sorted(got or [])}")
for label, files, args in (("empty file list", [], ()),
("truncated list", ["docs/a.md"], ("--expect-files", "5")),
("a workflow", [".github/workflows/stale.yml"], ()),
("Makefile.cbm", ["Makefile.cbm"], ()),
("scripts/build.sh", ["scripts/build.sh"], ()),
("the selector", ["scripts/ci/select-lanes.sh"], ()),
("the CodeQL config", [".github/codeql/codeql-config.yml"], ()),
("the retrace helper", ["scripts/ci/codeql-retrace-vendored.sh"], ())):
got = lanes_for(files, args)
if got is None or not CODEQL <= got:
failures.append(f"codeql (b): {label} must select every CodeQL language, "
f"got {sorted((got or set()) & CODEQL)}")
for quiet in ("tests/test_store.c", "tests/fixtures/x.py", "README.md", "docs/a.md",
"graph-ui/vendor/lib.js", "scripts/repro.sh"):
got = lanes_for([quiet])
if got is None or got & CODEQL:
failures.append(f"codeql: {quiet} must select no CodeQL lane, "
f"got {sorted((got or set()) & CODEQL)}")
for language, path in (("javascript-typescript", "graph-ui/src/App.tsx"),
("javascript-typescript", "pkg/npm/install.js"),
("python", "scripts/test-impact/x.py"),
("actions", ".github/workflows/pr.yml")):
rc, out, _ = run(["--codeql-scope", language], path + "\n")
if rc != 0 or out.split() != [path]:
failures.append(f"--codeql-scope {language} must keep {path}, printed {out!r}")
rc, _, err = run(["--codeql-scope", "go"], "src/main.c\n")
if rc != 2 or "Please consult --help." not in err:
failures.append("--codeql-scope with an unknown language must be a usage error (exit 2)")
retrace = pathlib.Path(selector).parent / "codeql-retrace-vendored.sh"
env = {k: v for k, v in os.environ.items() if k != "GITHUB_EVENT_NAME"}
changed = work / "codeql-changed.txt"
changed.write_text("src/main.c\ninternal/cbm/vendored/grammars/go/parser.c\n"
"vendored/sqlite3/sqlite3.c\nvendored/no-such-file.c\n"
"graph-ui/src/App.tsx\n", encoding="utf-8")
listed = work / "codeql-retrace.txt"
proc = subprocess.run([bash, str(retrace), "--changed", str(changed), str(listed)],
capture_output=True, text=True, env=env)
got = listed.read_text(encoding="utf-8").split() if listed.exists() else None
if proc.returncode != 0 or got != ["internal/cbm/vendored/grammars/go/parser.c",
"vendored/sqlite3/sqlite3.c"]:
failures.append(f"codeql (c): the retrace helper must list exactly the changed vendored "
f"C files that exist, got {got} (exit {proc.returncode})")
for label, args in (("an unreadable change list", ["--changed", str(work / "missing.txt")]),
("no pull request", [])):
listed.write_text("stale\n", encoding="utf-8")
proc = subprocess.run([bash, str(retrace), *args, str(listed)],
capture_output=True, text=True, env=env)
if proc.returncode != 0 or listed.read_text(encoding="utf-8") != "" \
or "undetermined" not in proc.stdout:
failures.append(f"codeql (b): with {label} the retrace helper must succeed with an "
f"empty list (the scan still covers all first-party C), "
f"exit {proc.returncode}: {proc.stdout.strip()!r}")
if failures:
print("LANE SELECTOR CONTRACT VIOLATED:")
for failure in failures:
+2
View File
@@ -386,6 +386,7 @@ if [ -d "$ROOT/scripts" ] && [ -f "$ROOT/scripts/test.sh" ]; then
HELP_ENTRIES="
scripts/test.sh
scripts/build.sh
scripts/ci/codeql-retrace-vendored.sh
scripts/lint.sh
scripts/smoke-local.sh
scripts/soak-legs.sh
@@ -433,6 +434,7 @@ test-infrastructure/vm/win.sh
STRICT_ENTRIES="
scripts/test.sh
scripts/build.sh
scripts/ci/codeql-retrace-vendored.sh
scripts/lint.sh
scripts/smoke-local.sh
scripts/soak-legs.sh