Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ jobs:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
actions: read
# Merge-group uploads can cancel this run after verifying that its candidate expired.
actions: write
contents: read
security-events: write
strategy:
Expand All @@ -32,6 +33,7 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Initialize CodeQL
Expand All @@ -41,6 +43,38 @@ jobs:
build-mode: none

- name: Perform CodeQL Analysis
id: analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: "/language:${{ matrix.language }}"
# A literal value is required by the action's failed-analysis diagnostic recovery.
upload: failure-only
post-processed-sarif-path: ${{ runner.temp }}/codeql-upload

- name: Publish CodeQL results
if: github.event_name != 'merge_group'
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
sarif_file: ${{ runner.temp }}/codeql-upload/upload.sarif
category: "/language:${{ matrix.language }}"

- name: Publish merge-group CodeQL results
if: github.event_name == 'merge_group'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
timeout-minutes: 8
env:
ANALYSIS_OUTCOME: ${{ steps.analysis.outcome }}
SARIF_FILE: ${{ runner.temp }}/codeql-upload/upload.sarif
SARIF_CATEGORY: "/language:${{ matrix.language }}"
with:
retries: 0
script: |
const publish = require(require('node:path').resolve('build_scripts', 'codeql_merge_group.cjs'));
await publish({
github, context, core,
sarifFile: process.env.SARIF_FILE,
category: process.env.SARIF_CATEGORY,
analysisOutcome: process.env.ANALYSIS_OUTCOME,
runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
checkout: process.env.GITHUB_WORKSPACE,
});
178 changes: 178 additions & 0 deletions build_scripts/codeql_merge_group.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// @ts-check

const { readFile } = require("node:fs/promises");
const { setTimeout: delay } = require("node:timers/promises");
const { gzipSync } = require("node:zlib");
const { pathToFileURL } = require("node:url");

/** @param {unknown} value @returns {value is Record<string, unknown>} */
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}

/** @param {unknown} value @returns {Record<string, unknown>} */
function object(value) {
if (!isObject(value)) throw new Error("Invalid GitHub API or SARIF object");
return value;
}

