mirror of
https://github.com/github/codeql-action.git
synced 2026-10-30 01:27:22 +00:00
Merge remote-tracking branch 'origin/main' into mbg/private-registry/oidc
This commit is contained in:
@@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
|
||||
@@ -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}.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
258
src/init.test.ts
258
src/init.test.ts
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
129
src/init.ts
129
src/init.ts
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum ToolsFeature {
|
||||
ForceOverwrite = "forceOverwrite",
|
||||
IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries",
|
||||
PythonDefaultIsToNotExtractStdlib = "pythonDefaultIsToNotExtractStdlib",
|
||||
SuppressesMissingFileBaselineWarning = "suppressesMissingFileBaselineWarning",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user