Compare commits

..

38 Commits

Author SHA1 Message Date
Michael B. Gale
9104ff3a11 Scaffold validation job 2026-10-12 17:51:15 +01:00
Michael B. Gale
a4fe152ba9 Log when we didn't get a normal configuration file 2026-10-12 17:24:21 +01:00
Michael B. Gale
bf0c20744a Upload failed SARIF as workflow artifact in test mode 2026-10-12 17:23:59 +01:00
Michael B. Gale
9cd21186b1 Add test for new generateFailedSarif behaviour 2026-10-12 16:56:51 +01:00
Michael B. Gale
61b1d55aa5 Address EXDEV issue 2026-10-12 16:37:06 +01:00
Michael B. Gale
93b4ae6c0e Handle failures in writeTemporaryDiagnosticFile 2026-10-12 16:20:25 +01:00
Michael B. Gale
104d981b76 Refactor writeTemporaryDiagnosticFile out of add[NoLanguage]Diagnostic 2026-10-12 16:15:55 +01:00
Michael B. Gale
ac89bf9d46 Fix typos 2026-10-12 16:09:49 +01:00
Michael B. Gale
7089ca74ea Fix hardcoded path separator 2026-10-12 16:08:41 +01:00
Michael B. Gale
9eeab46818 Add e2e test 2026-10-12 14:14:45 +01:00
Michael B. Gale
e0fedcf877 Use temporary diagnostic dir in generateFailedSarif 2026-10-12 14:09:21 +01:00
Michael B. Gale
bbeb45dd7a Make diagnostics less dependent on global state
Add limited test coverage
2026-10-12 14:09:18 +01:00
Michael B. Gale
50b827040e Write diagnostics to temporary location when database isn't ready 2026-10-12 13:09:59 +01:00
Michael B. Gale
23835d3cbe Refactor writeDiagnosticFile out of writeDiagnostic 2026-10-12 13:09:39 +01:00
Michael B. Gale
ac8654a8f2 Add printDebugLogs tests 2026-10-12 13:08:08 +01:00
Michael B. Gale
ecfc96e0a9 Add getDisplayActionName tests 2026-10-12 13:08:01 +01:00
Michael B. Gale
409be01136 Replace deprecated functions in init-action.post.ts 2026-10-12 13:07:55 +01:00
Michael B. Gale
f4a15a13fd Try to load failed config if normal config is unavailable
Adapt relevant functions by narrowing their types or skipping steps that would otherwise have been skipped as well
2026-10-12 13:07:48 +01:00
Michael B. Gale
4fdbb71b54 Use runInActions for init-post 2026-10-12 13:07:38 +01:00
Michael B. Gale
5136039854 Store partial configuration when init is aborted 2026-10-12 13:07:36 +01:00
Michael B. Gale
19445ddfed Reduce Config requirements for tryUploadSarifIfRunFailed 2026-10-12 13:07:13 +01:00
Michael B. Gale
317f264e1f Change diagnosticsExport to not require full Config 2026-10-12 13:07:06 +01:00
Michael B. Gale
309f6af89b Change getGeneratedCodeScanningConfigPath to not require full Config 2026-10-12 13:06:59 +01:00
Michael B. Gale
cf8f7b9787 Add ActionConfig type 2026-10-12 13:06:43 +01:00
Henry Mercer
0db2f2b4a5 Merge pull request #4205 from github/henrymercer/require-codeql-2-20-7
Bump minimum CodeQL CLI version to 2.20.7
2026-10-12 10:04:43 +00:00
Henry Mercer
ae96eed5bb Merge remote-tracking branch 'origin/main' into henrymercer/require-codeql-2-20-7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-12 10:11:02 +01:00
Henry Mercer
768784f193 Merge branch 'main' into henrymercer/require-codeql-2-20-7 2026-10-08 14:29:21 +01:00
Henry Mercer
5d90bea8a6 Add PR number to changelog entry and list it first
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:44:12 +01:00
Henry Mercer
8e7104f197 Clarify the oldest-version comment in the Rust PR check
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:41:20 +01:00
Henry Mercer
0b5c1fb069 Remove redundant CLI version requirement from CppDependencyInstallation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:41:15 +01:00
Henry Mercer
a8c7110b26 Remove unreachable minimum version check for Rust analysis
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:41:09 +01:00
Henry Mercer
3c1b1b65a3 Remove CodeQL version check from debug artifact upload
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:41:03 +01:00
Henry Mercer
10e415aae1 Remove Kotlin version limit workaround for CodeQL CLI 2.20.3
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:58 +01:00
Henry Mercer
b97fbb3711 Remove CODEQL_PROXY_* workaround for CLIs older than 2.20.7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:52 +01:00
Henry Mercer
17e7897d6c Add CodeQL 2.25.6 to the PR check matrix
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:49 +01:00
Henry Mercer
74aeac99e7 Stop testing CodeQL versions older than 2.20.7 in PR checks
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:46 +01:00
Henry Mercer
05d28f2f71 Bump version to 4.39.0
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:38 +01:00
Henry Mercer
772ffcd999 Bump minimum CodeQL CLI version to 2.20.7
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-07 14:40:35 +01:00
43 changed files with 1808 additions and 1045 deletions

119
.github/workflows/__diagnostics-without-db.yml generated vendored Normal file
View File

@@ -0,0 +1,119 @@
# Warning: This file is generated automatically, and should not be modified.
# Instead, please modify the template in the pr-checks directory and run:
# pr-checks/sync.sh
# to regenerate this file.
name: PR Check - Diagnostic export without database
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GO111MODULE: auto
on:
push:
branches:
- main
- releases/v*
pull_request: {}
merge_group:
types:
- checks_requested
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
inputs: {}
workflow_call:
inputs: {}
defaults:
run:
shell: bash
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: diagnostics-without-db-${{github.ref}}
jobs:
diagnostics-without-db:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: linked
name: Diagnostic export without database
if: github.triggering_actor != 'dependabot[bot]'
permissions:
contents: read
security-events: read
timeout-minutes: 45
runs-on: ${{ matrix.os }}
steps:
# We deliberately need to make this step fail before the database is initialised, but after
# some diagnostic has been produced. The simplest way to do that right now is to force the
# use of a nightly release and then fail because we are attempting to analyse Swift on
# a non-macOS runner.
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test
id: prepare-test
uses: ./.github/actions/prepare-test
with:
version: ${{ matrix.version }}
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Initialise failing analysis
id: init
uses: ./../action/init
continue-on-error: true
env:
CODEQL_ACTION_FORCE_NIGHTLY: true
with:
languages: swift
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check diagnostics appear on disk
env:
SARIF_PATH: '${{ runner.temp }}/codeql-action-temp-diagnostics/'
run: |
cd ../action
npx tsx ./pr-checks/diagnostics-without-db.ts
env:
CODEQL_ACTION_TEST_MODE: false
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
diagnostics-without-db-validation:
if: github.triggering_actor != 'dependabot[bot]'
needs:
- diagnostics-without-db
permissions:
contents: read
security-events: read
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Download workflow artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-mode-sarif
path: sarif
- name: List artifact contents
run: ls -laR ./sarif
env:
CODEQL_ACTION_TEST_MODE: true

View File

@@ -44,8 +44,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: stable-v2.19.4
- os: ubuntu-latest
version: stable-v2.20.7
- os: ubuntu-latest
@@ -56,6 +54,8 @@ jobs:
version: stable-v2.23.9
- os: ubuntu-latest
version: stable-v2.24.3
- os: ubuntu-latest
version: stable-v2.25.6
- os: ubuntu-latest
version: default
- os: ubuntu-latest

View File

@@ -44,8 +44,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: stable-v2.19.4
- os: ubuntu-latest
version: stable-v2.20.7
- os: ubuntu-latest
@@ -56,6 +54,8 @@ jobs:
version: stable-v2.23.9
- os: ubuntu-latest
version: stable-v2.24.3
- os: ubuntu-latest
version: stable-v2.25.6
- os: ubuntu-latest
version: default
- os: ubuntu-latest

View File

@@ -44,8 +44,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: stable-v2.19.4
- os: ubuntu-latest
version: stable-v2.20.7
- os: ubuntu-latest
@@ -56,6 +54,8 @@ jobs:
version: stable-v2.23.9
- os: ubuntu-latest
version: stable-v2.24.3
- os: ubuntu-latest
version: stable-v2.25.6
- os: ubuntu-latest
version: default
- os: ubuntu-latest

View File

@@ -54,10 +54,6 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
version: stable-v2.19.4
- os: macos-15-xlarge
version: stable-v2.19.4
- os: ubuntu-latest
version: stable-v2.20.7
- os: macos-15-xlarge
@@ -78,6 +74,10 @@ jobs:
version: stable-v2.24.3
- os: macos-latest-xlarge
version: stable-v2.24.3
- os: ubuntu-latest
version: stable-v2.25.6
- os: macos-latest-xlarge
version: stable-v2.25.6
- os: ubuntu-latest
version: default
- os: macos-latest-xlarge

2
.github/workflows/__rust.yml generated vendored
View File

@@ -35,7 +35,7 @@ jobs:
matrix:
include:
- os: ubuntu-latest
version: stable-v2.19.4
version: stable-v2.20.7
- os: ubuntu-latest
version: stable-v2.22.1
- os: ubuntu-latest

View File

@@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
version:
- stable-v2.20.3
- stable-v2.20.7
- default
- linked
- nightly-latest

View File

@@ -29,7 +29,7 @@ jobs:
fail-fast: false
matrix:
version:
- stable-v2.20.3
- stable-v2.20.7
- default
- linked
- nightly-latest
@@ -85,7 +85,7 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Check expected artifacts exist
run: |
VERSIONS="stable-v2.20.3 default linked nightly-latest"
VERSIONS="stable-v2.20.7 default linked nightly-latest"
LANGUAGES="cpp csharp go java javascript python"
for version in $VERSIONS; do
pushd "./my-debug-artifacts-${version//./}"