/**
* @typedef {{repo: {owner: string, repo: string}, eventName: string, ref: string, sha: string,
* runId: number, payload: {action?: string, merge_group?: {base_ref?: string, head_ref?: string, head_sha?: string}}}} Context
* @typedef {{request: (route: string, options: Record<string, unknown>) =>
* Promise<{status: number, data: unknown}>}} GitHub
* @param {{github: GitHub, context: Context, core: {notice: (message: string) => void, info: (message: string) => void},
* sarifFile: string, category: string, analysisOutcome: string, runAttempt: number,
* checkout: string, sleep?: (milliseconds: number) => Promise<void>}} options
*/
module.exports = async function publish({
github, context, core, sarifFile, category, analysisOutcome, runAttempt, checkout, sleep = delay
}) {
const group = context.payload.merge_group;
const queuePrefix = `refs/heads/gh-readonly-queue/${group?.base_ref?.slice(11)}/`;
if (analysisOutcome !== "success") throw new Error("Publishing requires successful CodeQL analysis");
if (
context.eventName !== "merge_group" || context.payload.action !== "checks_requested" ||
!group?.base_ref?.startsWith("refs/heads/") || !context.ref.startsWith(queuePrefix) || context.ref === queuePrefix ||
group.head_ref !== context.ref || group.head_sha !== context.sha || !/^[0-9a-f]{40}$/.test(context.sha) ||
!Number.isSafeInteger(context.runId) || context.runId <= 0 || !Number.isSafeInteger(runAttempt) || runAttempt <= 0
) {
throw new Error("The merge-group context does not match the tested queue ref, SHA and run");
}
const sarif = await readFile(sarifFile);
const document = object(JSON.parse(sarif.toString("utf8")));
if (
document.version !== "2.1.0" || !Array.isArray(document.runs) || !document.runs.length ||
document.runs.some((value) => {
const run = object(value);
return object(object(run.tool).driver).name !== "CodeQL" ||
object(run.automationDetails).id !== `${category}/`;
})
) {
throw new Error("Expected prepared CodeQL SARIF matching the matrix language category");
}
const missingRef = `ref '${context.ref}' not found in this repository`;
const runOptions = { run_id: context.runId };

/** @param {string} route @param {Record<string, unknown>} [options] @param {number} [status] */
async function request(route, options = {}, status = 200) {
const response = await github.request(route, {
...context.repo, ...options,
headers: { "X-GitHub-Api-Version": "2022-11-28" },
request: { timeout: 30000 }
});
if (response.status !== status) throw new Error(`Unexpected HTTP ${response.status} for ${route}`);
return response.data;
}

async function currentSha() {
const refs = await request("GET /repos/{owner}/{repo}/git/matching-refs/{ref}", { ref: context.ref.slice(5) });
if (!Array.isArray(refs) || refs.some((value) => typeof object(value).ref !== "string")) {
throw new Error("Invalid matching-refs response");
}
const matches = refs.map(object).filter((value) => value.ref === context.ref);
if (!matches.length) return null;
if (matches.length !== 1) throw new Error("Matching-refs returned multiple exact queue refs");
const target = object(matches[0].object);
if (target.type !== "commit" || typeof target.sha !== "string" || !/^[0-9a-f]{40}$/.test(target.sha)) {
throw new Error("The queue ref does not point to a valid commit");
}
return target.sha;
}

async function verifyRun() {
const run = object(await request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", runOptions));
const expected = {
id: context.runId, run_attempt: runAttempt, event: "merge_group", head_sha: context.sha,
head_branch: context.ref.slice(11), path: ".github/workflows/codeql.yml"
};
if (Object.entries(expected).some(([key, value]) => run[key] !== value)) {
throw new Error("Refusing to cancel a different workflow run or attempt");
}
if (run.status === "completed" && run.conclusion === "cancelled") throw new Error("CodeQL run is already cancelled");
if (run.status !== "in_progress" || run.conclusion !== null) {
throw new Error("Refusing to cancel a workflow run that is not in progress");
}
}

async function ensureLive() {
const sha = await currentSha();
if (sha === context.sha) return;
await verifyRun();
const result = object(await request(
"GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}/jobs",
{ ...runOptions, attempt_number: runAttempt, per_page: 100 }
));
if (!Array.isArray(result.jobs) || !result.jobs.length || result.total_count !== result.jobs.length) {
throw new Error("Cannot verify all CodeQL matrix jobs before cancellation");
}
for (const value of result.jobs) {
const job = object(value);
if (!Array.isArray(job.steps)) throw new Error("Invalid workflow job steps");
const conclusions = [job.conclusion, ...job.steps.map((step) => object(step).conclusion)];
if (conclusions.some((value) =>
value != null && value !== "success" && value !== "skipped" && value !== "cancelled"
)) {
throw new Error("A CodeQL matrix job or step failed; refusing to replace failure with cancellation");
}
}
core.notice(`Verified obsolete merge candidate ${context.ref}: ${sha === null ? "absent" : `now at ${sha}`}`);
try {
await request("POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel", runOptions, 202);
} catch (error) {
if (isObject(error) && error.status === 409) await verifyRun();
throw error;
}
// Never finish green while waiting for the runner's cancellation signal.
await sleep(60000);
throw new Error("Cancellation accepted, but the runner did not stop within 60 seconds");
}

await ensureLive();
let accepted;
try {
accepted = object(await request("POST /repos/{owner}/{repo}/code-scanning/sarifs", {
commit_sha: context.sha,
ref: context.ref,
sarif: gzipSync(sarif).toString("base64"),
checkout_uri: pathToFileURL(checkout).href,
tool_name: "CodeQL"
}, 202));
} catch (error) {
if (
isObject(error) && (error.status === 400 || error.status === 404) && isObject(error.response) &&
isObject(error.response.data) && error.response.data.message === missingRef &&
(error.response.data.errors == null ||
(Array.isArray(error.response.data.errors) && !error.response.data.errors.length))
) {
await ensureLive();
}
throw error;
}
const sarifId = accepted.id;
if (typeof sarifId !== "string" || !sarifId) throw new Error("SARIF upload accepted without a valid ID");
for (const milliseconds of [5000, 10000, 20000, 40000, 80000]) {
await sleep(milliseconds);
const result = object(await request(
"GET /repos/{owner}/{repo}/code-scanning/sarifs/{sarif_id}", { sarif_id: sarifId }
));
if (result.processing_status === "complete") {
await ensureLive();
core.info(`CodeQL SARIF ${sarifId} processed for ${context.ref} at ${context.sha}`);
return sarifId;
}
if (result.processing_status === "failed") {
if (Array.isArray(result.errors) && result.errors.length === 1 && result.errors[0] === missingRef) {
await ensureLive();
}
throw new Error(`SARIF processing failed: ${JSON.stringify(result.errors)}`);
}
if (result.processing_status !== "pending") {
throw new Error(`Invalid SARIF processing status: ${result.processing_status}`);
}
await ensureLive();
}
throw new Error("SARIF processing did not complete within five status checks");
};
29 changes: 29 additions & 0 deletions doc/contributing/4_running_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ server, including when the `litellm` or `all` extra is installed. Real adapter
and outgoing-header checks run separately as
[loopback integration tests](./6_integration_tests.md#litellm-loopback-integration-tests).

### CodeQL merge-group uploads

CodeQL still analyzes all three languages on PRs, merge groups, main/release pushes,
manual runs and its weekly schedule. Merge groups publish the analyzer's prepared
SARIF through the documented code-scanning API so an expired queue ref can be
distinguished from a genuine upload failure. Categories and fingerprints are
preserved; failed analysis retains CodeQL's native diagnostic upload.
The merge-group helper uses the authenticated client and context supplied by the
pinned `actions/github-script` action, with no separate HTTP client or Python setup.

Only a successful matching-refs response proving that the exact queue ref is absent
or points to another SHA permits retirement. A missing-ref upload rejection is
rechecked to cover deletion between verification and upload. Authentication,
service, processing and analysis failures stay failures. Retirement cancels the
verified current CodeQL run, never reports a skipped scan as success, and refuses
cancellation when a matrix job has already failed. The CodeQL job needs
`actions: write` for self-cancellation; workflow-wide permissions remain empty.
Fork PRs use native CodeQL publishing and GitHub's read-only fork token policy.

Run the deterministic, offline regression coverage with Node.js installed.
The pytest entry point runs the Node scenario tests and checks workflow wiring:

```bash
uv run --frozen pytest tests/unit/build_scripts/test_codeql_merge_group.py -q
```

The helper's scenarios can also run directly with
`node --test tests/unit/build_scripts/test_codeql_merge_group.cjs`.

## Running a subset while iterating

For a narrower run, invoke `pytest` directly. You can invoke pytest if it's in your path or via python; either `pytest` or `python -m pytest`. For the following examples, we will use `pytest`.
Expand Down
Loading
Loading