Merge remote-tracking branch 'origin/main' into mbg/private-registry/oidc

This commit is contained in:
Michael B. Gale
2026-03-11 16:15:07 +00:00
62 changed files with 4855 additions and 4126 deletions

View File

@@ -5,6 +5,7 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as api from "./api-client";
import { DO_NOT_RETRY_STATUSES } from "./api-client";
import { setupTests } from "./testing-utils";
import * as util from "./util";
@@ -37,7 +38,7 @@ test.serial("getApiClient", async (t) => {
log: sinon.match.any,
userAgent: `CodeQL-Action/${actionsUtil.getActionVersion()}`,
retry: {
doNotRetry: [400, 410, 422, 451],
doNotRetry: DO_NOT_RETRY_STATUSES,
},
}),
);

View File

@@ -19,6 +19,15 @@ import {
const GITHUB_ENTERPRISE_VERSION_HEADER = "x-github-enterprise-version";
/**
* HTTP status codes that should not be retried.
*
* The default Octokit list is 400, 401, 403, 404, 410, 422, and 451. We have
* observed transient errors with authentication, so we remove 401, 403, and 404
* from the default list to ensure that these errors are retried.
*/
export const DO_NOT_RETRY_STATUSES = [400, 410, 422, 451];
export type GitHubApiCombinedDetails = GitHubApiDetails &
GitHubApiExternalRepoDetails;
@@ -52,10 +61,7 @@ function createApiClientWithDetails(
error: core.error,
},
retry: {
// The default is 400, 401, 403, 404, 410, 422, and 451. We have observed transient errors
// with authentication, so we remove 401, 403, and 404 from the default list to ensure that
// these errors are retried.
doNotRetry: [400, 410, 422, 451],
doNotRetry: DO_NOT_RETRY_STATUSES,
},
}),
);

View File