View File

@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
No user facing changes.
- _Breaking change_: Bump the minimum required CodeQL bundle version to 2.20.7. [#4205](https://github.com/github/codeql-action/pull/4205)
## 4.38.3 - 08 Oct 2026

1270
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.38.4",
"version": "4.39.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.38.4",
"version": "4.39.0",
"license": "MIT",
"workspaces": [
"pr-checks"

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.38.4",
"version": "4.39.0",
"private": true,
"description": "CodeQL action",
"scripts": {

View File

@@ -0,0 +1,49 @@
name: "Diagnostic export without database"
description: "Tests that temporary diagnostics are correctly included in the failed SARIF."
versions:
# Overridden by the FF below.
- linked
installNode: true
env:
# Allow the failed SARIF file to be generated.
CODEQL_ACTION_TEST_MODE: false
# But don't actually upload it.
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
# Mark telemetry for this workflow so it can be treated separately.
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
steps:
# We deliberately need to make this step fail before the database is initialised, but after
# some diagnostic has been produced. The simplest way to do that right now is to force the
# use of a nightly release and then fail because we are attempting to analyse Swift on
# a non-macOS runner.
- name: Initialise failing analysis
id: init
uses: ./../action/init
continue-on-error: true
env:
CODEQL_ACTION_FORCE_NIGHTLY: true
with:
languages: swift
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check diagnostics appear on disk
env:
SARIF_PATH: "${{ runner.temp }}/codeql-action-temp-diagnostics/"
run: |
cd ../action
npx tsx ./pr-checks/diagnostics-without-db.ts
validationJobs:
diagnostics-without-db-validation:
checkout: true
installNode: true
steps:
- name: Download workflow artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: test-mode-sarif
path: sarif
- name: List artifact contents
run: ls -laR ./sarif

View File

@@ -10,7 +10,6 @@ operatingSystems:
- os: macos
runner-image: macos-15-xlarge
codeql-versions:
- stable-v2.19.4
- stable-v2.20.7
- stable-v2.21.4
- stable-v2.22.4

View File

@@ -1,8 +1,8 @@
name: "Rust analysis"
description: "Tests creation of a Rust database"
versions:
# experimental rust support introduced, requires action to set `CODEQL_ENABLE_EXPERIMENTAL_FEATURES`
- stable-v2.19.4
# oldest CodeQL version supported by the Action, where Rust support is experimental and requires the Action to set `CODEQL_ENABLE_EXPERIMENTAL_FEATURES`
- stable-v2.20.7
# first public preview version
- stable-v2.22.1
- linked

View File

@@ -0,0 +1,48 @@
import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import type * as diagnostics from "../src/diagnostics";
const DIAGNOSTIC_ID = "codeql-action/forced-nightly-cli";
const sarifDirectory = process.env["SARIF_PATH"];
if (sarifDirectory === undefined) {
throw new Error("SARIF_PATH is undefined.");
}
const paths = fs
.readdirSync(sarifDirectory, {
recursive: true,
encoding: "utf-8",
})
.filter((p) => p.endsWith(".json"));
core.info(`Found ${paths.length} paths in '${sarifDirectory}'.`);
core.info(`Searching for a diagnostic with id '${DIAGNOSTIC_ID}'...`);
let found = false;
for (const diagnosticPath of paths) {
if (path.extname(diagnosticPath) !== ".json") {
core.info(`Skipping ${diagnosticPath}.`);
continue;
}
core.info(`Found ${diagnosticPath}.`);
const contents = JSON.parse(
fs.readFileSync(path.resolve(sarifDirectory, diagnosticPath), "utf-8"),
) as diagnostics.DiagnosticMessage;
if (contents.source.id === DIAGNOSTIC_ID) {
core.info(
`Found '${DIAGNOSTIC_ID}' in '${diagnosticPath}'. Skipping remaining files.`,
);
found = true;
break;
}
}
if (!found) {
throw new Error(`Didn't find '${DIAGNOSTIC_ID}' diagnostic.`);
}

View File

@@ -90,7 +90,7 @@ interface Specification extends JobSpecification {
services?: any;
/** Additional jobs to run after the main PR check job. */
validationJobs?: Record<string, JobSpecification>;
validationJobs?: Record<string, ValidationJobSpecification>;
/** If set, this check is part of a named collection that gets its own caller workflow. */
collection?: string;
@@ -122,6 +122,10 @@ interface JobSpecification {
installYq?: boolean;
}
type ValidationJobSpecification = JobSpecification & {
checkout?: boolean;
};
/** Describes language/framework-specific steps and inputs. */
interface LanguageSetup {
specProperty: keyof JobSpecification;
@@ -136,8 +140,6 @@ type LanguageSetups = Partial<Record<BuiltInLanguage, LanguageSetup>>;
// The default set of CodeQL Bundle versions to use for the PR checks.
const defaultTestVersions = [
// The oldest supported CodeQL version. If bumping, update `CODEQL_MINIMUM_VERSION` in `codeql.ts`
"stable-v2.19.4",
// The last CodeQL release in the 2.20 series.
"stable-v2.20.7",
// The last CodeQL release in the 2.21 series.
"stable-v2.21.4",
@@ -147,6 +149,8 @@ const defaultTestVersions = [
"stable-v2.23.9",
// The last CodeQL release in the 2.24 series.
"stable-v2.24.3",
// The last CodeQL release in the 2.25 series.
"stable-v2.25.6",
// The default version of CodeQL for Dotcom, as determined by feature flags.
"default",
// The version of CodeQL shipped with the Action in `defaults.json`. During the release process
@@ -462,6 +466,20 @@ function generateJobMatrix(
return matrix;
}
/**
* Gets the `actions/checkout` step.
*/
function getCheckoutStep(): Step {
return {
name: "Check out repository",
uses: pinnedUses(
"actions/checkout",
"3d3c42e5aac5ba805825da76410c181273ba90b1",
"v7.0.1",
),
};
}
/**
* Retrieves setup steps and additional input definitions based on specific languages or frameworks
* that are requested by the `checkSpecification`.
@@ -531,14 +549,7 @@ function generateJob(
// Construct the workflow steps needed for this check.
const steps: Step[] = [
{
name: "Check out repository",
uses: pinnedUses(
"actions/checkout",
"3d3c42e5aac5ba805825da76410c181273ba90b1",
"v7.0.1",
),
},
getCheckoutStep(),
...setupInfo.steps,
{
name: "Prepare test",
@@ -609,13 +620,16 @@ function generateJob(
/** Generates a validation job. */
function generateValidationJob(
specDocument: yaml.Document,
jobSpecification: JobSpecification,
jobSpecification: ValidationJobSpecification,
checkName: string,
name: string,
) {
// Determine which languages or frameworks have to be installed.
const { inputs, steps } = getSetupSteps(jobSpecification);
// Construct a checkout step if requested.
const checkoutStep = jobSpecification.checkout ? [getCheckoutStep()] : [];
// Extract the sequence of steps from the YAML document to persist as much formatting as possible.
const specSteps = specDocument.getIn([
"validationJobs",
@@ -624,7 +638,7 @@ function generateValidationJob(
]) as yaml.YAMLSeq;
// Add the generated steps in front of the ones from the specification.
specSteps.items.unshift(...steps);
specSteps.items.unshift(...checkoutStep, ...steps);
const validationJob: Record<string, any> = {
name: jobSpecification.name,
@@ -635,7 +649,7 @@ function generateValidationJob(
"security-events": "read",
},
"timeout-minutes": 5,
"runs-on": "ubuntu-slim",
"runs-on": "ubuntu-latest",
steps: specSteps,
};

View File

@@ -19,6 +19,24 @@ import { getErrorMessage } from "./util";
setupTests(test);
for (const [key, displayName] of Object.entries(
statusReport.ACTION_DISPLAY_NAMES,
)) {
test(`getDisplayActionName - '${displayName}' instead of '${key}'`, (t) => {
t.is(statusReport.getDisplayActionName(key as ActionName), displayName);
});
}
test("getDisplayActionName - returns action name", (t) => {
for (const actionName of Object.values(ActionName)) {
if (statusReport.ACTION_DISPLAY_NAMES[actionName]) {
continue;
}
t.is(statusReport.getDisplayActionName(actionName), actionName);
}
});
interface RunInActionsTestOpts {
runFn?: () => Promise<any>;
expectedErrorMessage?: string;

View File

@@ -7,12 +7,13 @@ import {
isAnalyzingPullRequest,
isDefaultSetup,
isDynamicWorkflow,
printDebugLogs,
} from "./actions-util";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { setupTests } from "./testing-utils";
import { initializeEnvironment } from "./util";
import { initializeEnvironment, withTmpDir } from "./util";
setupTests(test);
@@ -100,6 +101,19 @@ test("computeAutomationID()", async (t) => {
);
});
test("printDebugLogs - doesn't throw when config doesn't have needed properties", async (t) => {
await t.notThrowsAsync(printDebugLogs({}));
await t.notThrowsAsync(printDebugLogs({ languages: [] }));
await t.notThrowsAsync(printDebugLogs({ dbLocation: "nowhere" }));
});
test("printDebugLogs - doesn't throw when language db doesn't exist", async (t) =>
withTmpDir(async (tmpDir) => {
await t.notThrowsAsync(
printDebugLogs({ dbLocation: tmpDir, languages: ["javascript"] }),
);
}));
test.serial("getPullRequestBranches() with pull request context", (t) => {
withMockedContext(
{

View File

@@ -6,7 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import type { Config } from "./config-utils";
import { hasConfigKey, type Config } from "./config/action-config";
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
import {
@@ -131,7 +131,14 @@ export function getWorkflowEvent(env: Env = getEnv()): any {
}
}
export async function printDebugLogs(config: Config) {
export async function printDebugLogs(config: Partial<Config>) {
if (
!hasConfigKey(config, "languages") ||
!hasConfigKey(config, "dbLocation")
) {
return;
}
for (const language of config.languages) {
const databaseDirectory = getCodeQLDatabasePath(config, language);
const logsDirectory = path.join(databaseDirectory, "log");

View File

@@ -9,7 +9,6 @@ import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { getCodeQL } from "./codeql";
import { getConfig } from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import {
@@ -38,12 +37,9 @@ export async function runWrapper() {
logger,
);
if (config !== undefined) {
const codeql = await getCodeQL(logger, config.codeQLCmd);
const version = await codeql.getVersion();
await debugArtifacts.uploadCombinedSarifArtifacts(
logger,
config.gitHubVersion.type,
version.version,
);
}
}

View File

@@ -268,17 +268,6 @@ async function run({
);
}
// Unset the CODEQL_PROXY_* environment variables when using older CodeQL
// CLIs, as they are not needed and can cause issues.
if (
process.env.CODEQL_PROXY_HOST === "" &&
!(await util.codeQlVersionAtLeast(codeql, "2.20.7"))
) {
delete process.env.CODEQL_PROXY_HOST;
delete process.env.CODEQL_PROXY_PORT;
delete process.env.CODEQL_PROXY_CA_CERTIFICATE;
}
if (actionsUtil.getOptionalInput("cleanup-level")) {
logger.info(
"The 'cleanup-level' input is ignored since the CodeQL Action now automatically " +

View File

@@ -9,6 +9,7 @@ import { getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { setupCppAutobuild } from "./autobuild";
import { type CodeQL } from "./codeql";
import type { ActionConfig } from "./config/action-config";
import { defaultSuites } from "./config/db-config";
import * as configUtils from "./config-utils";
import {
@@ -124,7 +125,7 @@ export async function runExtraction(
language === BuiltInLanguage.cpp &&
config.buildMode === BuildMode.Autobuild
) {
await setupCppAutobuild(codeql, logger);
await setupCppAutobuild(logger);
}
// The Java and C# `build-mode: none` extractors place dependencies in the
@@ -171,7 +172,7 @@ async function shouldExtractLanguage(
}
export function dbIsFinalized(
config: configUtils.Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
) {

View File

@@ -110,7 +110,7 @@ export async function determineAutobuildLanguages(
return languages;
}
export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
export async function setupCppAutobuild(logger: Logger) {
const envVar = featureConfig[Feature.CppDependencyInstallation].envVar;
const featureName = "C++ automatic installation of dependencies";
const gitHubVersion = await getGitHubVersion();
@@ -121,7 +121,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
getTemporaryDirectory(),
logger,
);
if (await features.getValue(Feature.CppDependencyInstallation, codeql)) {
if (await features.getValue(Feature.CppDependencyInstallation)) {
// disable autoinstall on self-hosted runners unless explicitly requested
if (
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] === "self-hosted" &&
@@ -155,7 +155,7 @@ export async function runAutobuild(
logger.startGroup(`Attempting to automatically build ${language} code`);
const codeQL = await getCodeQL(logger, config.codeQLCmd);
if (language === BuiltInLanguage.cpp) {
await setupCppAutobuild(codeQL, logger);
await setupCppAutobuild(logger);
}
if (config.buildMode) {
await codeQL.extractUsingBuildMode(config, language);

View File

@@ -15,6 +15,7 @@ import * as api from "./api-client";
import * as outputCache from "./cli/output-cache";
import type { VersionInfo } from "./cli/types";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import type { ActionConfig } from "./config/action-config";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
@@ -197,7 +198,8 @@ export interface CodeQL {
diagnosticsExport(
sarifFile: string,
automationDetailsId: string | undefined,
config: Config,
config: ActionConfig<"tempDir">,
diagnosticDir: string | undefined,
): Promise<void>;
/** Get the location of an extractor for the specified language. */
resolveExtractor(language: Language): Promise<string>;
@@ -253,7 +255,7 @@ let cachedCodeQL: CodeQL | undefined = undefined;
* The version flags below can be used to conditionally enable certain features
* on versions newer than this.
*/
const CODEQL_MINIMUM_VERSION = "2.19.4";
const CODEQL_MINIMUM_VERSION = "2.20.7";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
@@ -913,7 +915,8 @@ async function getCodeQLForCmd(
async diagnosticsExport(
sarifFile: string,
automationDetailsId: string | undefined,
config: Config,
config: ActionConfig<"tempDir">,
diagnosticDir: string | undefined,
): Promise<void> {
const args = [
"diagnostics",
@@ -923,6 +926,9 @@ async function getCodeQLForCmd(
`--sarif-codescanning-config=${getGeneratedCodeScanningConfigPath(
config,
)}`,
...(diagnosticDir !== undefined
? [`--diagnostic-dir=${diagnosticDir}`]
: []),
...getExtraOptionsFromEnv(["diagnostics", "export"]),
];
if (automationDetailsId !== undefined) {
@@ -1237,7 +1243,9 @@ export async function getTrapCachingExtractorConfigArgsForLang(
*
* This will not exist if the configuration is being parsed in the Action.
*/
function getGeneratedCodeScanningConfigPath(config: Config): string {
function getGeneratedCodeScanningConfigPath(
config: ActionConfig<"tempDir">,
): string {
return path.resolve(config.tempDir, "user-config.yaml");
}

View File

@@ -21,7 +21,11 @@ import {
import * as api from "./api-client";
import { getCachingKind } from "./caching-utils";
import { type CodeQL } from "./codeql";
import { type Config } from "./config/action-config";
import {
type AbortedInitConfig,
type ActionConfig,
type Config,
} from "./config/action-config";
import {
calculateAugmentation,
ExcludeQueryFilter,
@@ -71,6 +75,7 @@ import {
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { shouldSkipOverlayAnalysis } from "./overlay/status";
import { RepositoryNwo } from "./repository";
import { ActionName } from "./status-report";
import { ToolsFeature } from "./tools-features";
import { downloadTrapCaches } from "./trap-caching";
import {
@@ -1379,31 +1384,84 @@ export function getPathToParsedConfigFile(tempDir: string): string {
return path.join(tempDir, "config");
}
function getPathToFailedConfigFile(tempDir: string): string {
return path.join(tempDir, "failed-config");
}
/**
* Store the given config to the path returned from getPathToParsedConfigFile.
* Writes {@link config} to {@link configFilePath}.
*
* This function is not intended to be called, other than by
* {@link saveConfig} and {@link saveFailedConfig} below.
*/
export async function saveConfig(config: Config, logger: Logger) {
function saveConfigFile(
logger: Logger,
configFilePath: string,
config: Partial<Config>,
) {
const configString = JSON.stringify(config);
const configFile = getPathToParsedConfigFile(config.tempDir);
fs.mkdirSync(path.dirname(configFile), { recursive: true });
fs.writeFileSync(configFile, configString, "utf8");
fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
fs.writeFileSync(configFilePath, configString, "utf8");
logger.debug("Saved config:");
logger.debug(configString);
}
/**
* Get the config that has been saved to the given temp dir.
* If the config could not be found then returns undefined.
* Store the given config to the path returned from getPathToParsedConfigFile.
*/
export async function getConfig(
tempDir: string,
export async function saveConfig(config: Config, logger: Logger) {
const configFile = getPathToParsedConfigFile(config.tempDir);
saveConfigFile(logger, configFile, config);
}
/**
* Store parts of {@link config} as a failed configuration file.
*
* The file will only contain parts of {@link config} that are relevant to
* the `init-post` step in the workflow.
*/
export function saveAbortedInitConfig(
action: ActionState<["Base", "Logger"]>,
config: Config,
) {
// Should only be called by init.
if (action.name !== ActionName.Init) {
return;
}
// Create a copy of the input `Config` that only contains the keys we want.
const failedConfig: AbortedInitConfig = {
version: config.version,
tempDir: config.tempDir,
codeQLCmd: config.codeQLCmd,
debugMode: config.debugMode,
gitHubVersion: config.gitHubVersion,
languages: config.languages,
analysisKinds: config.analysisKinds,
};
action.logger.debug(`Saving config for failed init step.`);
saveConfigFile(
action.logger,
getPathToFailedConfigFile(config.tempDir),
failedConfig,
);
}
/**
* Attempts to read a {@link Config} from {@link configFilePath}.
*
* This function is not intended to be called, other than by
* {@link getConfig} and {@link getAbortedInitConfig} below.
*/
function readConfigFile(
logger: Logger,
): Promise<Config | undefined> {
const configFile = getPathToParsedConfigFile(tempDir);
if (!fs.existsSync(configFile)) {
configFilePath: string,
): Config | undefined {
if (!fs.existsSync(configFilePath)) {
return undefined;
}
const configString = fs.readFileSync(configFile, "utf8");
const configString = fs.readFileSync(configFilePath, "utf8");
logger.debug("Loaded config:");
logger.debug(configString);
@@ -1428,6 +1486,36 @@ export async function getConfig(
return config as Config;
}
/**
* Get the config that has been saved to the given temp dir.
* If the config could not be found then returns undefined.
*/
export async function getConfig(
tempDir: string,
logger: Logger,
): Promise<Config | undefined> {
const configFile = getPathToParsedConfigFile(tempDir);
return readConfigFile(logger, configFile);
}
/**
* Loads a {@link AbortedInitConfig} from a failed `init` step if it exists.
*
* The result will only contain parts of a {@link Config} that are relevant to
* the `init-post` step in the workflow.
*/
export function getAbortedInitConfig(
action: ActionState<["Base", "Logger"]>,
tempDir: string,
): AbortedInitConfig | undefined {
// Only intended to be called by the `init-post` action.
if (action.name !== ActionName.InitPost) {
return undefined;
}
return readConfigFile(action.logger, getPathToFailedConfigFile(tempDir));
}
/**
* Generate a `qlconfig.yml` file from the `registries` input.
* This file is used by the CodeQL CLI to list the registries to use for each
@@ -1618,21 +1706,27 @@ export function appendExtraQueryExclusions(
/**
* Returns `true` if Code Scanning analysis is enabled, or `false` if not.
*/
export function isCodeScanningEnabled(config: Config): boolean {
export function isCodeScanningEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.CodeScanning);
}
/**
* Returns `true` if Code Quality analysis is enabled, or `false` if not.
*/
export function isCodeQualityEnabled(config: Config): boolean {
export function isCodeQualityEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.CodeQuality);
}
/**
* Returns `true` if Code Scanning Risk Assessment analysis is enabled, or `false` if not.
*/
export function isRiskAssessmentEnabled(config: Config): boolean {
export function isRiskAssessmentEnabled(
config: ActionConfig<"analysisKinds">,
): boolean {
return config.analysisKinds.includes(AnalysisKind.RiskAssessment);
}

View File

@@ -126,3 +126,38 @@ export interface Config {
*/
enableFileCoverageInformation: boolean;
}
/** Properties available in `Config`. */
export type ActionConfigProperties = keyof Config;
/**
* The CodeQL Action configuration state that is persisted
* between steps of the CodeQL Action in a CodeQL workflow.
*
* By default, this is equivalent to `Config`. By explicitly
* providing required property names for `T` this can be restricted
* to requiring only those. For example, in `ActionConfig<"codeqlCmd">`
* all properties except `codeqlCmd` are optional.
*/
export type ActionConfig<
T extends ActionConfigProperties = ActionConfigProperties,
> = Partial<Config> & Pick<Config, T>;
/** Helper function to check whether `config` has `key` to narrow the type of `config`. */
export function hasConfigKey<Key extends ActionConfigProperties>(
config: Partial<Config>,
key: Key,
): config is ActionConfig<Key> {
return config[key] !== undefined;
}
/** A minimal configuration that we can make available even when `init` aborts. */
export type AbortedInitConfig = ActionConfig<
| "version"
| "tempDir"
| "gitHubVersion"
| "debugMode"
| "languages"
| "analysisKinds"
| "codeQLCmd"
>;

View File

@@ -9,8 +9,9 @@ import {
LoggedMessage,
makeMacro,
RecordingLogger,
setupActionsVars,
} from "../testing-utils";
import { ConfigurationError, prettyPrintPack } from "../util";
import { ConfigurationError, prettyPrintPack, withTmpDir } from "../util";
import * as dbConfig from "./db-config";
@@ -562,69 +563,84 @@ test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplie
t.deepEqual(result, configFile);
});
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
test.serial(
"mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
configFile,
);
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
},
);
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
test.serial(
"mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
},
} as unknown as dbConfig.DefaultSetupConfig,
},
} as unknown as dbConfig.DefaultSetupConfig,
},
configFile,
);
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(
", ",
);
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
},
);
/** Parses `contents` as a configuration without validating it. */
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {

View File

@@ -55,25 +55,20 @@ test("getArtifactSuffix", (t) => {
}
});
// These next tests check the correctness of the logic to determine whether or not
// artifacts are uploaded in debug mode. Since it's not easy to mock the actual
// call to upload an artifact, we just check that we get an "upload-failed" result,
// instead of actually uploading the artifact.
//
// For tests where we expect artifact upload to be blocked, we check for a different
// response from the function.
// These next tests check whether `uploadArtifacts` attempts to upload an artifact. Since it's not
// easy to mock the actual call to upload an artifact, we check that we get an "upload-failed"
// result, instead of actually uploading the artifact.
test("uploadDebugArtifacts when artifacts empty should emit 'no-artifacts-to-upload'", async (t) => {
test("uploadArtifacts returns 'no-artifacts-to-upload' when there are no files to upload", async (t) => {
// Test that no error is thrown if artifacts list is empty.
const logger = getActionsLogger();
await t.notThrowsAsync(async () => {
const uploaded = await debugArtifacts.uploadDebugArtifacts(
const uploaded = await debugArtifacts.uploadArtifacts(
logger,
[],
"i-dont-exist",
"artifactName",
GitHubVariant.DOTCOM,
undefined,
);
t.is(
uploaded,
@@ -83,17 +78,15 @@ test("uploadDebugArtifacts when artifacts empty should emit 'no-artifacts-to-upl
});
});
test("uploadDebugArtifacts when no codeql version is used should invoke artifact upload", async (t) => {
// Test that the artifact is uploaded.
test("uploadArtifacts attempts an upload when there are files to upload", async (t) => {
const logger = getActionsLogger();
await t.notThrowsAsync(async () => {
const uploaded = await debugArtifacts.uploadDebugArtifacts(
const uploaded = await debugArtifacts.uploadArtifacts(
logger,
["hucairz"],
"i-dont-exist",
"artifactName",
GitHubVariant.DOTCOM,
undefined,
);
t.is(
uploaded,
@@ -103,44 +96,3 @@ test("uploadDebugArtifacts when no codeql version is used should invoke artifact
);
});
});
test("uploadDebugArtifacts when new codeql version is used should invoke artifact upload", async (t) => {
// Test that the artifact is uploaded.
const logger = getActionsLogger();
await t.notThrowsAsync(async () => {
const uploaded = await debugArtifacts.uploadDebugArtifacts(
logger,
["hucairz"],
"i-dont-exist",
"artifactName",
GitHubVariant.DOTCOM,
"2.20.3",
);
t.is(
uploaded,
// The failure is expected since we don't want to actually upload any artifacts in unit tests.
"upload-failed",
"Expect failure to upload artifacts since root dir does not exist",
);
});
});
test("uploadDebugArtifacts when old codeql is used should avoid trying to upload artifacts", async (t) => {
// Test that the artifact is not uploaded.
const logger = getActionsLogger();
await t.notThrowsAsync(async () => {
const uploaded = await debugArtifacts.uploadDebugArtifacts(
logger,
["hucairz"],
"i-dont-exist",
"artifactName",
GitHubVariant.DOTCOM,
"2.20.2",
);
t.is(
uploaded,
"upload-not-supported",
"Expected artifact upload to be blocked because of old CodeQL version",
);
});
});

View File

@@ -10,15 +10,11 @@ import { getOptionalInput, getTemporaryDirectory } from "./actions-util";
import { dbIsFinalized } from "./analyze";
import { scanArtifactsForTokens } from "./artifact-scanner";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import type { ActionConfig } from "./config/action-config";
import { EnvVar, exportEnvVar } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger, withGroup } from "./logging";
import {
isSafeArtifactUpload,
SafeArtifactUploadVersion,
} from "./tools-features";
import {
bundleDb,
doesDirectoryExist,
@@ -40,7 +36,6 @@ export function sanitizeArtifactName(name: string): string {
export async function uploadCombinedSarifArtifacts(
logger: Logger,
gitHubVariant: GitHubVariant,
codeQlVersion: string | undefined,
) {
const tempDir = getTemporaryDirectory();
@@ -70,13 +65,12 @@ export async function uploadCombinedSarifArtifacts(
}
try {
await uploadDebugArtifacts(
await uploadArtifacts(
logger,
toUpload,
baseTempDir,
"combined-sarif-artifacts",
gitHubVariant,
codeQlVersion,
);
} catch (e) {
logger.warning(
@@ -95,7 +89,7 @@ export async function uploadCombinedSarifArtifacts(
* @return The path to that debug artifact, or undefined if an error occurs.
*/
function tryPrepareSarifDebugArtifact(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
): string | undefined {
@@ -137,7 +131,7 @@ function tryPrepareSarifDebugArtifact(
*/
async function tryBundleDatabase(
codeql: CodeQL,
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
logger: Logger,
): Promise<string | undefined> {
@@ -163,6 +157,11 @@ async function tryBundleDatabase(
}
}
/** The minimal configuration needed for `tryUploadAllAvailableDebugArtifacts`. */
export type UploadDebugArtifactsConfig = ActionConfig<
"languages" | "gitHubVersion" | "dbLocation" | "debugArtifactName"
>;
/**
* Attempt to upload all available debug artifacts.
*
@@ -170,9 +169,8 @@ async function tryBundleDatabase(
*/
export async function tryUploadAllAvailableDebugArtifacts(
codeql: CodeQL,
config: Config,
config: UploadDebugArtifactsConfig,
logger: Logger,
codeQlVersion: string | undefined,
) {
const filesToUpload: string[] = [];
try {
@@ -231,13 +229,12 @@ export async function tryUploadAllAvailableDebugArtifacts(
try {
await withGroup("Uploading debug artifacts", async () =>
uploadDebugArtifacts(
uploadArtifacts(
logger,
filesToUpload,
config.dbLocation,
config.debugArtifactName,
config.gitHubVersion.type,
codeQlVersion,
),
);
} catch (e) {
@@ -283,26 +280,6 @@ export type UploadArtifactsResult =
| "upload-successful"
| "upload-failed";
export async function uploadDebugArtifacts(
logger: Logger,
toUpload: string[],
rootDir: string,
artifactName: string,
ghVariant: GitHubVariant,
codeQlVersion: string | undefined,
): Promise<UploadArtifactsResult | "upload-not-supported"> {
const uploadSupported = isSafeArtifactUpload(codeQlVersion);
if (!uploadSupported) {
core.info(
`Skipping debug artifact upload because the current CLI does not support safe upload. Please upgrade to CLI v${SafeArtifactUploadVersion} or later.`,
);
return "upload-not-supported";
}
return uploadArtifacts(logger, toUpload, rootDir, artifactName, ghVariant);
}
/**
* Uploads the specified files as a single workflow artifact.
*
@@ -381,7 +358,7 @@ export async function getArtifactUploaderClient(
* all files in the database folder and return the path.
*/
async function createPartialDatabaseBundle(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
): Promise<string> {
const databasePath = getCodeQLDatabasePath(config, language);
@@ -422,7 +399,7 @@ async function createPartialDatabaseBundle(
*/
async function createDatabaseBundleCli(
codeql: CodeQL,
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
): Promise<string> {
const databaseBundlePath = await bundleDb(

111
src/diagnostics.test.ts Normal file
View File

@@ -0,0 +1,111 @@
import * as fs from "fs";
import path from "path";
import test, { type ExecutionContext } from "ava";
import * as diagnostics from "./diagnostics";
import { BuiltInLanguage } from "./languages";
import {
checkExpectedLogMessages,
createTestConfig,
initAllState,
RecordingLogger,
setupTests,
} from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
const id = "codeql-action/test-diagnostic";
const name = "Test title";
test("makeDiagnostic - adds expected properties", (t) => {
const diagnostic = diagnostics.makeDiagnostic(id, name, {});
t.truthy(diagnostic.timestamp);
t.is(diagnostic.source.id, id);
t.is(diagnostic.source.name, name);
});
/**
* Diagnostic files that are written to disk don't have deterministic names.
* This function recursively finds all `.json` files at {@link rootPath},
* asserts that there is exactly one, and that it matches {@link expected}.
*
* @param rootPath The path to start searching from.
*/
function assertSingleDiagnosticIn(
t: ExecutionContext<unknown>,
rootPath: string,
expected: diagnostics.DiagnosticMessage,
) {
// Find all `.json` files in `rootPath` and check that there is exactly one.
const filePaths = fs
.readdirSync(rootPath, {
recursive: true,
encoding: "utf-8",
})
.filter((p) => p.endsWith(".json"));
t.is(filePaths.length, 1);
// Check that the written diagnostic matches the one we added.
const contents = fs.readFileSync(
path.resolve(rootPath, filePaths[0]),
"utf-8",
);
const readDiagnostic = JSON.parse(contents) as diagnostics.DiagnosticMessage;
t.deepEqual(readDiagnostic, expected);
}
test("addDiagnostic writes temporary diagnostics and flushDiagnostics moves them to the database", async (t) =>
withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const tempDiagDir = path.resolve(tmpDir, "temp");
const testDbDir = path.resolve(tmpDir, "test", "database");
const diagnostic = diagnostics.makeDiagnostic(id, name, {});
// Write the diagnostic. The path provided for `dbLocation` doesn't exist,
// so the diagnostic will be written to a temporary location.
t.notThrows(() => {
diagnostics.addDiagnostic(
createTestConfig({ tempDir: tempDiagDir, dbLocation: testDbDir }),
BuiltInLanguage.actions,
diagnostic,
logger,
);
});
// Check that `tempDiagDir` was created.
t.true(fs.existsSync(tempDiagDir));
// Check that the diagnostic exists under `tempDiagDir`.
assertSingleDiagnosticIn(t, tempDiagDir, diagnostic);
// Flush the diagnostics. This will cause the diagnostic we wrote previously to
// be moved to a location under `dbLocation`.
t.notThrows(() => {
diagnostics.flushDiagnostics(
initAllState({ logger }),
createTestConfig({
tempDir: tempDiagDir,
dbLocation: testDbDir,
}),
);
});
// Check that `testDbDir` was created.
t.true(fs.existsSync(testDbDir));
// Check that the diagnostic exists under `testDbDir`.
assertSingleDiagnosticIn(t, testDbDir, diagnostic);
// Check that we logged that the database directory doesn't exist yet
// and that we moved one diagnostic.
checkExpectedLogMessages(t, logger.messages, [
`but the database at ${path.join(testDbDir, "actions")} does not exist yet`,
"Moving 1 diagnostic(s) to their respective databases.",
]);
}));

View File

@@ -1,10 +1,16 @@
import { existsSync, mkdirSync, writeFileSync } from "fs";
import * as fs from "fs";
import path from "path";
import type { ActionState } from "./action-common";
import { getTemporaryDirectory } from "./actions-util";
import type { Config } from "./config-utils";
import { Language } from "./languages";
import { getActionsLogger } from "./logging";
import { getCodeQLDatabasePath } from "./util";
import { getActionsLogger, type Logger } from "./logging";
import {
getCodeQLDatabasePath,
getErrorMessage,
isErrnoException,
} from "./util";
/**
* Known tags for diagnostics. There is currently only "internal-error",
@@ -84,22 +90,16 @@ export type DiagnosticMessage = DiagnosticMessageOptions & {
source: DiagnosticSource;
};
/** Represents a diagnostic message that has not yet been written to the database. */
interface UnwrittenDiagnostic {
/** The diagnostic message that has not yet been written. */
diagnostic: DiagnosticMessage;
/** The language the diagnostic is for. */
language: Language;
/** Represents a diagnostic message that has not yet been saved to the database. */
interface TemporaryDiagnostic {
/** The path to which the diagnostic has temporarily been written to. */
path: string;
/** The language the diagnostic is for, if any. */
language?: Language;
}
/** A list of diagnostics which have not yet been written to disk. */
let unwrittenDiagnostics: UnwrittenDiagnostic[] = [];
/**
* A list of diagnostics which have not yet been written to disk,
* and where the language does not matter.
*/
let unwrittenDefaultLanguageDiagnostics: DiagnosticMessage[] = [];
/** A list of diagnostics which have not yet been saved to the database. */
let temporaryDiagnostics: TemporaryDiagnostic[] = [];
/**
* Counter used to generate a unique suffix for each diagnostic filename, so that
@@ -128,6 +128,29 @@ export function makeDiagnostic(
};
}
/**
* Gets a path relative to {@link tmpDir} where diagnostics for {@link language}
* can temporarily be stored at before the database is initialised. If {@link language}
* is `undefined`, then the common base path for all languages relative to {@link tmpDir}
* is returned.
*/
export function getTempDiagnosticPath(tmpDir: string, language?: Language) {
const root = path.join(tmpDir, "codeql-action-temp-diagnostics");
return getDiagnosticsPath(root, language);
}
/**
* Gets a path relative to {@link basePath} where diagnostics for {@link language}
* should be stored at. If {@link language} is `undefined`, then {@link basePath}
* is returned instead.
*/
export function getDiagnosticsPath(basePath: string, language?: Language) {
if (language !== undefined) {
return path.resolve(basePath, language, "diagnostic", "codeql-action");
}
return basePath;
}
/**
* Adds the given diagnostic to the database. If the database does not yet exist,
* the diagnostic will be written to it once it has been created.
@@ -140,22 +163,23 @@ export function addDiagnostic(
config: Config,
language: Language,
diagnostic: DiagnosticMessage,
logger: Logger = getActionsLogger(),
) {
const logger = getActionsLogger();
const databasePath = language
? getCodeQLDatabasePath(config, language)
: config.dbLocation;
// Check that the database exists before writing to it. If the database does not yet exist,
// store the diagnostic in memory and write it later.
if (existsSync(databasePath)) {
writeDiagnostic(config, language, diagnostic);
if (fs.existsSync(databasePath)) {
writeDiagnostic({ logger }, config, language, diagnostic);
} else {
logger.debug(
`Writing a diagnostic for ${language}, but the database at ${databasePath} does not exist yet.`,
);
unwrittenDiagnostics.push({ diagnostic, language });
// Write the diagnostic to a temporary location.
writeTemporaryDiagnosticFile(logger, config.tempDir, language, diagnostic);
}
}
@@ -163,6 +187,7 @@ export function addDiagnostic(
export function addNoLanguageDiagnostic(
config: Config | undefined,
diagnostic: DiagnosticMessage,
logger: Logger = getActionsLogger(),
) {
if (config !== undefined) {
addDiagnostic(
@@ -173,10 +198,82 @@ export function addNoLanguageDiagnostic(
diagnostic,
);
} else {
unwrittenDefaultLanguageDiagnostics.push(diagnostic);
logger.debug(
`Writing a diagnostic not associated with any language, but we have no configuration to tell us where the database is yet.`,
);
// Write the diagnostic to a temporary location.
writeTemporaryDiagnosticFile(
logger,
getTemporaryDirectory(),
undefined,
diagnostic,
);
}
}
/**
* Writes {@link diagnostic} for an optional {@link language} to a subdirectory of {@link tempDir}
* and tracks it as a diagnostic that was written to a temporary location.
*/
function writeTemporaryDiagnosticFile(
logger: Logger,
tempDir: string,
language: Language | undefined,
diagnostic: DiagnosticMessage,
) {
const tempDiagnosticsPath = getTempDiagnosticPath(tempDir, language);
try {
// Write the diagnostic to a temporary location.
const diagnosticPath = writeDiagnosticFile(tempDiagnosticsPath, diagnostic);
// Track the temporary file.
temporaryDiagnostics.push({ path: diagnosticPath, language });
} catch (err) {
logger.warning(
`Unable to write temporary diagnostic to '${tempDiagnosticsPath}': ${getErrorMessage(err)}\n\n${JSON.stringify(diagnostic)}`,
);
}
}
/**
* Writes {@link diagnostic} to a file in {@link diagnosticsPath}.
*/
function writeDiagnosticFile(
diagnosticsPath: string,
diagnostic: DiagnosticMessage,
): string {
// Create the directory if it doesn't exist yet.
fs.mkdirSync(diagnosticsPath, { recursive: true });
// Include a monotonically increasing suffix to avoid filename collisions
// between diagnostics produced within the same millisecond.
const uniqueSuffix = (diagnosticCounter++).toString();
// We should only need to remove colons, but to be defensive, only allow a restricted set of
// characters.
const sanitizedTimestamp = diagnostic.timestamp.replace(
/[^a-zA-Z0-9.-]/g,
"",
);
const jsonPath = path.resolve(
diagnosticsPath,
`codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json`,
);
fs.writeFileSync(jsonPath, JSON.stringify(diagnostic));
return jsonPath;
}
/** Gets the path where diagnostics for {@link language} should be stored in the database. */
export function getDatabaseDiagnosticsPath(
config: Config,
language: Language | undefined,
) {
return getDiagnosticsPath(config.dbLocation, language);
}
/**
* Writes the given diagnostic to the database.
*
@@ -185,78 +282,85 @@ export function addNoLanguageDiagnostic(
* @param diagnostic The diagnostic message to add to the database.
*/
function writeDiagnostic(
action: ActionState<["Logger"]>,
config: Config,
language: Language | undefined,
diagnostic: DiagnosticMessage,
) {
const logger = getActionsLogger();
const databasePath = language
? getCodeQLDatabasePath(config, language)
: config.dbLocation;
const diagnosticsPath = path.resolve(
databasePath,
"diagnostic",
"codeql-action",
);
const diagnosticsPath = getDatabaseDiagnosticsPath(config, language);
try {
// Create the directory if it doesn't exist yet.
mkdirSync(diagnosticsPath, { recursive: true });
// Include a monotonically increasing suffix to avoid filename collisions
// between diagnostics produced within the same millisecond.
const uniqueSuffix = (diagnosticCounter++).toString();
// We should only need to remove colons, but to be defensive, only allow a restricted set of
// characters.
const sanitizedTimestamp = diagnostic.timestamp.replace(
/[^a-zA-Z0-9.-]/g,
"",
);
const jsonPath = path.resolve(
diagnosticsPath,
`codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json`,
);
writeFileSync(jsonPath, JSON.stringify(diagnostic));
writeDiagnosticFile(diagnosticsPath, diagnostic);
} catch (err) {
logger.warning(`Unable to write diagnostic message to database: ${err}`);
logger.debug(JSON.stringify(diagnostic));
action.logger.warning(
`Unable to write diagnostic message to database: ${err}`,
);
action.logger.debug(JSON.stringify(diagnostic));
}
}
/** Report if there are unwritten diagnostics and write them to the log. */
export function logUnwrittenDiagnostics() {
const logger = getActionsLogger();
const num = unwrittenDiagnostics.length;
/** Report if there are temporary diagnostics and write them to the log. */
export function logTemporaryDiagnostics(action: ActionState<["Logger"]>) {
const num = temporaryDiagnostics.length;
if (num > 0) {
logger.warning(
action.logger.warning(
`${num} diagnostic(s) could not be written to the database and will not appear on the Tool Status Page.`,
);
for (const unwritten of unwrittenDiagnostics) {
logger.debug(JSON.stringify(unwritten.diagnostic));
for (const temporary of temporaryDiagnostics) {
action.logger.debug(fs.readFileSync(temporary.path, "utf-8"));
}
}
}
/** Writes all unwritten diagnostics to disk. */
export function flushDiagnostics(config: Config) {
const logger = getActionsLogger();
/** Relocates all temporary diagnostics to the respective databases. */
export function flushDiagnostics(
action: ActionState<["Logger"]>,
config: Config,
) {
const diagnosticsCount = temporaryDiagnostics.length;
action.logger.debug(
`Moving ${diagnosticsCount} diagnostic(s) to their respective databases.`,
);
const diagnosticsCount =
unwrittenDiagnostics.length + unwrittenDefaultLanguageDiagnostics.length;
logger.debug(`Writing ${diagnosticsCount} diagnostic(s) to database.`);
const failedFlushDiagnostics: TemporaryDiagnostic[] = [];
for (const temporary of temporaryDiagnostics) {
// If `temporary.language` is `undefined`, then we didn't have a `config` at the time that
// `addNoLanguageDiagnostic` was called. In that case, we arbitrarily choose the first
// configured language here like we would have done in `addNoLanguageDiagnostic`.
const directory = getDatabaseDiagnosticsPath(
config,
temporary.language ?? config.languages[0],
);
const filename = path.basename(temporary.path);
const destination = path.join(directory, filename);
for (const unwritten of unwrittenDiagnostics) {
writeDiagnostic(config, unwritten.language, unwritten.diagnostic);
}
for (const unwritten of unwrittenDefaultLanguageDiagnostics) {
addNoLanguageDiagnostic(config, unwritten);
try {
fs.mkdirSync(directory, { recursive: true });
try {
fs.renameSync(temporary.path, destination);
} catch (renameErr: unknown) {
// Rethrow the error if it isn't an EXDEV one.
if (!isErrnoException(renameErr) || renameErr.code !== "EXDEV") {
throw renameErr;
}
// The error was an EXDEV one: try to copy the file and delete the temporary one.
fs.copyFileSync(temporary.path, destination);
fs.unlinkSync(temporary.path);
}
} catch (err) {
action.logger.warning(
`Failed to relocate '${temporary.path}' to '${destination}': ${getErrorMessage(err)}`,
);
failedFlushDiagnostics.push(temporary);
}
}
// Reset the unwritten diagnostics arrays.
unwrittenDiagnostics = [];
unwrittenDefaultLanguageDiagnostics = [];
// Reset the temporary diagnostics arrays.
temporaryDiagnostics = failedFlushDiagnostics;
}
/**

View File

@@ -240,7 +240,7 @@ export const featureConfig = {
defaultValue: false,
envVar: "CODEQL_EXTRACTOR_CPP_AUTOINSTALL_DEPENDENCIES",
legacyApi: true,
minimumVersion: "2.15.0",
minimumVersion: undefined,
},
[Feature.CsharpCacheBuildModeNone]: {
defaultValue: false,

View File

@@ -1,3 +1,6 @@
import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
@@ -8,6 +11,7 @@ import * as apiClient from "./api-client";
import * as codeql from "./codeql";
import * as configUtils from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import { getTempDiagnosticPath } from "./diagnostics";
import { EnvVar } from "./environment";
import { Feature } from "./feature-flags";
import * as initActionPostHelper from "./init-action-post-helper";
@@ -35,6 +39,47 @@ const NUM_BYTES_PER_GIB = 1024 * 1024 * 1024;
setupTests(test);
test.serial("generateFailedSarif picks up unwritten diagnostics", async (t) =>
util.withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const tempDiagDir = getTempDiagnosticPath(tmpDir);
fs.mkdirSync(tempDiagDir);
const codeqlObject = await codeql.getCodeQLForTesting();
const diagnosticsExport = sinon
.stub(codeqlObject, "diagnosticsExport")
.resolves();
sinon.stub(codeql, "getCodeQL").resolves(codeqlObject);
const sarifFile = path.join(tmpDir, "failed.sarif");
const result = await initActionPostHelper.generateFailedSarif(
logger,
createFeatures([]),
createTestConfig({
tempDir: tmpDir,
// Explicitly undefined to ensure we get the "no database" path.
dbLocation: undefined,
}),
"category",
"",
sarifFile,
);
t.truthy(result);
t.is(diagnosticsExport.callCount, 1);
t.true(
diagnosticsExport.calledOnceWith(
sarifFile,
"category",
sinon.match.any,
tempDiagDir,
),
);
}),
);
test.serial("init-post action with debug mode off", async (t) => {
return await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);

View File

@@ -7,6 +7,11 @@ import * as actionsUtil from "./actions-util";
import { CodeScanning, RiskAssessment } from "./analyses";
import { getApiClient, getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import {
hasConfigKey,
type AbortedInitConfig,
type ActionConfig,
} from "./config/action-config";
import {
Config,
isCodeScanningEnabled,
@@ -16,9 +21,11 @@ import {
getArtifactSuffix,
getArtifactUploaderClient,
sanitizeArtifactName,
type UploadDebugArtifactsConfig,
} from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching";
import { EnvVar, ReadOnlyEnv } from "./environment";
import { getTempDiagnosticPath } from "./diagnostics";
import { EnvVar, isInTestMode, ReadOnlyEnv } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
@@ -86,6 +93,11 @@ interface FailedSarifInfo {
checkoutPath: string;
}
/** The minimal Action configuration required to prepare and upload a failed SARIF. */
export type FailedSarifConfig = ActionConfig<
"codeQLCmd" | "tempDir" | "analysisKinds" | "languages"
>;
/**
* Tries to prepare a SARIF file that contains information about a failed analysis.
*
@@ -94,7 +106,7 @@ interface FailedSarifInfo {
async function prepareFailedSarif(
logger: Logger,
features: FeatureEnablement,
config: Config,
config: FailedSarifConfig,
): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> {
if (!config.codeQLCmd) {
return new Failure({
@@ -157,10 +169,19 @@ async function prepareFailedSarif(
}
}
async function generateFailedSarif(
/**
* Generates a SARIF file for a failed run, i.e. an analysis where no SARIF was produced as a result of the analysis.
*
* Not intended to be called directly, other than by `prepareFailedSarif`.
*
* @param category The analysis category to include in the SARIF.
* @param checkoutPath The path where the repository was checked out at.
* @param sarifFile The SARIF file to produce.
*/
export async function generateFailedSarif(
logger: Logger,
features: FeatureEnablement,
config: Config,
config: ActionConfig<"codeQLCmd" | "tempDir">,
category: string | undefined,
checkoutPath: string,
sarifFile?: string,
@@ -178,7 +199,19 @@ async function generateFailedSarif(
databasePath === undefined ||
!(await features.getValue(Feature.ExportDiagnosticsEnabled, codeql))
) {
await codeql.diagnosticsExport(sarifFile, category, config);
// If we don't have a database path, then we may have diagnostics that were written to a
// temporary location. Get the path that they would have been written to and check whether
// it exists. If so, pass it on to the CLI. The CLI will not complain if we give it a
// path that doesn't exist, so we are just being extra defensive here.
let diagnosticDir: string | undefined = getTempDiagnosticPath(
config.tempDir,
);
if (!fs.existsSync(diagnosticDir)) {
diagnosticDir = undefined;
}
await codeql.diagnosticsExport(sarifFile, category, config, diagnosticDir);
} else {
// We call 'database export-diagnostics' to find any per-database diagnostics.
await codeql.databaseExportDiagnostics(databasePath, sarifFile, category);
@@ -192,7 +225,7 @@ async function generateFailedSarif(
* category for the workflow.
*/
async function maybeUploadFailedSarif(
config: Config,
config: FailedSarifConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
@@ -220,6 +253,19 @@ async function maybeUploadFailedSarif(
logger,
{ isUnsuccessfulExecution: true },
);
// If we are in test mode, `uploadFiles` will not have uploaded the failed SARIF file
// and instead will have written it to disk. Upload it as a workflow artifact so that
// we can inspect it in validation jobs.
if (uploadResult.sarifID && isInTestMode()) {
await uploadSarifArtifact(logger, "test-mode-sarif", {
// We defaulted to CodeScanning in the `uploadFiles` call above.
sarifFile: uploadLib.getSavedPayloadPath(CodeScanning.kind),
category: failedSarif.category,
checkoutPath: failedSarif.checkoutPath,
});
}
return uploadResult
? { ...uploadResult.statusReport, sarifID: uploadResult.sarifID }
: {};
@@ -227,7 +273,7 @@ async function maybeUploadFailedSarif(
/** Uploads a failed SARIF file as workflow artifact, if it can be generated. */
async function maybeUploadFailedSarifArtifact(
config: Config,
config: FailedSarifConfig,
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
@@ -238,7 +284,15 @@ async function maybeUploadFailedSarifArtifact(
}
const failedSarif = failedSarifResult.value;
return uploadSarifArtifact(logger, "sarif-artifact", failedSarif);
}
/** Uploads a failed SARIF file as workflow artifact. */
async function uploadSarifArtifact(
logger: Logger,
prefix: string,
failedSarif: FailedSarifInfo,
): Promise<UploadFailedSarifResult> {
logger.info(
`Uploading failed SARIF file ${failedSarif.sarifFile} as artifact`,
);
@@ -247,7 +301,7 @@ async function maybeUploadFailedSarifArtifact(
const client = await getArtifactUploaderClient(logger, gitHubVersion.type);
const suffix = getArtifactSuffix(actionsUtil.getOptionalInput("matrix"));
const name = sanitizeArtifactName(`sarif-artifact-${suffix}`);
const name = sanitizeArtifactName(`${prefix}-${suffix}`);
await client.uploadArtifact(
name,
[path.normalize(failedSarif.sarifFile)],
@@ -267,7 +321,7 @@ async function maybeUploadFailedSarifArtifact(
* @returns The results of uploading the SARIF file for the failure.
*/
export async function tryUploadSarifIfRunFailed(
config: Config,
config: FailedSarifConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
@@ -324,13 +378,12 @@ export async function tryUploadSarifIfRunFailed(
export async function uploadFailureInfo(
uploadAllAvailableDebugArtifacts: (
codeql: CodeQL,
config: Config,
config: UploadDebugArtifactsConfig,
logger: Logger,
codeQlVersion: string,
) => Promise<void>,
printDebugLogs: (config: Config) => Promise<void>,
printDebugLogs: (config: Partial<Config>) => Promise<void>,
codeql: CodeQL,
config: Config,
config: Config | AbortedInitConfig,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
jobStatus: string | undefined,
@@ -377,21 +430,15 @@ export async function uploadFailureInfo(
}
// Upload appropriate Actions artifacts for debugging
if (config.debugMode) {
if (config.debugMode && hasConfigKey(config, "dbLocation")) {
logger.info(
"Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts...",
);
const version = await codeql.getVersion();
await uploadAllAvailableDebugArtifacts(
codeql,
config,
logger,
version.version,
);
await uploadAllAvailableDebugArtifacts(codeql, config, logger);
await printDebugLogs(config);
}
if (actionsUtil.isSelfHostedRunner()) {
if (actionsUtil.isSelfHostedRunner() && config.dbLocation) {
try {
fs.rmSync(config.dbLocation, {
recursive: true,
@@ -454,7 +501,7 @@ function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
*/
async function recordOverlayStatus(
codeql: CodeQL,
config: Config,
config: Partial<Config>,
features: FeatureEnablement,
jobStatus: string | undefined,
env: ReadOnlyEnv,
@@ -463,7 +510,8 @@ async function recordOverlayStatus(
if (
config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase ||
env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" ||
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
!(await features.getValue(Feature.OverlayAnalysisStatusSave)) ||
config.languages === undefined
) {
return;
}

View File

@@ -6,6 +6,7 @@
import * as core from "@actions/core";
import { runInActions, type Action, type ActionState } from "./action-common";
import {
restoreInputs,
getOptionalInput,
@@ -15,22 +16,25 @@ import {
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CachingKind } from "./caching-utils";
import { getCodeQL } from "./codeql";
import { type Config, getConfig } from "./config-utils";
import {
hasConfigKey,
type AbortedInitConfig,
type Config,
} from "./config/action-config";
import { getAbortedInitConfig, getConfig } from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import {
DependencyCachingUsageReport,
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar, getEnv, exportEnvVar } from "./environment";
import { EnvVar, type ReadOnlyEnv } from "./environment";
import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
import { getActionsLogger } from "./logging";
import { getRepositoryNwo } from "./repository";
import {
StatusReportBase,
sendStatusReport,
sendUnhandledErrorStatusReport,
createStatusReportBase,
getActionsStatus,
ActionName,
@@ -45,16 +49,17 @@ interface InitPostStatusReport
initActionPostHelper.JobStatusReport,
initActionPostHelper.DependencyCachingUsageReport {}
async function run(startedAt: Date) {
async function run(action: ActionState<["Base", "Logger", "Env"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = action.logger;
const logger = getActionsLogger();
let config: Config | undefined;
let config: Config | AbortedInitConfig | undefined;
let uploadFailedSarifResult:
| initActionPostHelper.UploadFailedSarifResult
| undefined;
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
try {
// Read the job status before restoring inputs, since it is provided by the Actions runtime
// environment for this step and would otherwise be overwritten by the value that the `init`
@@ -64,7 +69,18 @@ async function run(startedAt: Date) {
// Restore inputs from `init` Action.
restoreInputs(logger);
config = await getConfig(getTemporaryDirectory(), logger);
const tmpDir = getTemporaryDirectory();
config = await getConfig(tmpDir, logger);
// If we weren't able to read the normal configuration file, the `init` step failed before it could write it.
// Try to load the "failed" configuration file instead which may have been written by the `init` step
// with a minimal configuration for this post-action.
if (config === undefined) {
logger.debug(
"Will try to load the configuration file from an aborted 'init' step, since there is no regular one.",
);
config = getAbortedInitConfig(action, tmpDir);
}
const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
checkGitHubVersionInRange(gitHubVersion, logger);
@@ -77,7 +93,7 @@ async function run(startedAt: Date) {
logger,
);
if (config === undefined) {
if (config?.codeQLCmd === undefined) {
logger.warning(
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
);
@@ -92,7 +108,7 @@ async function run(startedAt: Date) {
repositoryNwo,
features,
jobStatus,
getEnv(),
action.env,
logger,
);
@@ -102,6 +118,7 @@ async function run(startedAt: Date) {
// and where caching may not be enabled.
if (
(await gitUtils.isAnalyzingDefaultBranch()) &&
hasConfigKey(config, "dependencyCachingEnabled") &&
config.dependencyCachingEnabled !== CachingKind.None
) {
dependencyCachingUsage = await getDependencyCacheUsage(logger);
@@ -112,9 +129,9 @@ async function run(startedAt: Date) {
core.setFailed(error.message);
const statusReportBase = await createStatusReportBase(
ActionName.InitPost,
action.name,
getActionsStatus(error),
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -126,13 +143,13 @@ async function run(startedAt: Date) {
}
return;
}
const jobStatus = getFinalJobStatus(config);
const jobStatus = getFinalJobStatus(action, config);
logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
const statusReportBase = await createStatusReportBase(
ActionName.InitPost,
action.name,
"success",
startedAt,
action.startedAt,
config,
await checkDiskUsage(logger),
logger,
@@ -157,16 +174,21 @@ async function run(startedAt: Date) {
* Otherwise, we determine the job status based on whether the analyze step
* completed successfully and whether we have a valid CodeQL config.
*/
function getFinalJobStatus(config: Config | undefined): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment();
function getFinalJobStatus(
action: ActionState<["Env"]>,
config: Partial<Config> | undefined,
): JobStatus {
const existingJobStatus = getJobStatusFromEnvironment(action.env);
if (existingJobStatus !== undefined) {
return existingJobStatus;
}
let jobStatus: JobStatus;
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
exportEnvVar(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
if (
action.env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true"
) {
action.env.export(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
jobStatus = JobStatus.SuccessStatus;
} else if (config !== undefined) {
// - We have computed a CodeQL config
@@ -191,7 +213,7 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
// This shouldn't be necessary, but in the odd case that we run more than one
// `init` post step, ensure the job status is consistent between them.
exportEnvVar(EnvVar.JOB_STATUS, jobStatus);
action.env.export(EnvVar.JOB_STATUS, jobStatus);
return jobStatus;
}
@@ -200,8 +222,8 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
*
* If the job status is invalid, return `UnknownStatus`.
*/
function getJobStatusFromEnvironment(): JobStatus | undefined {
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS];
function getJobStatusFromEnvironment(env: ReadOnlyEnv): JobStatus | undefined {
const jobStatusFromEnvironment = env.getOptional(EnvVar.JOB_STATUS);
if (jobStatusFromEnvironment !== undefined) {
// Validate the job status from the environment. If it is invalid, return unknown.
@@ -216,18 +238,11 @@ function getJobStatusFromEnvironment(): JobStatus | undefined {
return undefined;
}
const initPost: Action = {
name: ActionName.InitPost,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`init post action failed: ${wrapError(error).message}`);
await sendUnhandledErrorStatusReport(
ActionName.InitPost,
startedAt,
error,
logger,
);
}
return await runInActions(initPost);
}

View File

@@ -40,7 +40,7 @@ import {
addDiagnostic,
addNoLanguageDiagnostic,
flushDiagnostics,
logUnwrittenDiagnostics,
logTemporaryDiagnostics,
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
@@ -355,7 +355,7 @@ async function run(
// to the workflow log. No exceptions are thrown.
await checkWorkflow(logger, codeql);
// Set CODEQL_ENABLE_EXPERIMENTAL_FEATURES for Rust if between 2.19.3 (included) and 2.22.1 (excluded)
// Set CODEQL_ENABLE_EXPERIMENTAL_FEATURES for Rust if the CLI is older than 2.22.1.
// We need to set this environment variable before initializing the config, otherwise Rust
// analysis will not be enabled (experimental language packs are only active with that environment
// variable set to `true`).
@@ -366,14 +366,8 @@ async function run(
.getRawLanguagesNoAutodetect(getOptionalInput("languages"))
.includes(BuiltInLanguage.rust)
) {
const experimental = "2.19.3";
const publicPreview = "2.22.1";
const actualVer = (await codeql.getVersion()).version;
if (semver.lt(actualVer, experimental)) {
throw new ConfigurationError(
`Rust analysis is supported by CodeQL CLI version ${experimental} or higher, but found version ${actualVer}`,
);
}
if (semver.lt(actualVer, publicPreview)) {
actionState.env.export(EnvVar.EXPERIMENTAL_FEATURES, "true");
logger.info("Experimental Rust analysis enabled");
@@ -463,6 +457,18 @@ async function run(
await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) {
// If we have initialised the Action state, then try to save a subset of it for use by the `init-post` action
// so that it can try to create and upload additional diagnostic information.
if (config !== undefined) {
try {
configUtils.saveAbortedInitConfig(actionState, config);
} catch (configSaveError) {
logger.warning(
`Unable to save configuration for '${ActionName.InitPost}' step: ${getErrorMessage(configSaveError)}`,
);
}
}
const error = wrapError(unwrappedError);
core.setFailed(error.message);
const statusReportBase = await createStatusReportBase(
@@ -478,6 +484,7 @@ async function run(
if (statusReportBase !== undefined) {
await sendStatusReport(statusReportBase);
}
logTemporaryDiagnostics(actionState);
return;
}
@@ -639,15 +646,6 @@ async function run(
actionState.env.export("CODEQL_GIT_BACKEND", "jgit");
}
const kotlinLimitVar =
"CODEQL_EXTRACTOR_KOTLIN_OVERRIDE_MAXIMUM_VERSION_LIMIT";
if (
(await codeQlVersionAtLeast(codeql, "2.20.3")) &&
!(await codeQlVersionAtLeast(codeql, "2.20.4"))
) {
actionState.env.export(kotlinLimitVar, "2.1.20");
}
// Restore dependency cache(s), if they exist.
if (shouldRestoreCache(config.dependencyCachingEnabled)) {
const dependencyCachingResult = await downloadDependencyCaches(
@@ -769,9 +767,9 @@ async function run(
);
}
// Write diagnostics to the database that we previously stored in memory because the database
// did not exist until now.
flushDiagnostics(config);
// Move diagnostics to the database that we previously kept in a temporary location
// because the database did not exist until now
flushDiagnostics(actionState, config);
// We save the config here instead of at the end of `initConfig` because we
// may have updated the config returned from `initConfig`, e.g. to revert to
@@ -800,7 +798,7 @@ async function run(
);
return;
} finally {
logUnwrittenDiagnostics();
logTemporaryDiagnostics(actionState);
}
await sendCompletedStatusReport(
startedAt,

View File

@@ -1647,16 +1647,20 @@ test.serial(
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
]);
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result =
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
]);
});
},
);
@@ -1773,21 +1777,25 @@ test.serial(
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersionDryRun]),
getRunnerLogger(true),
);
t.deepEqual(
result,
[],
"Dry-run should return an empty list so the caller falls back.",
);
t.assert(
listStub.calledOnce,
"Dry-run should still list Actions caches to populate the diagnostic.",
);
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result =
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersionDryRun]),
getRunnerLogger(true),
);
t.deepEqual(
result,
[],
"Dry-run should return an empty list so the caller falls back.",
);
t.assert(
listStub.calledOnce,
"Dry-run should still list Actions caches to populate the diagnostic.",
);
});
},
);
@@ -1801,18 +1809,22 @@ test.serial(
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([
Feature.OverlayAnalysisMatchCodeqlVersion,
Feature.OverlayAnalysisMatchCodeqlVersionDryRun,
]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
]);
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result =
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([
Feature.OverlayAnalysisMatchCodeqlVersion,
Feature.OverlayAnalysisMatchCodeqlVersionDryRun,
]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
]);
});
},
);

View File

@@ -61,13 +61,21 @@ export enum ActionName {
UploadSarif = "upload-sarif",
}
/** A partial mapping from `ActionName` to names for actions whose display name is not the `ActionName` value. */
export const ACTION_DISPLAY_NAMES: Partial<Record<ActionName, string>> = {
[ActionName.Analyze]: "analyze",
[ActionName.InitPost]: "init post",
};
/**
* Maps an `ActionName` to its display name. Usually that is the same, except
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
* Maps an {@link ActionName} to its display name. Usually that is the same, except
* for actions which have a different display name in {@link ACTION_DISPLAY_NAMES}.
*/
export function getDisplayActionName(actionName: ActionName): string {
if (actionName === ActionName.Analyze) {
return "analyze";
const displayName = ACTION_DISPLAY_NAMES[actionName];
if (displayName !== undefined) {
return displayName;
}
return actionName;
}

View File

@@ -1,5 +1,3 @@
import * as semver from "semver";
import type { VersionInfo } from "./cli/types";
export enum ToolsFeature {
@@ -23,21 +21,3 @@ export function isSupportedToolsFeature(
): boolean {
return !!versionInfo.features && versionInfo.features[feature];
}
export const SafeArtifactUploadVersion = "2.20.3";
/**
* The first version of the CodeQL CLI where artifact upload is safe to use
* for failed runs. This is not really a feature flag, but it is easiest to
* model the behavior as a feature flag.
*
* This was not captured in a tools feature, so we need to use semver.
*
* @param codeQlVersion The version of the CodeQL CLI to check. If not provided, it is assumed to be safe.
* @returns True if artifact upload is safe to use for failed runs or false otherwise.
*/
export function isSafeArtifactUpload(codeQlVersion?: string): boolean {
return !codeQlVersion
? true
: semver.gte(codeQlVersion, SafeArtifactUploadVersion);
}

View File

@@ -227,6 +227,14 @@ function getAutomationID(
return api.computeAutomationID(analysis_key, environment);
}
/** Gets the path to a saved payload file for {@link analysisKind}. */
export function getSavedPayloadPath(analysisKind: analyses.AnalysisKind) {
return path.join(
actionsUtil.getTemporaryDirectory(),
`payload-${analysisKind}.json`,
);
}
/**
* Upload the given payload.
* If the request fails then this will retry a small number of times.
@@ -241,10 +249,7 @@ export async function uploadPayload(
logger.info("Uploading results");
if (util.shouldSkipSarifUpload()) {
const payloadSaveFile = path.join(
actionsUtil.getTemporaryDirectory(),
`payload-${analysis.kind}.json`,
);
const payloadSaveFile = getSavedPayloadPath(analysis.kind);
logger.info(
`SARIF upload disabled by an environment variable. Saving to ${payloadSaveFile}`,
);

View File

@@ -34,13 +34,7 @@ export async function runWrapper() {
return;
}
await withGroup("Uploading combined SARIF debug artifact", () =>
debugArtifacts.uploadCombinedSarifArtifacts(
logger,
gitHubVersion.type,
// The codeqlVersion is not applicable for uploading non-codeql sarif.
// We can assume all versions are safe to upload.
undefined,
),
debugArtifacts.uploadCombinedSarifArtifacts(logger, gitHubVersion.type),
);
}
} catch (error) {

View File

@@ -12,6 +12,7 @@ import * as semver from "semver";
import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL } from "./codeql";
import type { ActionConfig } from "./config/action-config";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import {
@@ -445,7 +446,10 @@ export function getThreadsFlag(
/**
* Get the path where the CodeQL database for the given language lives.
*/
export function getCodeQLDatabasePath(config: Config, language: Language) {
export function getCodeQLDatabasePath(
config: ActionConfig<"dbLocation">,
language: Language,
) {
return path.resolve(config.dbLocation, language);
}
@@ -639,7 +643,9 @@ export async function codeQlVersionAtLeast(
return semver.gte((await codeql.getVersion()).version, requiredVersion);
}
export function getBaseDatabaseOidsFilePath(config: Config): string {
export function getBaseDatabaseOidsFilePath(
config: ActionConfig<"dbLocation">,
): string {
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
}
@@ -651,7 +657,7 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
* given `includeDiagnostics` value.
*/
export async function bundleDb(
config: Config,
config: ActionConfig<"dbLocation">,
language: Language,
codeql: CodeQL,
dbName: string,
@@ -918,6 +924,21 @@ export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Approximates whether {@link error} is like a {@link NodeJS.ErrnoException}.
* We only typically care about the `code` property, so that's what we check for here.
*/
export function isErrnoException(
error: unknown,
): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string"
);
}
export function prettyPrintPack(pack: Pack) {
return `${pack.name}${pack.version ? `@${pack.version}` : ""}${
pack.path ? `:${pack.path}` : ""