mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
5afdfcaac9
commit
453b21e680
18 files changed
+694
-158
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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)
|
||||
|
+20
-16
@@ -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.
|
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user