Merge branch 'main' into henrymercer/skip-file-coverage-rollout

This commit is contained in:
Henry Mercer
2026-03-10 16:25:21 +00:00
5 changed files with 1054 additions and 593 deletions

1184
lib/init-action-post.js generated

File diff suppressed because it is too large Load Diff

View File

@@ -1555,6 +1555,13 @@ export function isCodeQualityEnabled(config: Config): 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 {
return config.analysisKinds.includes(AnalysisKind.RiskAssessment);
}
/**
* Returns the primary analysis kind that the Action is initialised with. If there is only
* one analysis kind, then that is returned.

View File

@@ -1,10 +1,13 @@
import * as core from "@actions/core";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client";
import * as codeql from "./codeql";
import * as configUtils from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import { EnvVar } from "./environment";
import { Feature } from "./feature-flags";
import * as initActionPostHelper from "./init-action-post-helper";
@@ -17,6 +20,7 @@ import {
createTestConfig,
DEFAULT_ACTIONS_VARS,
makeVersionInfo,
RecordingLogger,
setupActionsVars,
setupTests,
} from "./testing-utils";
@@ -46,7 +50,7 @@ test.serial("init-post action with debug mode off", async (t) => {
const uploadAllAvailableDebugArtifactsSpy = sinon.spy();
const printDebugLogsSpy = sinon.spy();
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
uploadAllAvailableDebugArtifactsSpy,
printDebugLogsSpy,
codeql.createStubCodeQL({}),
@@ -68,7 +72,7 @@ test.serial("init-post action with debug mode on", async (t) => {
const uploadAllAvailableDebugArtifactsSpy = sinon.spy();
const printDebugLogsSpy = sinon.spy();
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
uploadAllAvailableDebugArtifactsSpy,
printDebugLogsSpy,
codeql.createStubCodeQL({}),
@@ -334,7 +338,7 @@ test.serial(
});
t.is(
result.upload_failed_run_skipped_because,
"Code Scanning is not enabled.",
"No analysis kind that supports failed SARIF uploads is enabled.",
);
},
);
@@ -359,7 +363,7 @@ test.serial(
const stubCodeQL = codeql.createStubCodeQL({});
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
sinon.spy(),
sinon.spy(),
stubCodeQL,
@@ -427,7 +431,7 @@ test.serial(
.stub(overlayStatus, "saveOverlayStatus")
.resolves(true);
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
sinon.spy(),
sinon.spy(),
codeql.createStubCodeQL({}),
@@ -464,7 +468,7 @@ test.serial("does not save overlay status when build successful", async (t) => {
.stub(overlayStatus, "saveOverlayStatus")
.resolves(true);
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
sinon.spy(),
sinon.spy(),
codeql.createStubCodeQL({}),
@@ -501,7 +505,7 @@ test.serial(
.stub(overlayStatus, "saveOverlayStatus")
.resolves(true);
await initActionPostHelper.run(
await initActionPostHelper.uploadFailureInfo(
sinon.spy(),
sinon.spy(),
codeql.createStubCodeQL({}),
@@ -658,3 +662,197 @@ async function testFailedSarifUpload(
}
return result;
}
const singleLanguageMatrix = JSON.stringify({
language: "javascript",
category: "/language:javascript",
"build-mode": "none",
runner: "ubuntu-latest",
});
async function mockRiskAssessmentEnv(matrix: string) {
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] = "false";
process.env["GITHUB_JOB"] = "analyze";
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["GITHUB_WORKSPACE"] =
"/home/runner/work/codeql-action-fake-repository/codeql-action-fake-repository";
sinon
.stub(apiClient, "getGitHubVersion")
.resolves({ type: util.GitHubVariant.GHES, version: "3.0.0" });
const codeqlObject = await codeql.getCodeQLForTesting();
const databaseExportDiagnostics = sinon
.stub(codeqlObject, "databaseExportDiagnostics")
.resolves();
const diagnosticsExport = sinon
.stub(codeqlObject, "diagnosticsExport")
.resolves();
sinon.stub(codeql, "getCodeQL").resolves(codeqlObject);
sinon.stub(core, "getInput").withArgs("matrix").returns(matrix);
const uploadArtifact = sinon.stub().resolves();
const artifactClient = { uploadArtifact };
sinon
.stub(debugArtifacts, "getArtifactUploaderClient")
.value(() => artifactClient);
return { uploadArtifact, databaseExportDiagnostics, diagnosticsExport };
}
test.serial(
"tryUploadSarifIfRunFailed - uploads as artifact for risk assessments (diagnosticsExport)",
async (t) => {
const logger = new RecordingLogger();
const { uploadArtifact, databaseExportDiagnostics, diagnosticsExport } =
await mockRiskAssessmentEnv(singleLanguageMatrix);
const config = createTestConfig({
analysisKinds: [AnalysisKind.RiskAssessment],
codeQLCmd: "codeql-for-testing",
languages: ["javascript"],
});
const features = createFeatures([]);
const result = await initActionPostHelper.tryUploadSarifIfRunFailed(
config,
parseRepositoryNwo("github/codeql-action-fake-repository"),
features,
logger,
);
const expectedName = debugArtifacts.sanitizeArtifactName(
`sarif-artifact-${debugArtifacts.getArtifactSuffix(singleLanguageMatrix)}`,
);
const expectedFilePattern = /codeql-failed-sarif-javascript\.csra\.sarif$/;
t.is(result.upload_failed_run_skipped_because, undefined);
t.is(result.upload_failed_run_error, undefined);
t.is(result.sarifID, expectedName);
t.assert(
uploadArtifact.calledOnceWith(
expectedName,
[sinon.match(expectedFilePattern)],
sinon.match.string,
),
);
t.assert(databaseExportDiagnostics.notCalled);
t.assert(
diagnosticsExport.calledOnceWith(
sinon.match(expectedFilePattern),
"/language:javascript",
config,
),
);
},
);
test.serial(
"tryUploadSarifIfRunFailed - uploads as artifact for risk assessments (databaseExportDiagnostics)",
async (t) => {
const logger = new RecordingLogger();
const { uploadArtifact, databaseExportDiagnostics, diagnosticsExport } =
await mockRiskAssessmentEnv(singleLanguageMatrix);
const dbLocation = "/some/path";
const config = createTestConfig({
analysisKinds: [AnalysisKind.RiskAssessment],
codeQLCmd: "codeql-for-testing",
languages: ["javascript"],
dbLocation: "/some/path",
});
const features = createFeatures([Feature.ExportDiagnosticsEnabled]);
const result = await initActionPostHelper.tryUploadSarifIfRunFailed(
config,
parseRepositoryNwo("github/codeql-action-fake-repository"),
features,
logger,
);
const expectedName = debugArtifacts.sanitizeArtifactName(
`sarif-artifact-${debugArtifacts.getArtifactSuffix(singleLanguageMatrix)}`,
);
const expectedFilePattern = /codeql-failed-sarif-javascript\.csra\.sarif$/;
t.is(result.upload_failed_run_skipped_because, undefined);
t.is(result.upload_failed_run_error, undefined);
t.is(result.sarifID, expectedName);
t.assert(
uploadArtifact.calledOnceWith(
expectedName,
[sinon.match(expectedFilePattern)],
sinon.match.string,
),
);
t.assert(diagnosticsExport.notCalled);
t.assert(
databaseExportDiagnostics.calledOnceWith(
dbLocation,
sinon.match(expectedFilePattern),
"/language:javascript",
),
);
},
);
const skippedUploadTest = test.macro({
exec: async (
t: ExecutionContext<unknown>,
config: Partial<configUtils.Config>,
expectedSkippedReason: string,
) => {
const logger = new RecordingLogger();
const { uploadArtifact, diagnosticsExport } =
await mockRiskAssessmentEnv(singleLanguageMatrix);
const features = createFeatures([]);
const result = await initActionPostHelper.tryUploadSarifIfRunFailed(
createTestConfig(config),
parseRepositoryNwo("github/codeql-action-fake-repository"),
features,
logger,
);
t.is(result.upload_failed_run_skipped_because, expectedSkippedReason);
t.assert(uploadArtifact.notCalled);
t.assert(diagnosticsExport.notCalled);
},
title: (providedTitle: string = "") =>
`tryUploadSarifIfRunFailed - skips upload ${providedTitle}`,
});
test.serial(
"without CodeQL command",
skippedUploadTest,
// No codeQLCmd
{
analysisKinds: [AnalysisKind.RiskAssessment],
languages: ["javascript"],
} satisfies Partial<configUtils.Config>,
"CodeQL command not found",
);
test.serial(
"if no language is configured",
skippedUploadTest,
// No explicit language configuration
{
analysisKinds: [AnalysisKind.RiskAssessment],
codeQLCmd: "codeql-for-testing",
} satisfies Partial<configUtils.Config>,
"Unexpectedly, the configuration is not for a single language.",
);
test.serial(
"if multiple languages is configured",
skippedUploadTest,
// Multiple explicit languages configured
{
analysisKinds: [AnalysisKind.RiskAssessment],
codeQLCmd: "codeql-for-testing",
languages: ["javascript", "python"],
} satisfies Partial<configUtils.Config>,
"Unexpectedly, the configuration is not for a single language.",
);

View File

@@ -1,12 +1,22 @@
import * as fs from "fs";
import path from "path";
import * as github from "@actions/github";
import * as actionsUtil from "./actions-util";
import { CodeScanning } from "./analyses";
import { getApiClient } from "./api-client";
import { CodeScanning, RiskAssessment } from "./analyses";
import { getApiClient, getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import { Config, isCodeScanningEnabled } from "./config-utils";
import {
Config,
isCodeScanningEnabled,
isRiskAssessmentEnabled,
} from "./config-utils";
import {
getArtifactSuffix,
getArtifactUploaderClient,
sanitizeArtifactName,
} from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
@@ -23,10 +33,13 @@ import * as uploadLib from "./upload-lib";
import {
checkDiskUsage,
delay,
Failure,
getErrorMessage,
getRequiredEnvParam,
parseMatrixInput,
Result,
shouldSkipSarifUpload,
Success,
wrapError,
} from "./util";
import {
@@ -66,37 +79,96 @@ function createFailedUploadFailedSarifResult(
};
}
/** Records details about a SARIF file that contains information about a failed analysis. */
interface FailedSarifInfo {
sarifFile: string;
category: string | undefined;
checkoutPath: string;
}
/**
* Upload a failed SARIF file if we can verify that SARIF upload is enabled and determine the SARIF
* category for the workflow.
* Tries to prepare a SARIF file that contains information about a failed analysis.
*
* @returns Either information about the SARIF file that was produced, or a reason why it couldn't be produced.
*/
async function maybeUploadFailedSarif(
config: Config,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
async function prepareFailedSarif(
logger: Logger,
): Promise<UploadFailedSarifResult> {
features: FeatureEnablement,
config: Config,
): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> {
if (!config.codeQLCmd) {
return { upload_failed_run_skipped_because: "CodeQL command not found" };
return new Failure({
upload_failed_run_skipped_because: "CodeQL command not found",
});
}
const workflow = await getWorkflow(logger);
const jobName = getRequiredEnvParam("GITHUB_JOB");
const matrix = parseMatrixInput(actionsUtil.getRequiredInput("matrix"));
const shouldUpload = getUploadInputOrThrow(workflow, jobName, matrix);
if (
!["always", "failure-only"].includes(
actionsUtil.getUploadValue(shouldUpload),
) ||
shouldSkipSarifUpload()
) {
return { upload_failed_run_skipped_because: "SARIF upload is disabled" };
}
const category = getCategoryInputOrThrow(workflow, jobName, matrix);
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
const databasePath = config.dbLocation;
if (shouldSkipSarifUpload()) {
return new Failure({
upload_failed_run_skipped_because: "SARIF upload is disabled",
});
}
if (isRiskAssessmentEnabled(config)) {
if (config.languages.length !== 1) {
return new Failure({
upload_failed_run_skipped_because:
"Unexpectedly, the configuration is not for a single language.",
});
}
// We can make these assumptions for risk assessments.
const language = config.languages[0];
const category = `/language:${language}`;
const checkoutPath = ".";
const result = await generateFailedSarif(
features,
config,
category,
checkoutPath,
`../codeql-failed-sarif-${language}${RiskAssessment.sarifExtension}`,
);
return new Success(result);
} else {
const workflow = await getWorkflow(logger);
const shouldUpload = getUploadInputOrThrow(workflow, jobName, matrix);
if (
!["always", "failure-only"].includes(
actionsUtil.getUploadValue(shouldUpload),
)
) {
return new Failure({
upload_failed_run_skipped_because: "SARIF upload is disabled",
});
}
const category = getCategoryInputOrThrow(workflow, jobName, matrix);
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
const result = await generateFailedSarif(
features,
config,
category,
checkoutPath,
);
return new Success(result);
}
}
async function generateFailedSarif(
features: FeatureEnablement,
config: Config,
category: string | undefined,
checkoutPath: string,
sarifFile?: string,
) {
const databasePath = config.dbLocation;
const codeql = await getCodeQL(config.codeQLCmd);
const sarifFile = "../codeql-failed-run.sarif";
// Set the filename for the SARIF file if not already set.
if (sarifFile === undefined) {
sarifFile = "../codeql-failed-run.sarif";
}
// If there is no database or the feature flag is off, we run 'export diagnostics'
if (
@@ -109,11 +181,32 @@ async function maybeUploadFailedSarif(
await codeql.databaseExportDiagnostics(databasePath, sarifFile, category);
}
logger.info(`Uploading failed SARIF file ${sarifFile}`);
return { sarifFile, category, checkoutPath };
}
/**
* Upload a failed SARIF file if we can verify that SARIF upload is enabled and determine the SARIF
* category for the workflow.
*/
async function maybeUploadFailedSarif(
config: Config,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
const failedSarifResult = await prepareFailedSarif(logger, features, config);
if (failedSarifResult.isFailure()) {
return failedSarifResult.value;
}
const failedSarif = failedSarifResult.value;
logger.info(`Uploading failed SARIF file ${failedSarif.sarifFile}`);
const uploadResult = await uploadLib.uploadFiles(
sarifFile,
checkoutPath,
category,
failedSarif.sarifFile,
failedSarif.checkoutPath,
failedSarif.category,
features,
logger,
CodeScanning,
@@ -129,31 +222,78 @@ async function maybeUploadFailedSarif(
: {};
}
/** Uploads a failed SARIF file as workflow artifact, if it can be generated. */
async function maybeUploadFailedSarifArtifact(
config: Config,
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
const failedSarifResult = await prepareFailedSarif(logger, features, config);
if (failedSarifResult.isFailure()) {
return failedSarifResult.value;
}
const failedSarif = failedSarifResult.value;
logger.info(
`Uploading failed SARIF file ${failedSarif.sarifFile} as artifact`,
);
const gitHubVersion = await getGitHubVersion();
const client = await getArtifactUploaderClient(logger, gitHubVersion.type);
const suffix = getArtifactSuffix(actionsUtil.getOptionalInput("matrix"));
const name = sanitizeArtifactName(`sarif-artifact-${suffix}`);
await client.uploadArtifact(
name,
[path.normalize(failedSarif.sarifFile)],
path.normalize(".."),
);
return { sarifID: name };
}
/**
* Tries to upload a SARIF file with information about the run, if it failed.
*
* @param config The CodeQL Action configuration.
* @param repositoryNwo The name and owner of the repository.
* @param features Information about enabled features.
* @param logger The logger to use.
* @returns The results of uploading the SARIF file for the failure.
*/
export async function tryUploadSarifIfRunFailed(
config: Config,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
): Promise<UploadFailedSarifResult> {
// Only upload the failed SARIF to Code scanning if Code scanning is enabled.
if (!isCodeScanningEnabled(config)) {
return {
upload_failed_run_skipped_because: "Code Scanning is not enabled.",
};
}
// There's nothing to do here if the analysis succeeded.
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
return {
upload_failed_run_skipped_because:
"Analyze Action completed successfully",
};
}
try {
return await maybeUploadFailedSarif(
config,
repositoryNwo,
features,
logger,
);
// Only upload the failed SARIF to Code scanning if Code scanning is enabled.
if (isCodeScanningEnabled(config)) {
return await maybeUploadFailedSarif(
config,
repositoryNwo,
features,
logger,
);
} else if (isRiskAssessmentEnabled(config)) {
return await maybeUploadFailedSarifArtifact(config, features, logger);
} else {
return {
upload_failed_run_skipped_because:
"No analysis kind that supports failed SARIF uploads is enabled.",
};
}
} catch (e) {
logger.debug(
`Failed to upload a SARIF file for this failed CodeQL code scanning run. ${e}`,
@@ -162,7 +302,21 @@ export async function tryUploadSarifIfRunFailed(
}
}
export async function run(
/**
* Handles the majority of the `post-init` step logic which, depending on the configuration,
* mainly involves uploading a SARIF file with information about the failed run, debug
* artifacts, and performing clean-up operations.
*
* @param uploadAllAvailableDebugArtifacts A function with which to upload debug artifacts.
* @param printDebugLogs A function with which to print debug logs.
* @param codeql The CodeQL CLI instance.
* @param config The CodeQL Action configuration.
* @param repositoryNwo The name and owner of the repository.
* @param features Information about enabled features.
* @param logger The logger to use.
* @returns The results of uploading the SARIF file for the failure.
*/
export async function uploadFailureInfo(
uploadAllAvailableDebugArtifacts: (
codeql: CodeQL,
config: Config,
@@ -175,7 +329,7 @@ export async function run(
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
logger: Logger,
) {
): Promise<UploadFailedSarifResult> {
await recordOverlayStatus(codeql, config, features, logger);
const uploadFailedSarifResult = await tryUploadSarifIfRunFailed(
@@ -187,7 +341,7 @@ export async function run(
if (uploadFailedSarifResult.upload_failed_run_skipped_because) {
logger.debug(
"Won't upload a failed SARIF file for this CodeQL code scanning run because: " +
"Won't upload a failed SARIF file for this CodeQL analysis because: " +
`${uploadFailedSarifResult.upload_failed_run_skipped_because}.`,
);
}

View File

@@ -77,7 +77,7 @@ async function run(startedAt: Date) {
} else {
const codeql = await getCodeQL(config.codeQLCmd);
uploadFailedSarifResult = await initActionPostHelper.run(
uploadFailedSarifResult = await initActionPostHelper.uploadFailureInfo(
debugArtifacts.tryUploadAllAvailableDebugArtifacts,
printDebugLogs,
codeql,