@@ -2011,6 +2011,23 @@ for (const language in KnownLanguage) {
);
}
// Verify that a language without a per-language overlay feature flag cannot have
// overlay analysis enabled, even when the base overlay feature flag is on.
// Using cpp here as it doesn't currently have overlay support — update this if
// cpp gains overlay support.
test.serial(
checkOverlayEnablementMacro,
"No overlay analysis for language without per-language overlay feature flag",
{
languages: [KnownLanguage.cpp],
features: [Feature.OverlayAnalysis],
isPullRequest: true,
},
{
disabledReason: OverlayDisabledReason.LanguageNotEnabled,
},
);
test.serial(
"hasActionsWorkflows doesn't throw if workflows folder doesn't exist",
async (t) => {

View File

@@ -630,30 +630,29 @@ async function loadUserConfig(
}
}
const OVERLAY_ANALYSIS_FEATURES: Record<Language, Feature> = {
actions: Feature.OverlayAnalysisActions,
cpp: Feature.OverlayAnalysisCpp,
/**
* Maps languages to their overlay analysis feature flags. Only languages that
* are GA or in staff-ship for overlay analysis are included here. Languages
* without an entry will have overlay analysis disabled.
*/
const OVERLAY_ANALYSIS_FEATURES: Partial<Record<Language, Feature>> = {
csharp: Feature.OverlayAnalysisCsharp,
go: Feature.OverlayAnalysisGo,
java: Feature.OverlayAnalysisJava,
javascript: Feature.OverlayAnalysisJavascript,
python: Feature.OverlayAnalysisPython,
ruby: Feature.OverlayAnalysisRuby,
rust: Feature.OverlayAnalysisRust,
swift: Feature.OverlayAnalysisSwift,
};
const OVERLAY_ANALYSIS_CODE_SCANNING_FEATURES: Record<Language, Feature> = {
actions: Feature.OverlayAnalysisCodeScanningActions,
cpp: Feature.OverlayAnalysisCodeScanningCpp,
const OVERLAY_ANALYSIS_CODE_SCANNING_FEATURES: Partial<
Record<Language, Feature>
> = {
csharp: Feature.OverlayAnalysisCodeScanningCsharp,
go: Feature.OverlayAnalysisCodeScanningGo,
java: Feature.OverlayAnalysisCodeScanningJava,
javascript: Feature.OverlayAnalysisCodeScanningJavascript,
python: Feature.OverlayAnalysisCodeScanningPython,
ruby: Feature.OverlayAnalysisCodeScanningRuby,
rust: Feature.OverlayAnalysisCodeScanningRust,
swift: Feature.OverlayAnalysisCodeScanningSwift,
};
/**
@@ -1555,6 +1554,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

@@ -15,6 +15,7 @@ import * as gitUtils from "./git-utils";
import { KnownLanguage } from "./languages";
import { RepositoryNwo } from "./repository";
import {
checkExpectedLogMessages,
createFeatures,
createTestConfig,
getRecordingLogger,
@@ -93,7 +94,7 @@ test.serial(
.returns("false");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const loggedMessages = [];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -102,14 +103,9 @@ test.serial(
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Database upload disabled in workflow. Skipping upload.",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Database upload disabled in workflow. Skipping upload.",
]);
});
},
);
@@ -127,7 +123,7 @@ test.serial(
await mockHttpRequests(201);
const loggedMessages = [];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -139,14 +135,9 @@ test.serial(
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
]);
});
},
);
@@ -163,7 +154,7 @@ test.serial("Abort database upload if running against GHES", async (t) => {
const config = getTestConfig(tmpDir);
config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" };
const loggedMessages = [];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -172,14 +163,9 @@ test.serial("Abort database upload if running against GHES", async (t) => {
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Not running against github.com or GHEC-DR. Skipping upload.",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Not running against github.com or GHEC-DR. Skipping upload.",
]);
});
});
@@ -194,7 +180,7 @@ test.serial(
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
const loggedMessages = [];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -203,48 +189,90 @@ test.serial(
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message === "Not analyzing default branch. Skipping upload.",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Not analyzing default branch. Skipping upload.",
]);
});
},
);
test.serial("Don't crash if uploading a database fails", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
test.serial(
"Don't crash if uploading a database fails with a non-retryable error",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(500);
const databaseUploadSpy = await mockHttpRequests(422);
const loggedMessages = [] as LoggedMessage[];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "warning" &&
v.message ===
"Failed to upload database for javascript: some error message",
) !== undefined,
);
});
});
checkExpectedLogMessages(t, loggedMessages, [
"Failed to upload database for javascript: some error message",
]);
// Non-retryable errors should not be retried.
t.is(databaseUploadSpy.callCount, 1);
});
},
);
test.serial(
"Don't crash if uploading a database fails with a retryable error",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const databaseUploadSpy = await mockHttpRequests(500);
// Stub setTimeout to fire immediately to avoid real delays from retry backoff.
const originalSetTimeout = global.setTimeout;
const setTimeoutStub = sinon
.stub(global, "setTimeout")
.callsFake((fn: () => void) => originalSetTimeout(fn, 0));
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
);
checkExpectedLogMessages(t, loggedMessages, [
"Failed to upload database for javascript: some error message",
]);
// Retryable errors should be retried the expected number of times.
t.is(databaseUploadSpy.callCount, 4);
// setTimeout should have been called with the expected backoff delays.
const setTimeoutDelays = setTimeoutStub.args.map(
(args) => args[1] as number,
);
t.deepEqual(setTimeoutDelays, [15_000, 30_000, 60_000]);
});
},
);
test.serial("Successfully uploading a database to github.com", async (t) => {
await withTmpDir(async (tmpDir) => {
@@ -257,7 +285,7 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
await mockHttpRequests(201);
const loggedMessages = [] as LoggedMessage[];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -266,13 +294,9 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
v.message === "Successfully uploaded database for javascript",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Successfully uploaded database for javascript",
]);
});
});
@@ -287,7 +311,7 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
const databaseUploadSpy = await mockHttpRequests(201);
const loggedMessages = [] as LoggedMessage[];
const loggedMessages: LoggedMessage[] = [];
await cleanupAndUploadDatabases(
testRepoName,
getCodeQL(),
@@ -300,13 +324,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
createFeatures([]),
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v) =>
v.type === "debug" &&
v.message === "Successfully uploaded database for javascript",
) !== undefined,
);
checkExpectedLogMessages(t, loggedMessages, [
"Successfully uploaded database for javascript",
]);
t.assert(
databaseUploadSpy.calledOnceWith(
sinon.match.string,

View File

@@ -2,7 +2,11 @@ import * as fs from "fs";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import { getApiClient, GitHubApiDetails } from "./api-client";
import {
DO_NOT_RETRY_STATUSES,
getApiClient,
GitHubApiDetails,
} from "./api-client";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import { Feature, FeatureEnablement } from "./feature-flags";
@@ -11,7 +15,7 @@ import { Logger, withGroupAsync } from "./logging";
import { OverlayDatabaseMode } from "./overlay";
import { RepositoryNwo } from "./repository";
import * as util from "./util";
import { bundleDb, CleanupLevel, parseGitHubUrl } from "./util";
import { asHTTPError, bundleDb, CleanupLevel, parseGitHubUrl } from "./util";
/** Information about a database upload. */
export interface DatabaseUploadResult {
@@ -81,18 +85,6 @@ export async function cleanupAndUploadDatabases(
await codeql.databaseCleanupCluster(config, cleanupLevel);
});
const client = getApiClient();
const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url));
uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`;
// Octokit expects the baseUrl to not have a trailing slash,
// but it is included by default in a URL.
let uploadsBaseUrl = uploadsUrl.toString();
if (uploadsBaseUrl.endsWith("/")) {
uploadsBaseUrl = uploadsBaseUrl.slice(0, -1);
}
const reports: DatabaseUploadResult[] = [];
for (const language of config.languages) {
let bundledDbSize: number | undefined = undefined;
@@ -105,40 +97,51 @@ export async function cleanupAndUploadDatabases(
includeDiagnostics: false,
});
bundledDbSize = fs.statSync(bundledDb).size;
const bundledDbReadStream = fs.createReadStream(bundledDb);
const commitOid = await gitUtils.getCommitOid(
actionsUtil.getRequiredInput("checkout_path"),
);
try {
const startTime = performance.now();
await client.request(
`POST /repos/:owner/:repo/code-scanning/codeql/databases/:language?name=:name&commit_oid=:commit_oid`,
{
baseUrl: uploadsBaseUrl,
owner: repositoryNwo.owner,
repo: repositoryNwo.repo,
// Upload with manual retry logic. We disable Octokit's built-in retries
// because the request body is a ReadStream, which can only be consumed
// once.
const maxAttempts = 4; // 1 initial attempt + 3 retries, identical to the default retry behavior of Octokit
let uploadDurationMs: number | undefined;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
uploadDurationMs = await uploadBundledDatabase(
repositoryNwo,
language,
name: `${language}-database`,
commit_oid: commitOid,
data: bundledDbReadStream,
headers: {
authorization: `token ${apiDetails.auth}`,
"Content-Type": "application/zip",
"Content-Length": bundledDbSize,
},
},
);
const endTime = performance.now();
reports.push({
language,
zipped_upload_size_bytes: bundledDbSize,
is_overlay_base: shouldUploadOverlayBase,
upload_duration_ms: endTime - startTime,
});
logger.debug(`Successfully uploaded database for ${language}`);
} finally {
bundledDbReadStream.close();
commitOid,
bundledDb,
bundledDbSize,
apiDetails,
);
break;
} catch (e) {
const httpError = asHTTPError(e);
const isRetryable =
!httpError || !DO_NOT_RETRY_STATUSES.includes(httpError.status);
if (!isRetryable) {
throw e;
} else if (attempt === maxAttempts) {
logger.error(
`Maximum retry attempts exhausted (${attempt}), aborting database upload`,
);
throw e;
}
const backoffMs = 15_000 * Math.pow(2, attempt - 1); // 15s, 30s, 60s
logger.debug(
`Database upload attempt ${attempt} of ${maxAttempts} failed for ${language}: ${util.getErrorMessage(e)}. Retrying in ${backoffMs / 1000}s...`,
);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
reports.push({
language,
zipped_upload_size_bytes: bundledDbSize,
is_overlay_base: shouldUploadOverlayBase,
upload_duration_ms: uploadDurationMs,
});
logger.debug(`Successfully uploaded database for ${language}`);
} catch (e) {
// Log a warning but don't fail the workflow
logger.warning(
@@ -155,3 +158,58 @@ export async function cleanupAndUploadDatabases(
}
return reports;
}
/**
* Uploads a bundled database to the GitHub API.
*
* @returns the duration of the upload in milliseconds
*/
async function uploadBundledDatabase(
repositoryNwo: RepositoryNwo,
language: string,
commitOid: string,
bundledDb: string,
bundledDbSize: number,
apiDetails: GitHubApiDetails,
): Promise<number> {
const client = getApiClient();
const uploadsUrl = new URL(parseGitHubUrl(apiDetails.url));
uploadsUrl.hostname = `uploads.${uploadsUrl.hostname}`;
// Octokit expects the baseUrl to not have a trailing slash,
// but it is included by default in a URL.
let uploadsBaseUrl = uploadsUrl.toString();
if (uploadsBaseUrl.endsWith("/")) {
uploadsBaseUrl = uploadsBaseUrl.slice(0, -1);
}
const bundledDbReadStream = fs.createReadStream(bundledDb);
try {
const startTime = performance.now();
await client.request(
`POST /repos/:owner/:repo/code-scanning/codeql/databases/:language?name=:name&commit_oid=:commit_oid`,
{
baseUrl: uploadsBaseUrl,
owner: repositoryNwo.owner,
repo: repositoryNwo.repo,
language,
name: `${language}-database`,
commit_oid: commitOid,
data: bundledDbReadStream,
headers: {
authorization: `token ${apiDetails.auth}`,
"Content-Type": "application/zip",
"Content-Length": bundledDbSize,
},
// Disable `octokit/plugin-retry.js`, since the request body is a ReadStream which can only be consumed once.
request: {
retries: 0,
},
},
);
return performance.now() - startTime;
} finally {
bundledDbReadStream.close();
}
}

View File

@@ -47,6 +47,15 @@ export enum EnvVar {
/** Whether the init action has been run. */
INIT_ACTION_HAS_RUN = "CODEQL_ACTION_INIT_HAS_RUN",
/** Whether the deprecation warning for file coverage on PRs has been logged. */
DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION = "CODEQL_ACTION_DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION",
/**
* Set to `true` to opt out of the upcoming change that skips file coverage
* information on pull requests.
*/
FILE_COVERAGE_ON_PRS = "CODEQL_ACTION_FILE_COVERAGE_ON_PRS",
/** Whether the error for a deprecated version of the CodeQL Action was logged. */
LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION",

View File

@@ -7,7 +7,15 @@ import { getApiClient } from "./api-client";
import type { CodeQL } from "./codeql";
import * as defaults from "./defaults.json";
import { Logger } from "./logging";
import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay";
import {
CODEQL_OVERLAY_MINIMUM_VERSION,
CODEQL_OVERLAY_MINIMUM_VERSION_CSHARP,
CODEQL_OVERLAY_MINIMUM_VERSION_GO,
CODEQL_OVERLAY_MINIMUM_VERSION_JAVA,
CODEQL_OVERLAY_MINIMUM_VERSION_JAVASCRIPT,
CODEQL_OVERLAY_MINIMUM_VERSION_PYTHON,
CODEQL_OVERLAY_MINIMUM_VERSION_RUBY,
} from "./overlay";
import { RepositoryNwo } from "./repository";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
@@ -49,24 +57,18 @@ export enum Feature {
IgnoreGeneratedFiles = "ignore_generated_files",
JavaNetworkDebugging = "java_network_debugging",
OverlayAnalysis = "overlay_analysis",
OverlayAnalysisActions = "overlay_analysis_actions",
OverlayAnalysisCodeScanningActions = "overlay_analysis_code_scanning_actions",
OverlayAnalysisCodeScanningCpp = "overlay_analysis_code_scanning_cpp",
OverlayAnalysisCodeScanningCsharp = "overlay_analysis_code_scanning_csharp",
OverlayAnalysisCodeScanningGo = "overlay_analysis_code_scanning_go",
OverlayAnalysisCodeScanningJava = "overlay_analysis_code_scanning_java",
OverlayAnalysisCodeScanningJavascript = "overlay_analysis_code_scanning_javascript",
OverlayAnalysisCodeScanningPython = "overlay_analysis_code_scanning_python",
OverlayAnalysisCodeScanningRuby = "overlay_analysis_code_scanning_ruby",
OverlayAnalysisCodeScanningRust = "overlay_analysis_code_scanning_rust",
OverlayAnalysisCodeScanningSwift = "overlay_analysis_code_scanning_swift",
OverlayAnalysisCpp = "overlay_analysis_cpp",
OverlayAnalysisCsharp = "overlay_analysis_csharp",
OverlayAnalysisGo = "overlay_analysis_go",
/** Controls whether the Actions cache is checked for overlay build outcomes. */
OverlayAnalysisStatusCheck = "overlay_analysis_status_check",
/** Controls whether overlay build failures on are stored in the Actions cache. */
OverlayAnalysisStatusSave = "overlay_analysis_status_save",
OverlayAnalysisGo = "overlay_analysis_go",
OverlayAnalysisJava = "overlay_analysis_java",
OverlayAnalysisJavascript = "overlay_analysis_javascript",
OverlayAnalysisPython = "overlay_analysis_python",
@@ -76,10 +78,8 @@ export enum Feature {
*/
OverlayAnalysisResourceChecksV2 = "overlay_analysis_resource_checks_v2",
OverlayAnalysisRuby = "overlay_analysis_ruby",
OverlayAnalysisRust = "overlay_analysis_rust",
/** Controls whether hardware checks are skipped for overlay analysis. */
OverlayAnalysisSkipResourceChecks = "overlay_analysis_skip_resource_checks",
OverlayAnalysisSwift = "overlay_analysis_swift",
PythonDefaultIsToNotExtractStdlib = "python_default_is_to_not_extract_stdlib",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Note that this currently only disables baseline file coverage information. */
@@ -194,70 +194,48 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION,
},
[Feature.OverlayAnalysisActions]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_ACTIONS",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningActions]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_ACTIONS",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningCpp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_CPP",
minimumVersion: undefined,
},
// Per-language overlay feature flags. Each has minimumVersion set to the
// minimum CLI version that supports overlay analysis for that language.
// Only languages that are GA or in staff-ship should have feature flags here.
[Feature.OverlayAnalysisCodeScanningCsharp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_CSHARP",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_CSHARP,
},
[Feature.OverlayAnalysisCodeScanningGo]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_GO",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_GO,
},
[Feature.OverlayAnalysisCodeScanningJava]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_JAVA",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_JAVA,
},
[Feature.OverlayAnalysisCodeScanningJavascript]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_JAVASCRIPT",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_JAVASCRIPT,
},
[Feature.OverlayAnalysisCodeScanningPython]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_PYTHON",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_PYTHON,
},
[Feature.OverlayAnalysisCodeScanningRuby]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_RUBY",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningRust]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_RUST",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningSwift]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_SWIFT",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCpp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CPP",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_RUBY,
},
[Feature.OverlayAnalysisCsharp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CSHARP",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_CSHARP,
},
[Feature.OverlayAnalysisGo]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_GO",
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_GO,
},
[Feature.OverlayAnalysisStatusCheck]: {
defaultValue: false,
@@ -269,25 +247,20 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_STATUS_SAVE",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisGo]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_GO",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisJava]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_JAVA",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_JAVA,
},
[Feature.OverlayAnalysisJavascript]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_JAVASCRIPT",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_JAVASCRIPT,
},
[Feature.OverlayAnalysisPython]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_PYTHON",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_PYTHON,
},
[Feature.OverlayAnalysisResourceChecksV2]: {
defaultValue: false,
@@ -297,23 +270,13 @@ export const featureConfig = {
[Feature.OverlayAnalysisRuby]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RUBY",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisRust]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RUST",
minimumVersion: undefined,
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_RUBY,
},
[Feature.OverlayAnalysisSkipResourceChecks]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SKIP_RESOURCE_CHECKS",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisSwift]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SWIFT",
minimumVersion: undefined,
},
[Feature.PythonDefaultIsToNotExtractStdlib]: {
defaultValue: false,
envVar: "CODEQL_ACTION_DISABLE_PYTHON_STANDARD_LIBRARY_EXTRACTION",
@@ -329,11 +292,8 @@ export const featureConfig = {
[Feature.SkipFileCoverageOnPrs]: {
defaultValue: false,
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
// For testing, this is not behind a CLI version check yet. However
// before rolling this out externally, we should set a minimum version here
// since current versions of the CodeQL CLI will log if baseline information
// cannot be found when interpreting results.
minimumVersion: undefined,
toolsFeature: ToolsFeature.SuppressesMissingFileBaselineWarning,
},
[Feature.StartProxyRemoveUnusedRegistries]: {
defaultValue: false,

View File

@@ -8,12 +8,14 @@ import { RepositoryNwo } from "../repository";
export enum RepositoryPropertyName {
DISABLE_OVERLAY = "github-codeql-disable-overlay",
EXTRA_QUERIES = "github-codeql-extra-queries",
FILE_COVERAGE_ON_PRS = "github-codeql-file-coverage-on-prs",
}
/** Parsed types of the known repository properties. */
export type AllRepositoryProperties = {
[RepositoryPropertyName.DISABLE_OVERLAY]: boolean;
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
};
/** Parsed repository properties. */
@@ -23,6 +25,7 @@ export type RepositoryProperties = Partial<AllRepositoryProperties>;
export type RepositoryPropertyApiType = {
[RepositoryPropertyName.DISABLE_OVERLAY]: string;
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: string;
};
/** The type of functions which take the `value` from the API and try to convert it to the type we want. */
@@ -69,6 +72,7 @@ const repositoryPropertyParsers: {
} = {
[RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty,
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
};
/**

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,

View File

@@ -48,6 +48,7 @@ import {
checkPacksForOverlayCompatibility,
cleanupDatabaseClusterDirectory,
getFileCoverageInformationEnabled,
logFileCoverageOnPrsDeprecationWarning,
initCodeQL,
initConfig,
runDatabaseInitCluster,
@@ -343,6 +344,14 @@ async function run(startedAt: Date) {
analysisKinds = await getAnalysisKinds(logger);
const debugMode = getOptionalInput("debug") === "true" || core.isDebug();
const repositoryProperties = repositoryPropertiesResult.orElse({});
const fileCoverageResult = await getFileCoverageInformationEnabled(
debugMode,
codeql,
features,
repositoryProperties,
);
config = await initConfig(features, {
analysisKinds,
languagesInput: getOptionalInput("languages"),
@@ -372,12 +381,8 @@ async function run(startedAt: Date) {
githubVersion: gitHubVersion,
apiDetails,
features,
repositoryProperties: repositoryPropertiesResult.orElse({}),
enableFileCoverageInformation: await getFileCoverageInformationEnabled(
debugMode,
repositoryNwo,
features,
),
repositoryProperties,
enableFileCoverageInformation: fileCoverageResult.enabled,
logger,
});
@@ -394,6 +399,21 @@ async function run(startedAt: Date) {
);
}
if (fileCoverageResult.enabledByRepositoryProperty) {
addNoLanguageDiagnostic(
config,
makeTelemetryDiagnostic(
"codeql-action/file-coverage-on-prs-enabled-by-repository-property",
"File coverage on PRs enabled by repository property",
{},
),
);
}
if (fileCoverageResult.showDeprecationWarning) {
logFileCoverageOnPrsDeprecationWarning(logger);
}
await checkInstallPython311(config.languages, codeql);
} catch (unwrappedError) {
const error = wrapError(unwrappedError);

View File

@@ -1,6 +1,8 @@
import * as fs from "fs";
import path from "path";
import * as core from "@actions/core";
import * as github from "@actions/github";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
@@ -11,9 +13,9 @@ import {
checkPacksForOverlayCompatibility,
cleanupDatabaseClusterDirectory,
getFileCoverageInformationEnabled,
logFileCoverageOnPrsDeprecationWarning,
} from "./init";
import { KnownLanguage } from "./languages";
import { parseRepositoryNwo } from "./repository";
import {
createFeatures,
LoggedMessage,
@@ -453,13 +455,15 @@ test(
);
test("file coverage information enabled when debugMode is true", async (t) => {
t.true(
await getFileCoverageInformationEnabled(
true, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
const result = await getFileCoverageInformationEnabled(
true, // debugMode
createStubCodeQL({}),
createFeatures([Feature.SkipFileCoverageOnPrs]),
{},
);
t.true(result.enabled);
t.false(result.enabledByRepositoryProperty);
t.false(result.showDeprecationWarning);
});
test.serial(
@@ -467,43 +471,69 @@ test.serial(
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(false);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
const result = await getFileCoverageInformationEnabled(
false, // debugMode
createStubCodeQL({}),
createFeatures([Feature.SkipFileCoverageOnPrs]),
{},
);
t.true(result.enabled);
t.false(result.enabledByRepositoryProperty);
t.false(result.showDeprecationWarning);
},
);
test.serial(
"file coverage information enabled when owner is not 'github'",
"file coverage information enabled when feature flag is not enabled, with deprecation warning",
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("other-org/some-repo"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
const result = await getFileCoverageInformationEnabled(
false, // debugMode
createStubCodeQL({}),
createFeatures([]),
{},
);
t.true(result.enabled);
t.false(result.enabledByRepositoryProperty);
t.true(result.showDeprecationWarning);
},
);
test.serial(
"file coverage information enabled when feature flag is not enabled",
"file coverage information enabled when repository property is set",
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.true(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
),
const result = await getFileCoverageInformationEnabled(
false, // debugMode
createStubCodeQL({}),
createFeatures([Feature.SkipFileCoverageOnPrs]),
{
"github-codeql-file-coverage-on-prs": true,
},
);
t.true(result.enabled);
t.true(result.enabledByRepositoryProperty);
t.false(result.showDeprecationWarning);
},
);
test.serial(
"file coverage information enabled when env var opt-out is set",
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
process.env["CODEQL_ACTION_FILE_COVERAGE_ON_PRS"] = "true";
const result = await getFileCoverageInformationEnabled(
false, // debugMode
createStubCodeQL({}),
createFeatures([Feature.SkipFileCoverageOnPrs]),
{},
);
t.true(result.enabled);
t.false(result.enabledByRepositoryProperty);
t.false(result.showDeprecationWarning);
},
);
@@ -512,12 +542,174 @@ test.serial(
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(true);
t.false(
await getFileCoverageInformationEnabled(
false, // debugMode
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.SkipFileCoverageOnPrs]),
),
const result = await getFileCoverageInformationEnabled(
false, // debugMode
createStubCodeQL({}),
createFeatures([Feature.SkipFileCoverageOnPrs]),
{},
);
t.false(result.enabled);
t.false(result.enabledByRepositoryProperty);
t.false(result.showDeprecationWarning);
},
);
test.serial(
"file coverage deprecation warning for org-owned repo with default setup recommends repo property",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = {
repository: {
name: "test-repo",
owner: { login: "test-org", type: "Organization" },
},
};
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, create a custom repository property " +
'with the name `github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for org-owned repo with advanced setup recommends env var and repo property",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = {
repository: {
name: "test-repo",
owner: { login: "test-org", type: "Organization" },
},
};
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`. " +
"Alternatively, create a custom repository property " +
'with the name `github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for user-owned repo with default setup recommends advanced setup",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = {
repository: {
name: "test-repo",
owner: { login: "test-user", type: "User" },
},
};
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, switch to an advanced setup workflow and " +
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for user-owned repo with advanced setup recommends env var",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = {
repository: {
name: "test-repo",
owner: { login: "test-user", type: "User" },
},
};
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for unknown owner type with default setup recommends advanced setup",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(true);
github.context.payload = { repository: undefined };
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, switch to an advanced setup workflow and " +
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"file coverage deprecation warning for unknown owner type with advanced setup recommends env var",
(t) => {
const exportVariableStub = sinon.stub(core, "exportVariable");
sinon.stub(actionsUtil, "isDefaultSetup").returns(false);
github.context.payload = { repository: undefined };
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 1);
t.is(messages[0].type, "warning");
t.is(
messages[0].message,
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.\n\n" +
"To opt out of this change, set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.",
);
t.true(exportVariableStub.calledOnce);
},
);
test.serial(
"logFileCoverageOnPrsDeprecationWarning does not log if already logged",
(t) => {
process.env["CODEQL_ACTION_DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION"] =
"true";
const exportVariableStub = sinon.stub(core, "exportVariable");
const messages: LoggedMessage[] = [];
logFileCoverageOnPrsDeprecationWarning(getRecordingLogger(messages));
t.is(messages.length, 0);
t.true(exportVariableStub.notCalled);
},
);

