mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
Break-glass merge PR #2734
This commit is contained in:
@@ -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,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:
|
||||
|
||||
@@ -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
@@ -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) ==="
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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 (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
@@ -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
|
||||
|
||||
|
||||
Executable
+121
@@ -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"
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
@@ -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
@@ -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.
|
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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