Break-glass merge PR #2734

This commit is contained in:
Martin Vogel
2026-10-21 00:30:54 +02:00
committed by GitHub
23 changed files with 959 additions and 231 deletions
+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
+1 -1
View File
@@ -1,5 +1,5 @@
# Reusable: lint only (cppcheck + clang-format).
# Security-static and CodeQL gate are separate — see _security.yml.
# Security-static is separate — see _security.yml; CodeQL — see codeql.yml.
name: Lint & Security
on:
+3 -94
View File
@@ -1,4 +1,6 @@
# Reusable: security-static + CodeQL gate.
# Reusable: security-static + license gate.
# CodeQL is not here: PRs do not run it. It runs on push to main, weekly, and
# inside dry-run.yml / release.yml, which call codeql.yml directly.
# Runs independently from lint/test/build — does not block the main pipeline.
# Affects overall workflow success status.
name: Security Gate
@@ -52,96 +54,3 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: scripts/audit-license-provenance.py
codeql-gate:
if: ${{ inputs.lanes == 'all' || contains(fromJSON(inputs.lanes), 'codeql-gate') }}
runs-on: ubuntu-latest
timeout-minutes: 240
# Without security-events:read this job inherits the workflow's
# contents:read, the code-scanning alert API answers 403, and the gate
# below reported "0 alerts" on every run it has ever made. actions:read is
# what the CodeQL-run poll above needs for the same reason.
permissions:
contents: read
security-events: read
actions: read
steps:
- name: Wait for CodeQL on current commit (max 150 min)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# On pull_request events github.sha is the synthetic merge commit;
# CodeQL runs are recorded against the PR head SHA.
CURRENT_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
echo "Waiting for CodeQL to complete on $CURRENT_SHA..."
# 300 attempts x 30s = 150 min. The previous budget was 90 x 30s = 45 min,
# which is shorter than CodeQL actually takes on this repository: a measured
# run on PR #1426 (head 7b72652a) completed with conclusion=success after
# 124 min, 7 minutes AFTER this gate had already given up. That marked at
# least five contributor PRs red for a scan that passed. The job's own
# timeout-minutes is 240, so 150 still leaves headroom.
for attempt in $(seq 1 300); do
LATEST=$(gh api "repos/${{ github.repository }}/actions/workflows/codeql.yml/runs?head_sha=$CURRENT_SHA&per_page=1" \
--jq '.workflow_runs[] | "\(.conclusion) \(.status)"' 2>/dev/null | head -1 || echo "")
if [ -z "$LATEST" ]; then
echo " $attempt/300: no run yet..."; sleep 30; continue
fi
CONCLUSION=$(echo "$LATEST" | cut -d' ' -f1)
STATUS=$(echo "$LATEST" | cut -d' ' -f2)
if [ "$STATUS" = "completed" ] && [ "$CONCLUSION" = "success" ]; then
echo "=== CodeQL passed ==="; exit 0
elif [ "$STATUS" = "completed" ]; then
echo "BLOCKED: CodeQL $CONCLUSION"; exit 1
fi
echo " $attempt/300: $STATUS..."; sleep 30
done
echo "BLOCKED: CodeQL timeout"; exit 1
- name: Check for open code scanning alerts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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
# BODY to stdout, so a 403 made ALERTS the string
# {"message":"Resource not accessible by integration",...,"status":"403"}0
# every `[` comparison then failed as non-integer, the `if` took its
# false branch, and the step printed "CodeQL gate passed (0 alerts)"
# and exited 0. That is exactly how a security gate should NEVER fail:
# silently, in the direction of "everything is fine". An unreadable
# alert list is now an error, not a zero.
# NOTE: this runs in a command substitution, so it must `return`, not
# `exit` -- an `exit` here would only end the subshell and let the
# caller continue with an empty count, recreating the very bug this
# 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' \
--jq 'length' 2>/dev/null)
status=$?
if [ $status -ne 0 ]; then
echo "BLOCKED: cannot read code scanning alerts (gh exit $status)." >&2
echo " Refusing to treat an unreadable alert list as zero." >&2
echo " Most likely cause: the job lacks security-events:read." >&2
return 1
fi
case "$body" in
''|*[!0-9]*)
echo "BLOCKED: alert count was not a number: ${body:-<empty>}" >&2
return 1
;;
esac
printf '%s' "$body"
}
echo "Waiting 60s for alert API to settle..."
sleep 60
ALERTS=$(count_open_alerts) || exit 1
sleep 15
ALERTS2=$(count_open_alerts) || exit 1
[ "$ALERTS" -lt "$ALERTS2" ] && ALERTS=$ALERTS2
if [ "$ALERTS" -gt 0 ]; then
echo "BLOCKED: $ALERTS open alert(s)"
gh api 'repos/${{ github.repository }}/code-scanning/alerts?state=open' \
--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
echo "=== CodeQL gate passed (0 alerts) ==="
+163 -16
View File
@@ -1,47 +1,194 @@
name: CodeQL SAST
# One analysis per language: c-cpp, javascript-typescript (graph-ui, pkg/npm),
# python, actions. Scope: .github/codeql/codeql-config.yml (first-party source).
#
# 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]
pull_request:
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
# One analysis per pull request and one per pushed branch. A newer push to main
# cancels the analysis of an older main commit; only the newest commit's
# results matter for main, and a merge-heavy day otherwise queues one 40-100
# minute analysis per merge ahead of every pull request run.
concurrency:
group: codeql-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
analyze:
runs-on: ubuntu-latest
# CodeQL needs security-events: write to upload results; scoped to this job so
# the workflow's top-level token stays read-only (Scorecard TokenPermissions).
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-${{ 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
contents: read
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: 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 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
# BODY to stdout, so a 403 made ALERTS the string
# {"message":"Resource not accessible by integration",...,"status":"403"}0
# every `[` comparison then failed as non-integer, the `if` took its
# false branch, and the step printed "CodeQL gate passed (0 alerts)"
# and exited 0. That is exactly how a security gate should NEVER fail:
# silently, in the direction of "everything is fine". An unreadable
# alert list is now an error, not a zero.
# NOTE: this runs in a command substitution, so it must `return`, not
# `exit` -- an `exit` here would only end the subshell and let the
# caller continue with an empty count, recreating the very bug this
# 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&ref=$ALERT_REF" \
--jq 'length' 2>/dev/null)
status=$?
if [ $status -ne 0 ]; then
echo "BLOCKED: cannot read code scanning alerts (gh exit $status)." >&2
echo " Refusing to treat an unreadable alert list as zero." >&2
echo " Most likely cause: the job lacks security-events:read." >&2
return 1
fi
case "$body" in
''|*[!0-9]*)
echo "BLOCKED: alert count was not a number: ${body:-<empty>}" >&2
return 1
;;
esac
printf '%s' "$body"
}
echo "Waiting 60s for alert API to settle..."
sleep 60
ALERTS=$(count_open_alerts) || exit 1
sleep 15
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 $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&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
echo "=== CodeQL gate passed (0 alerts) ==="
+17 -7
View File
@@ -2,9 +2,10 @@
# Each step can be skipped for faster iteration.
#
# Pipeline: lint → test → build + candidate scan/select → smoke/soak
# Security: security-static + codeql-gate (independent island)
# Security: security-static + license-gate (independent island)
# CodeQL: codeql.yml full scan of this commit + alert check (independent island)
#
# Security does NOT block lint/test/build/smoke/soak. All jobs must pass
# Security and CodeQL do NOT block lint/test/build/smoke/soak. All jobs must pass
# for the overall workflow to be green.
#
# Candidate VirusTotal scanning and tuple-local selection are part of the
@@ -62,15 +63,24 @@ jobs:
# ── Security (independent island — does not block main pipeline) ──
security:
uses: ./.github/workflows/_security.yml
# The called job requests security-events:read to count code-scanning
# alerts; a reusable workflow cannot request more than its caller grants,
# so withholding this here fails the whole run at startup.
# No job in _security.yml reads code scanning or the Actions API any more
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
permissions:
contents: read
security-events: read
actions: read
secrets: inherit
# ── 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:
contents: read
security-events: write
actions: read
# ── Lint (cppcheck + clang-format) ────────────────────────────
lint:
if: ${{ inputs.skip_lint != true }}
+29 -6
View File
@@ -26,13 +26,10 @@ jobs:
security:
needs: [changes]
uses: ./.github/workflows/_security.yml
# The called job requests security-events:read to count code-scanning
# alerts; a reusable workflow cannot request more than its caller grants,
# so withholding this here fails the whole run at startup.
# No job in _security.yml reads code scanning or the Actions API any more
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
permissions:
contents: read
security-events: read
actions: read
secrets: inherit
with:
lanes: ${{ needs.changes.outputs.lanes }}
@@ -128,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:
@@ -180,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
@@ -361,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
+20 -10
View File
@@ -1,7 +1,8 @@
# Release pipeline: lint → test → build → smoke/soak → draft → verify → publish
#
# Security (security-static + codeql-gate) starts immediately and runs in
# parallel with lint/test/build/smoke/soak. It only blocks the final verify
# 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.
name: Release
@@ -44,15 +45,24 @@ jobs:
# ── Security (starts immediately, blocks only verify) ───────────
security:
uses: ./.github/workflows/_security.yml
# The called job requests security-events:read to count code-scanning
# alerts; a reusable workflow cannot request more than its caller grants,
# so withholding this here fails the whole run at startup.
# No job in _security.yml reads code scanning or the Actions API any more
# (the CodeQL gate moved to codeql.yml), so the token stays read-only.
permissions:
contents: read
security-events: read
actions: read
secrets: inherit
# ── CodeQL (starts immediately, blocks only verify) ──────────────
# 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:
contents: read
security-events: write
actions: read
# ── 0. Preflight: refuse malformed dispatch inputs ──────────────
# The tag is inputs.version VERBATIM. A bare "0.10.7" publishes a release the
# installers can never resolve (they fetch releases/download/v<version>/...),
@@ -312,12 +322,12 @@ jobs:
body: ${{ inputs.release_notes || '' }}
generate_release_notes: ${{ inputs.release_notes == '' }}
# ── 7. Verify + Publish (requires security gate) ───────────────
# ── 7. Verify + Publish (requires the security and CodeQL gates) ─
verify:
needs: [release-draft, security]
needs: [release-draft, security, codeql]
# Explicit condition: a deliberately skipped test ancestor must not skip
# the publish chain; the draft itself proves smoke and soak both succeeded.
if: ${{ !cancelled() && !failure() && needs.security.result == 'success' && needs.release-draft.result == 'success' }}
if: ${{ !cancelled() && !failure() && needs.security.result == 'success' && needs.codeql.result == 'success' && needs.release-draft.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
+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, CodeQL) | 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 (taint analysis, CWE detection, data flow tracking). Any open alert 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
+56 -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,47 @@ 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 -- the retrace helper supplies direct compilation entrypoints, so nested
# includes do not depend on Makefile wildcard coverage; 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
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# codeql-retrace-vendored.sh — prebuilt C/C++ translation units to retrace,
# 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, including grammar/runtime wrappers. This lists their
# compilation entrypoints; scripts/build.sh --reuse-vendored --retrace-from
# <list> then rebuilds their objects under tracing. Use direct prerequisites
# for make -W: its shallow wildcard dependencies miss nested headers/sources,
# and compiler include paths can contain aliases such as ../unix/prim.c.
#
# 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 entrypoints 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
entrypoints=()
case "$path" in
internal/cbm/grammar_*.c | internal/cbm/ts_runtime.c)
entrypoints=("$path")
;;
vendored/mimalloc/*) entrypoints=(vendored/mimalloc/src/static.c) ;;
vendored/sqlite3/*) entrypoints=(vendored/sqlite3/sqlite3.c) ;;
vendored/tre/*) entrypoints=(vendored/tre/tre_all.c) ;;
internal/cbm/vendored/lz4/*)
entrypoints=(internal/cbm/vendored/lz4/lz4.c internal/cbm/vendored/lz4/lz4hc.c)
;;
internal/cbm/vendored/zstd/*) entrypoints=(internal/cbm/vendored/zstd/zstd.c) ;;
internal/cbm/vendored/ts_runtime/*) entrypoints=(internal/cbm/ts_runtime.c) ;;
internal/cbm/vendored/grammars/*)
family="${path#internal/cbm/vendored/grammars/}"
wrapper="internal/cbm/grammar_${family%%/*}.c"
if [ -f "$ROOT/$wrapper" ]; then
entrypoints=("$wrapper")
else
# Shared or unfamiliar grammar code can have many consumers.
entrypoints=("$ROOT"/internal/cbm/grammar_*.c)
fi
;;
internal/cbm/vendored/common/*)
entrypoints=("$ROOT"/internal/cbm/grammar_*.c)
;;
internal/cbm/vendored/* | vendored/*)
# Other vendored code is compiled with the first-party build.
entrypoints=("$path")
;;
esac
for entrypoint in "${entrypoints[@]+"${entrypoints[@]}"}"; do
entrypoint="${entrypoint#"$ROOT"/}"
if [ -f "$ROOT/$entrypoint" ]; then printf '%s\n' "$entrypoint"; fi
done
done | LC_ALL=C sort -u >"$OUT"
count=$(wc -l <"$OUT" | tr -d ' ')
echo "prebuilt translation units to retrace: $count"
cat "$OUT"
+3 -2
View File
@@ -66,12 +66,13 @@ if 'LANES' in os.environ:
single = {
'lint / lint': 'lint', 'lint / lint-mem': 'lint-mem',
'security / security-static': 'security-static',
'security / license-gate': 'license-gate', 'security / codeql-gate': 'codeql-gate',
'security / license-gate': 'license-gate',
'test / test-diag': 'diag', 'test / test-msan': 'msan',
'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]
+112 -8
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) ──
FULL = {"codeql-gate", "diag", "license-gate", "lint", "lint-mem", "lsan-macos",
# 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"}
@@ -308,9 +335,9 @@ R1 = {
# The contract steps police test-infrastructure/ and lint config too.
"T0b-nonproduct": LINT | {"contracts"},
"T1-tests": LINT | CORE_LEGS,
"T2-extract": LINT | CORE_LEGS | {"codeql-gate", "tsan-x86", "smoke-ubuntu"},
"T3-mid": LINT | CORE_LEGS | {"codeql-gate", "tsan-x86", "smoke-ubuntu"},
"T4-platform": FULL - {"msan", "diag", "memwaste"},
"T2-extract": LINT | CORE_LEGS | {"tsan-x86", "smoke-ubuntu"},
"T3-mid": LINT | CORE_LEGS | {"tsan-x86", "smoke-ubuntu"},
"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
+2
View File
@@ -383,6 +383,8 @@ bash "$ROOT/tests/test_setup_scripts_contract.sh"
# every September 2026 PR push and real failure against it.
echo "=== Step 0z: PR lane selector contract ==="
bash "$ROOT/tests/test_select_lanes.sh"
echo "=== Step 0z1: CodeQL retraces affected prebuilt objects ==="
bash "$ROOT/tests/test_codeql_retrace.sh"
echo "=== Step 0z2: lane selector history replay ==="
bash "$ROOT/scripts/test-impact/replay-selector.sh"
echo "=== Step 0z3: lane-aware aggregate gates (ci-ok, shard union) ==="
+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 codeql-gate 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 codeql-gate 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 codeql-gate 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 codeql-gate 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 codeql-gate 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 codeql-gate 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 codeql-gate 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.
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Exercise the real production Make rules with tiny C fixtures. CodeQL starts
# tracing after cbm-third-party, so every affected object must compile again,
# while unrelated objects must remain reused. No dry-run/timestamp assertions.
set -euo pipefail
ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
python3 - "$ROOT" "$BASH" <<'PY'
import os
import pathlib
import re
import subprocess
import sys
import tempfile
root, bash = pathlib.Path(sys.argv[1]), sys.argv[2]
failures = []
with tempfile.TemporaryDirectory(prefix="cbm-codeql-retrace-") as directory:
work = pathlib.Path(directory)
def write(path, text):
dest = work / path
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_text(text, encoding="utf-8")
for path in ("Makefile.cbm", "scripts/ci/codeql-retrace-vendored.sh",
"scripts/ci/select-lanes.sh", ".github/codeql/codeql-config.yml"):
write(path, (root / path).read_text(encoding="utf-8"))
# Nested and aliased includes deliberately escape the Makefile's shallow
# wildcard dependencies. The compiler really compiles these translation units.
write("vendored/mimalloc/src/static.c",
'#include "mimalloc/internal.h"\n#include "prim/osx/prim.c"\n')
write("vendored/mimalloc/include/mimalloc/internal.h", "#define MI_VALUE 7\n")
write("vendored/mimalloc/src/prim/osx/prim.c", '#include "../unix/prim.c"\n')
write("vendored/mimalloc/src/prim/unix/prim.c", "int mi_value(void) { return MI_VALUE; }\n")
write("internal/cbm/vendored/common/scanner.h", "#define SHARED_VALUE 3\n")
for language in ("go", "json"):
write(f"internal/cbm/grammar_{language}.c",
f'#include "vendored/grammars/{language}/parser.c"\n')
write(f"internal/cbm/vendored/grammars/{language}/parser.c",
'#include "nested/value.h"\n#include "../../common/scanner.h"\n'
f'int grammar_{language}(void) {{ return VALUE + SHARED_VALUE; }}\n')
write(f"internal/cbm/vendored/grammars/{language}/nested/value.h", "#define VALUE 2\n")
write("internal/cbm/vendored/grammars/shared/shared.h", "/* future shared grammar helper */\n")
write("internal/cbm/ts_runtime.c", '#include "vendored/ts_runtime/src/lib.c"\n')
write("internal/cbm/vendored/ts_runtime/src/lib.c", "int runtime_value(void) { return 1; }\n")
for path, symbol in (("vendored/sqlite3/sqlite3.c", "sqlite_value"),
("vendored/tre/tre_all.c", "tre_value"),
("internal/cbm/vendored/lz4/lz4.c", "lz4_value"),
("internal/cbm/vendored/lz4/lz4hc.c", "lz4hc_value"),
("internal/cbm/vendored/zstd/zstd.c", "zstd_value")):
write(path, f"int {symbol}(void) {{ return 1; }}\n")
write("internal/cbm/vendored/lz4/lz4.h", "/* shared LZ4 header */\n")
write("vendored/sqlite3/sqlite3.h", "/* sqlite header */\n")
write("internal/cbm/vendored/zstd/zstd.h", "/* zstd header */\n")
write("vendored/nomic/code_vectors_blob.S", ".text\n")
write("vendored/nomic/code_vectors.bin", "")
write("src/main.c", "int main(void) { return 0; }\n")
def run(args):
proc = subprocess.run(args, cwd=work, capture_output=True, text=True)
if proc.returncode:
raise RuntimeError(f"{args}: exit {proc.returncode}\n{proc.stdout}\n{proc.stderr}")
return proc.stdout
make = ["make", "--no-print-directory", "-f", "Makefile.cbm", "cbm-third-party",
"BUILD_DIR=build/c", "CC=" + os.environ.get("CC", "cc")]
def compiled(output):
return set(re.findall(r" -c -o build/c/([^\s]+\.o)(?:\s|$)", output))
prebuilt = compiled(run(make))
expected = {"prod_mimalloc.o", "prod_sqlite3.o", "prod_grammar_go.o",
"prod_grammar_json.o", "prod_ts_runtime.o", "prod_lz4.o",
"prod_lz4hc.o", "prod_zstd.o", "unixcoder_blob.o"}
# TRE is a Windows-only production object. All other objects are mandatory.
if prebuilt not in (expected, expected | {"prod_tre.o"}):
raise RuntimeError(f"incomplete prebuild: {sorted(prebuilt)}")
if compiled(run(make)):
raise RuntimeError("unchanged prebuild is not reused")
cases = [
("nested allocator header", ["vendored/mimalloc/include/mimalloc/internal.h"], {"prod_mimalloc.o"}),
("aliased allocator source", ["vendored/mimalloc/src/prim/unix/prim.c"], {"prod_mimalloc.o"}),
("nested grammar header", ["internal/cbm/vendored/grammars/go/nested/value.h"], {"prod_grammar_go.o"}),
("grammar wrapper", ["internal/cbm/grammar_go.c"], {"prod_grammar_go.o"}),
("runtime wrapper", ["internal/cbm/ts_runtime.c"], {"prod_ts_runtime.o"}),
("runtime source", ["internal/cbm/vendored/ts_runtime/src/lib.c"], {"prod_ts_runtime.o"}),
("shared grammar header", ["internal/cbm/vendored/common/scanner.h"], {"prod_grammar_go.o", "prod_grammar_json.o"}),
("unknown grammar family", ["internal/cbm/vendored/grammars/shared/shared.h"], {"prod_grammar_go.o", "prod_grammar_json.o"}),
("LZ4 header", ["internal/cbm/vendored/lz4/lz4.h"], {"prod_lz4.o", "prod_lz4hc.o"}),
("SQLite header", ["vendored/sqlite3/sqlite3.h"], {"prod_sqlite3.o"}),
("zstd header", ["internal/cbm/vendored/zstd/zstd.h"], {"prod_zstd.o"}),
("first-party only", ["src/main.c"], set()),
("duplicate changes", ["internal/cbm/grammar_go.c", "internal/cbm/grammar_go.c"], {"prod_grammar_go.o"}),
]
for label, paths, expected_objects in cases:
write("changed.txt", "\n".join(paths) + "\n")
run([bash, "scripts/ci/codeql-retrace-vendored.sh", "--changed", "changed.txt", "retrace.txt"])
retrace = (work / "retrace.txt").read_text(encoding="utf-8").splitlines()
actual = compiled(run(make + [arg for path in retrace for arg in ("-W", path)]))
if actual != expected_objects:
failures.append(f"{label}: expected {sorted(expected_objects)}, compiled {sorted(actual)}")
else:
print(f"PASS {label}: {len(actual)}/{len(prebuilt)} third-party objects retraced")
if failures:
print("CODEQL RETRACE CONTRACT VIOLATED:\n " + "\n ".join(failures))
sys.exit(1)
print("CodeQL retrace contract OK (real compiler + production Make rules)")
PY
-1
View File
@@ -45,7 +45,6 @@ t1_jobs() { # the jobs of a green T1 run; $1 = conclusion of the x86 leg
job "changes" success
job "security / security-static" success
job "security / license-gate" skipped
job "security / codeql-gate" skipped
job "lint / lint" success
job "lint / lint-mem" success
job "test / setup-matrix" success
+2 -2
View File
@@ -156,10 +156,10 @@ 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"),
"codeql-gate": ("_security.yml", "codeql-gate"),
"pkg-wrappers": ("_test.yml", "test-package-wrappers"),
"diag": ("_test.yml", "test-diag"), "msan": ("_test.yml", "test-msan"),
"lsan-macos": ("_test.yml", "test-lsan-macos"),
+168 -57
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 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")
#
@@ -21,6 +22,18 @@
# 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 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)"
@@ -28,6 +41,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
python3 - "$ROOT" <<'PY'
from __future__ import annotations
import json
import pathlib
import re
import sys
@@ -41,25 +55,48 @@ def require(condition: bool, message: str) -> None:
failures.append(message)
workflow = (root / ".github" / "workflows" / "_security.yml").read_text(encoding="utf-8")
workflow_dir = root / ".github" / "workflows"
# Isolate the codeql-gate job so a permission granted to some OTHER job cannot
# satisfy this test.
gate = re.search(r"\n codeql-gate:\n(?P<body>(?: .*\n|\n)*)", workflow)
require(gate is not None, "_security.yml no longer defines a codeql-gate job")
body = gate.group("body") if gate else ""
def text(name: str) -> str:
return (workflow_dir / name).read_text(encoding="utf-8")
def job_body(source: str, job: str) -> str | None:
m = re.search(rf"\n {re.escape(job)}:\n(?P<body>(?: .*\n|\n)*)", source)
return m.group("body") if m else None
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 ""
# 1. The read has to be possible at all.
require(
re.search(r"^ permissions:\s*$", body, re.M) is not None,
"codeql-gate 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,
"codeql-gate 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
@@ -71,11 +108,11 @@ require(
)
require(
"BLOCKED: cannot read code scanning alerts" in body,
"codeql-gate 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,
"codeql-gate 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
@@ -92,53 +129,127 @@ require(
"inside a command substitution does not stop the caller on its own",
)
# 4. Every caller must GRANT what the job requests. A reusable workflow cannot
# request more permission than its caller holds: asking for
# security-events:read 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. Checking only the called workflow is how this test passed
# while the PR pipeline never started.
# The caller list is DERIVED, never hardcoded. An earlier revision listed
# ("pr.yml", "release.yml") by hand and stayed green while dry-run.yml -- a
# third caller nobody had listed -- failed every run at startup. A test that
# only checks the callers you remembered cannot catch the one you forgot.
workflow_dir = root / ".github" / "workflows"
# 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 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")
# 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/_security.yml" in path.read_text(encoding="utf-8")
if "uses: ./.github/workflows/codeql.yml" in path.read_text(encoding="utf-8")
)
require(
len(callers) >= 3,
"expected at least 3 callers of _security.yml, found: "
+ (", ".join(callers) or "none")
+ " -- if a caller was deliberately removed, lower this floor on purpose",
)
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")
# The next sibling may be a comment line, not a key, so the lookahead has to
# accept any 2-space-indented non-space -- matching only ` \w` silently
# failed to find release.yml's job at all.
call = re.search(
r"\n security:\n(?P<body>(?: .*\n|\n)*?)(?=\n? \S|\Z)", caller
)
require(call is not None, f"{caller_name} no longer has a security: job")
call_body = call.group("body") if call else ""
require(
"uses: ./.github/workflows/_security.yml" in call_body,
f"{caller_name}'s security job must call _security.yml",
)
require(
re.search(r"^ security-events:\s*read\s*$", call_body, re.M) is not None,
f"{caller_name} must grant security-events: read to the security job -- "
"a called workflow cannot request more than its caller grants, and the "
"run fails at startup rather than degrading",
)
require(
re.search(r"^ actions:\s*read\s*$", call_body, re.M) is not None,
f"{caller_name} must grant actions: read to the security job",
)
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")
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")
# 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,
"release.yml verify must require needs.codeql.result == 'success'")
if failures:
print("Security gate fail-closed contract FAILED:")
+86 -1
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
@@ -47,12 +48,14 @@ failures = []
# Every lane of the full PR pipeline, spelled out here rather than read from
# the selector, so a lane silently dropped from the selector's universe fails.
FULL = {
"codeql-gate", "diag", "license-gate", "lint", "lint-mem", "lsan-macos",
"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",
"codeql-cpp", "codeql-js", "codeql-py", "codeql-actions",
}
CODEQL = {"codeql-cpp", "codeql-js", "codeql-py", "codeql-actions"}
UNIVERSE = FULL | {"contracts"}
@@ -201,6 +204,88 @@ 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"
"internal/cbm/grammar_json.c\ninternal/cbm/ts_runtime.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/grammar_go.c",
"internal/cbm/grammar_json.c",
"internal/cbm/ts_runtime.c",
"vendored/sqlite3/sqlite3.c"]:
failures.append(f"codeql (c): the retrace helper must list the translation units for "
f"changed vendored code and wrappers, 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