View File

@@ -1,26 +1,33 @@
import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import {
getOptionalInput,
isAnalyzingPullRequest,
isDefaultSetup,
isSelfHostedRunner,
} from "./actions-util";
import { GitHubApiDetails } from "./api-client";
import { CodeQL, setupCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { EnvVar } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
FeatureEnablement,
} from "./feature-flags";
import {
RepositoryProperties,
RepositoryPropertyName,
} from "./feature-flags/properties";
import { KnownLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { RepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
@@ -300,18 +307,112 @@ export function cleanupDatabaseClusterDirectory(
export async function getFileCoverageInformationEnabled(
debugMode: boolean,
repositoryNwo: RepositoryNwo,
codeql: CodeQL,
features: FeatureEnablement,
): Promise<boolean> {
return (
// Always enable file coverage information in debug mode
debugMode ||
// We're most interested in speeding up PRs, and we want to keep
// submitting file coverage information for the default branch since
// it is used to populate the status page.
!isAnalyzingPullRequest() ||
// For now, restrict this feature to the GitHub org
repositoryNwo.owner !== "github" ||
!(await features.getValue(Feature.SkipFileCoverageOnPrs))
);
repositoryProperties: RepositoryProperties,
): Promise<{
enabled: boolean;
enabledByRepositoryProperty: boolean;
showDeprecationWarning: boolean;
}> {
// Always enable file coverage information in debug mode
if (debugMode) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// We're most interested in speeding up PRs, and we want to keep
// submitting file coverage information for the default branch since
// it is used to populate the status page.
if (!isAnalyzingPullRequest()) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// If the user has explicitly opted out via an environment variable, don't
// show the deprecation warning.
if (
(process.env[EnvVar.FILE_COVERAGE_ON_PRS] || "").toLocaleLowerCase() ===
"true"
) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// Allow repositories to opt in to file coverage information on PRs
// using a repository property. In this case, don't show the deprecation
// warning since the repository has explicitly opted in.
if (
repositoryProperties[RepositoryPropertyName.FILE_COVERAGE_ON_PRS] === true
) {
return {
enabled: true,
enabledByRepositoryProperty: true,
showDeprecationWarning: false,
};
}
// If the feature is disabled, then maintain the previous behavior of
// unconditionally computing file coverage information, but warn that
// file coverage on PRs will be disabled in a future release.
if (!(await features.getValue(Feature.SkipFileCoverageOnPrs, codeql))) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: true,
};
}
// Otherwise, disable file coverage information on PRs to speed up analysis.
return {
enabled: false,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
/**
* Log a warning about the deprecation of file coverage information on PRs, including how to opt
* back in via an environment variable or repository property.
*/
export function logFileCoverageOnPrsDeprecationWarning(logger: Logger): void {
if (process.env[EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION]) {
return;
}
const repositoryOwnerType: string | undefined =
github.context.payload.repository?.owner.type;
let message =
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.";
const envVarOptOut =
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.";
const repoPropertyOptOut =
"create a custom repository property with the name " +
'`github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.";
if (repositoryOwnerType === "Organization") {
// Org-owned repo: can use the repository property
if (isDefaultSetup()) {
message += `\n\nTo opt out of this change, ${repoPropertyOptOut}`;
} else {
message += `\n\nTo opt out of this change, ${envVarOptOut} Alternatively, ${repoPropertyOptOut}`;
}
} else if (isDefaultSetup()) {
// User-owned repo on default setup: no repo property available and
// no way to set env vars, so need to switch to advanced setup.
message += `\n\nTo opt out of this change, switch to an advanced setup workflow and ${envVarOptOut}`;
} else {
// User-owned repo on advanced setup: can set the env var
message += `\n\nTo opt out of this change, ${envVarOptOut}`;
}
logger.warning(message);
core.exportVariable(EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION, "true");
}

View File

@@ -33,6 +33,15 @@ export enum OverlayDatabaseMode {
export const CODEQL_OVERLAY_MINIMUM_VERSION = "2.23.8";
// Per-language minimum CLI versions for overlay analysis, based on release
// validation data.
export const CODEQL_OVERLAY_MINIMUM_VERSION_CSHARP = "2.24.1";
export const CODEQL_OVERLAY_MINIMUM_VERSION_GO = "2.24.2";
export const CODEQL_OVERLAY_MINIMUM_VERSION_JAVA = "2.23.8";
export const CODEQL_OVERLAY_MINIMUM_VERSION_JAVASCRIPT = "2.23.9";
export const CODEQL_OVERLAY_MINIMUM_VERSION_PYTHON = "2.23.9";
export const CODEQL_OVERLAY_MINIMUM_VERSION_RUBY = "2.23.9";
/**
* The maximum (uncompressed) size of the overlay base database that we will
* upload. By default, the Actions Cache has an overall capacity of 10 GB, and

View File

@@ -10,6 +10,7 @@ export enum ToolsFeature {
ForceOverwrite = "forceOverwrite",
IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries",
PythonDefaultIsToNotExtractStdlib = "pythonDefaultIsToNotExtractStdlib",
SuppressesMissingFileBaselineWarning = "suppressesMissingFileBaselineWarning",
}
/**