Merge remote-tracking branch 'origin/main' into mbg/all-action-state

This commit is contained in:
Michael B. Gale
2026-10-22 15:00:14 +01:00
68 changed files with 2194 additions and 1600 deletions

View File

@@ -11,7 +11,6 @@ import {
} from "./actions-util";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { RecordingLogger, setupTests } from "./testing-utils";
import { initializeEnvironment, withTmpDir } from "./util";
@@ -238,7 +237,7 @@ test.serial("fixCodeQualityCategory", (t) => {
GITHUB_EVENT_NAME: "dynamic",
},
() => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
// Categories that should get adjusted.
t.is(fixCodeQualityCategory(logger, "/language:c#"), "/language:csharp");

View File

@@ -15,7 +15,6 @@ import {
supportedAnalysisKinds,
} from "./analyses";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import {
createFeatures,
RecordingLogger,
@@ -65,7 +64,7 @@ test.serial(
.withArgs("analysis-kinds")
.returns("code-scanning,code-quality");
const result = await getAnalysisKinds(
getRunnerLogger(true),
new RecordingLogger(),
features,
true,
);
@@ -139,7 +138,7 @@ test.serial(
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
optionalInputStub.withArgs("quality-queries").returns("code-quality");
const result = await getAnalysisKinds(
getRunnerLogger(true),
new RecordingLogger(),
features,
true,
);
@@ -155,7 +154,7 @@ test.serial(
const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
requiredInputStub.withArgs("analysis-kinds").returns("no-such-thing");
await t.throwsAsync(
getAnalysisKinds(getRunnerLogger(true), features, true),
getAnalysisKinds(new RecordingLogger(), features, true),
);
},
);
@@ -181,7 +180,7 @@ for (let i = 0; i < analysisKinds.length; i++) {
.withArgs("analysis-kinds")
.returns([analysisKind, otherAnalysis].join(","));
const result = await getAnalysisKinds(
getRunnerLogger(true),
new RecordingLogger(),
features,
true,
);
@@ -199,7 +198,7 @@ for (let i = 0; i < analysisKinds.length; i++) {
.withArgs("analysis-kinds")
.returns([analysisKind, otherAnalysis].join(","));
await t.throwsAsync(
getAnalysisKinds(getRunnerLogger(true), features, true),
getAnalysisKinds(new RecordingLogger(), features, true),
{
instanceOf: ConfigurationError,
message: `${analysisKind} and ${otherAnalysis} cannot be enabled at the same time`,

View File

@@ -50,9 +50,8 @@ import {
StatusReportBase,
} from "./status-report";
import {
cleanupTrapCaches,
TrapCacheCleanupStatusReport,
uploadTrapCaches,
warnIfTrapCacheCleanupEnvVarIsSet,
} from "./trap-caching";
import * as uploadLib from "./upload-lib";
import { UploadResult } from "./upload-lib";
@@ -86,7 +85,6 @@ async function sendStatusReport(
trapCacheUploadTime: number | undefined,
dbCreationTimings: DatabaseCreationTimings | undefined,
didUploadTrapCaches: boolean,
trapCacheCleanup: TrapCacheCleanupStatusReport | undefined,
dependencyCacheResults: DependencyCacheUploadStatusReport | undefined,
databaseUploadResults: DatabaseUploadResult[],
logger: Logger,
@@ -107,7 +105,6 @@ async function sendStatusReport(
...statusReportBase,
...(stats || {}),
...(dbCreationTimings || {}),
...(trapCacheCleanup || {}),
dependency_caching_upload_results: dependencyCacheResults,
database_upload_results: databaseUploadResults,
};
@@ -229,8 +226,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
| undefined = undefined;
let runStats: QueriesStatusReport | undefined = undefined;
let config: Config | undefined = undefined;
let trapCacheCleanupTelemetry: TrapCacheCleanupStatusReport | undefined =
undefined;
let trapCacheUploadTime: number | undefined = undefined;
let dbCreationTimings: DatabaseCreationTimings | undefined = undefined;
let didUploadTrapCaches = false;
@@ -278,6 +273,8 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
);
}
warnIfTrapCacheCleanupEnvVarIsSet(action);
const apiDetails = getApiDetails();
const outputDir = actionsUtil.getRequiredInput("output");
action.env.export(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
@@ -416,13 +413,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
didUploadTrapCaches = await uploadTrapCaches(codeql, config, logger);
trapCacheUploadTime = performance.now() - trapCacheUploadStartTime;
// Clean up TRAP caches
trapCacheCleanupTelemetry = await cleanupTrapCaches(
config,
features,
logger,
);
// Store dependency cache(s) if dependency caching is enabled.
if (shouldStoreCache(config.dependencyCachingEnabled)) {
dependencyCacheResults = await uploadDependencyCaches(
@@ -474,7 +464,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
trapCacheUploadTime,
dbCreationTimings,
didUploadTrapCaches,
trapCacheCleanupTelemetry,
dependencyCacheResults,
databaseUploadResults,
logger,
@@ -497,7 +486,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
trapCacheUploadTime,
dbCreationTimings,
didUploadTrapCaches,
trapCacheCleanupTelemetry,
dependencyCacheResults,
databaseUploadResults,
logger,
@@ -511,7 +499,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
trapCacheUploadTime,
dbCreationTimings,
didUploadTrapCaches,
trapCacheCleanupTelemetry,
dependencyCacheResults,
databaseUploadResults,
logger,
@@ -525,7 +512,6 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
trapCacheUploadTime,
dbCreationTimings,
didUploadTrapCaches,
trapCacheCleanupTelemetry,
dependencyCacheResults,
databaseUploadResults,
logger,

View File

@@ -17,12 +17,12 @@ import { defaultSuites } from "./config/db-config";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import {
setupTests,
setupActionsVars,
createFeatures,
createTestConfig,
RecordingLogger,
callee,
} from "./testing-utils";
import * as uploadLib from "./upload-lib";
@@ -195,7 +195,7 @@ test.serial("status report fields", async (t) => {
undefined,
codeql,
config,
getRunnerLogger(true),
new RecordingLogger(),
createFeatures([Feature.QaTelemetryEnabled]),
);
t.deepEqual(Object.keys(statusReport).sort(), [

View File

@@ -401,21 +401,6 @@ export async function listActionsCaches(
);
}
/**
* Delete an Actions cache item by its ID.
*
* See https://docs.github.com/en/rest/actions/cache#delete-a-github-actions-cache-for-a-repository-using-a-cache-id.
*/
export async function deleteActionsCache(id: number) {
const repositoryNwo = getRepositoryNwo();
await getApiClient().rest.actions.deleteActionsCacheById({
owner: repositoryNwo.owner,
repo: repositoryNwo.repo,
cache_id: id,
});
}
/**
* Retrieve all custom repository properties.
*

View File

@@ -10,12 +10,12 @@ import {
scanArtifactsForTokens,
TokenType,
} from "./artifact-scanner";
import { getRunnerLogger } from "./logging";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
makeTestToken,
RecordingLogger,
} from "./testing-utils";
test("makeTestToken", (t) => {
@@ -121,7 +121,7 @@ for (const { type, value, checkPattern } of testTokens) {
}
test("scanArtifactsForTokens handles files without tokens", async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
try {

View File

@@ -3,15 +3,14 @@ import path from "path";
import test from "ava";
import { getRunnerLogger } from "../logging";
import { setupTests } from "../testing-utils";
import { RecordingLogger, setupTests } from "../testing-utils";
import * as util from "../util";
import { getCachedCodeQlVersion } from "./output-cache";
setupTests(test);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
test.serial(
"getCachedCodeQlVersion reuses a version persisted by an earlier step",

View File

@@ -22,7 +22,6 @@ import type { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { DocUrl } from "./doc-url";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import { ToolsSource } from "./setup-codeql";
import {
setupTests,
@@ -34,6 +33,8 @@ import {
makeVersionInfo,
createTestConfig,
makeMacro,
getCodeQLForTesting,
RecordingLogger,
} from "./testing-utils";
import { ToolsDownloadStatusReport } from "./tools-download";
import * as util from "./util";
@@ -102,7 +103,7 @@ async function installIntoToolcache({
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
false,
);
}
@@ -140,7 +141,7 @@ function mockApiDetails(apiDetails: Required<GitHubApiDetails>) {
}
async function stubCodeql(): Promise<codeql.CodeQL> {
const codeqlObject = await codeql.getCodeQLForTesting();
const codeqlObject = await getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
sinon
.stub(codeqlObject, "isTracedLanguage")
@@ -176,7 +177,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
@@ -212,7 +213,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
@@ -252,7 +253,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.assert(toolcache.find("CodeQL", "0.0.0-20200610"));
@@ -362,7 +363,7 @@ for (const {
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.assert(toolcache.find("CodeQL", expectedToolcacheVersion));
@@ -405,7 +406,7 @@ for (const toolcacheVersion of [
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.is(
@@ -450,7 +451,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.deepEqual(result.toolsVersion, "0.0.0-20200601");
@@ -497,7 +498,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.deepEqual(result.toolsVersion, defaults.cliVersion);
@@ -537,7 +538,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
t.deepEqual(result.toolsVersion, defaults.cliVersion);
@@ -579,7 +580,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
features,
getRunnerLogger(true),
new RecordingLogger(),
false,
);
@@ -664,7 +665,7 @@ const injectedConfigMacro = makeMacro({
tempDir,
};
thisStubConfig.computedConfig = generateCodeScanningConfig(
getRunnerLogger(true),
new RecordingLogger(),
thisStubConfig.originalUserInput,
augmentationProperties,
);
@@ -1039,7 +1040,7 @@ test.serial("runTool summarizes autobuilder errors", async (t) => {
[2019-09-18 12:00:01] [autobuild] A non-error message
`;
stubToolRunnerConstructor(1, stderr);
const codeqlObject = await codeql.getCodeQLForTesting();
const codeqlObject = await getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
sinon.stub(codeqlObject, "resolveExtractor").resolves("/path/to/extractor");
// io throws because of the test CodeQL object.
@@ -1095,7 +1096,7 @@ test.serial("runTool recognizes fatal internal errors", async (t) => {
com.semmle.util.exception.CatastrophicError: An error occurred while evaluating ControlFlowGraph::ControlFlow::Root.isRootOf/1#dispred#f610e6ed/2@86282cc8
Severe disk cache trouble (corruption or out of space) at /home/runner/work/_temp/codeql_databases/go/db-go/default/cache/pages/28/33.pack: Failed to write item to disk`;
stubToolRunnerConstructor(1, stderr);
const codeqlObject = await codeql.getCodeQLForTesting();
const codeqlObject = await getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
sinon.stub(codeqlObject, "resolveExtractor").resolves("/path/to/extractor");
// io throws because of the test CodeQL object.

View File

@@ -26,7 +26,7 @@ import {
} from "./feature-flags";
import { isAnalyzingDefaultBranch } from "./git-utils";
import { Language } from "./languages";
import { getRunnerLogger, Logger } from "./logging";
import type { Logger } from "./logging";
import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "./overlay";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import * as setupCodeql from "./setup-codeql";
@@ -199,6 +199,7 @@ export interface CodeQL {
sarifFile: string,
automationDetailsId: string | undefined,
config: ActionConfig<"tempDir">,
diagnosticDir: string | undefined,
): Promise<void>;
/** Get the location of an extractor for the specified language. */
resolveExtractor(language: Language): Promise<string>;
@@ -486,18 +487,6 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
};
}
/**
* Get a real, newly created CodeQL instance for testing. The instance refers to
* a non-existent placeholder codeql command, so tests that use this function
* should also stub the toolrunner.ToolRunner constructor.
*/
export async function getCodeQLForTesting(
cmd = "codeql-for-testing",
logger: Logger = getRunnerLogger(true),
): Promise<CodeQL> {
return getCodeQLForCmd(logger, cmd, false);
}
/**
* Return a CodeQL object for CodeQL CLI access.
*
@@ -506,7 +495,7 @@ export async function getCodeQLForTesting(
* version requirement. Must be set to true outside tests.
* @returns A new CodeQL object
*/
async function getCodeQLForCmd(
export async function getCodeQLForCmd(
logger: Logger,
cmd: string,
checkVersion: boolean,
@@ -915,15 +904,43 @@ async function getCodeQLForCmd(
sarifFile: string,
automationDetailsId: string | undefined,
config: ActionConfig<"tempDir">,
diagnosticDir: string | undefined,
): Promise<void> {
// We may be exporting diagnostics for a failed SARIF, in which case the configuration
// file may not have been written to disk and there may not be a database. The CLI
// will produce a SARIF for diagnostics from `diagnosticDir` without it. Therefore,
// we check whether `configPath` exists and only provide it to the CLI if it does.
// Although not needed, the CLI will produce an error if it
// is provided with an argument to `--sarif-codescanning-config` that does not exist.
let configPath: string | undefined =
getGeneratedCodeScanningConfigPath(config);
if (!fs.existsSync(configPath)) {
// Warn if there is no configuration file, even though `config` claims that `init`
// was successful. This shouldn't be happening.
if (config.successfullyInitialised) {
logger.warning(
[
`Expected a configuration file at '${configPath}', which does not exist.`,
"Continuing with diagnostics export anyway, without a configuration file.",
].join(" "),
);
}
configPath = undefined;
}
const args = [
"diagnostics",
"export",
"--format=sarif-latest",
`--output=${sarifFile}`,
`--sarif-codescanning-config=${getGeneratedCodeScanningConfigPath(
config,
)}`,
...(configPath !== undefined
? [`--sarif-codescanning-config=${configPath}`]
: []),
...(diagnosticDir !== undefined
? [`--diagnostic-dir=${diagnosticDir}`]
: []),
...getExtraOptionsFromEnv(["diagnostics", "export"]),
];
if (automationDetailsId !== undefined) {

View File

@@ -21,7 +21,6 @@ import { RepositoryProperties } from "./feature-flags/properties";
import * as gitUtils from "./git-utils";
import { GitVersionInfo } from "./git-utils";
import { BuiltInLanguage, Language } from "./languages";
import { getRunnerLogger } from "./logging";
import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay";
import * as overlayDiagnostics from "./overlay/diagnostics";
import { OverlayDisabledReason } from "./overlay/diagnostics";
@@ -42,6 +41,7 @@ import {
callee,
SAMPLE_DOTCOM_API_DETAILS,
AssertableTarget,
RecordingLogger,
} from "./testing-utils";
import {
GitHubVariant,
@@ -102,7 +102,7 @@ function createTestInitConfigInputs(
features: createFeatures([]),
repositoryProperties: {},
enableFileCoverageInformation: true,
logger: getRunnerLogger(true),
logger: new RecordingLogger(),
} satisfies configUtils.InitConfigInputs,
overrides,
);
@@ -150,7 +150,7 @@ function mockListLanguages(languages: string[]) {
test.serial("load empty config", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const languages = "javascript,python";
setupActionsVars(tempDir, tempDir);
@@ -196,7 +196,7 @@ test.serial("load empty config", async (t) => {
test.serial("load code quality config", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const languages = "actions";
setupActionsVars(tempDir, tempDir);
@@ -252,7 +252,7 @@ test.serial(
"initActionState doesn't throw if there are queries configured in the repository properties",
async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const languages = "javascript";
setupActionsVars(tempDir, tempDir);
@@ -315,7 +315,7 @@ test.serial(
test.serial("loading a saved config produces the same config", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = createStubCodeQL({
async resolveLanguages() {
@@ -363,7 +363,7 @@ test.serial("loading a saved config produces the same config", async (t) => {
test.serial("loading config with version mismatch throws", async (t) => {
return await withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = createStubCodeQL({
async resolveLanguages() {
@@ -788,12 +788,12 @@ test.serial("Unknown languages", async (t) => {
});
});
const mockLogger = getRunnerLogger(true);
const mockLogger = new RecordingLogger();
test.serial("no generateRegistries when registries is undefined", async (t) => {
return await withTmpDir(async (tmpDir) => {
const registriesInput = undefined;
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
@@ -814,7 +814,7 @@ test.serial(
token: "not-a-token",
},
]);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
@@ -2225,7 +2225,7 @@ test("applyIncrementalAnalysisSettings: no-op when mode is not Overlay and diff
const config = createTestConfig({});
config.overlayDatabaseMode = OverlayDatabaseMode.None;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
await configUtils.applyIncrementalAnalysisSettings(
config,
@@ -2243,7 +2243,7 @@ test("applyIncrementalAnalysisSettings: keeps overlay mode and adds exclusions w
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
});
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
await configUtils.applyIncrementalAnalysisSettings(
config,
@@ -2266,7 +2266,7 @@ test.serial(
});
config.useOverlayDatabaseCaching = true;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const addDiagnosticsStub = sinon
.stub(overlayDiagnostics, "addOverlayDisablementDiagnostics")
.resolves();
@@ -2298,7 +2298,7 @@ test.serial(
config.useOverlayDatabaseCaching = false;
config.overlayModeSetExplicitly = true;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const addDiagnosticsStub = sinon
.stub(overlayDiagnostics, "addOverlayDisablementDiagnostics")
.resolves();
@@ -2321,7 +2321,7 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
const config = createTestConfig({});
config.overlayDatabaseMode = OverlayDatabaseMode.None;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
await configUtils.applyIncrementalAnalysisSettings(
config,
@@ -2535,7 +2535,7 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a
// also check whether loading the configuration from disk that was written
// by `determineUserConfig` matches our expectations.
const loadedFromDisk = configUtils.getLocalConfig(
getRunnerLogger(true),
new RecordingLogger(),
expectedConfigPath,
false,
);

View File

@@ -465,6 +465,8 @@ export async function initActionState(
overlayModeSetExplicitly: false,
repositoryProperties,
enableFileCoverageInformation,
// We don't set this to `true` until `init` is successful.
successfullyInitialised: false,
};
}

View File

@@ -130,6 +130,9 @@ export interface Config {
* Whether to enable file coverage information.
*/
enableFileCoverageInformation: boolean;
/** Whether the analysis was successfully initialised using the configuration. */
successfullyInitialised: boolean;
}
/** Properties available in `Config`. */

View File

@@ -2,15 +2,15 @@ import test, { ExecutionContext } from "ava";
import { RepositoryProperties } from "../feature-flags/properties";
import { BuiltInLanguage, Language } from "../languages";
import { getRunnerLogger } from "../logging";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
makeMacro,
RecordingLogger,
setupActionsVars,
} from "../testing-utils";
import { ConfigurationError, prettyPrintPack } from "../util";
import { ConfigurationError, prettyPrintPack, withTmpDir } from "../util";
import * as dbConfig from "./db-config";
@@ -384,7 +384,7 @@ calculateAugmentationErrorMacro(
test("parseUserConfig - successfully parses valid YAML", (t) => {
const result = dbConfig.parseUserConfig(
getRunnerLogger(true),
new RecordingLogger(),
"test",
`
paths-ignore:
@@ -410,7 +410,7 @@ test("parseUserConfig - throws a ConfigurationError if the file is not valid YAM
t.throws(
() =>
dbConfig.parseUserConfig(
getRunnerLogger(true),
new RecordingLogger(),
"test",
`
paths-ignore:
@@ -562,74 +562,89 @@ test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplie
t.deepEqual(result, configFile);
});
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
test.serial(
"mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
configFile,
);
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
},
);
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
test.serial(
"mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup",
async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
},
} as unknown as dbConfig.DefaultSetupConfig,
},
} as unknown as dbConfig.DefaultSetupConfig,
},
configFile,
);
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(
", ",
);
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
},
);
/** Parses `contents` as a configuration without validating it. */
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {
return dbConfig.parseUserConfig(
getRunnerLogger(true),
new RecordingLogger(),
"test",
contents,
false,

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

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

View File

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

View File

@@ -10,7 +10,6 @@ import {
exportedForTesting,
} from "./diff-informed-analysis-utils";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
setupTests,
@@ -19,6 +18,7 @@ import {
mockFeatureFlagApiEndpoint,
setupActionsVars,
makeMacro,
RecordingLogger,
} from "./testing-utils";
import { GitHubVariant, withTmpDir } from "./util";
import type { GitHubVersion } from "./util";
@@ -55,7 +55,7 @@ const testShouldPerformDiffInformedAnalysis = makeMacro({
setupActionsVars(tmpDir, tmpDir);
const testCase = { ...defaultTestCase, ...partialTestCase };
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = mockCodeQLVersion(testCase.codeQLVersion);
if (testCase.diffInformedQueriesEnvVar !== undefined) {
@@ -182,7 +182,7 @@ test.serial(
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
@@ -207,7 +207,7 @@ test.serial(
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = mockCodeQLVersion("2.21.0");
// A features implementation whose getValue rejects, simulating an
// unexpected failure when determining whether diff-informed analysis
@@ -237,7 +237,7 @@ test.serial(
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
@@ -274,7 +274,7 @@ test.serial(
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
@@ -313,7 +313,7 @@ function runGetDiffRanges(changes: number, patch: string[] | undefined): any {
changes,
patch: patch?.join("\n"),
},
getRunnerLogger(true),
new RecordingLogger(),
);
}

View File

@@ -3,7 +3,6 @@
*/
export enum DocUrl {
ASSIGNING_PERMISSIONS_TO_JOBS = "https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs",
AUTOMATIC_BUILD_FAILED = "https://docs.github.com/en/code-security/code-scanning/troubleshooting-code-scanning/automatic-build-failed",
CODEQL_BUILD_MODES = "https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages#codeql-build-modes",
DEFINE_ENV_VARIABLES = "https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow",

View File

@@ -74,7 +74,6 @@ export enum Feature {
AllowMergeConfigFiles = "allow_merge_config_files",
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
CleanupTrapCaches = "cleanup_trap_caches",
/** Whether to allow the `config-file` input to be specified via a repository property. */
ConfigFileRepositoryProperty = "config_file_repository_property",
CppDependencyInstallation = "cpp_dependency_installation_enabled",
@@ -106,19 +105,6 @@ export enum Feature {
OverlayAnalysisGo = "overlay_analysis_go",
OverlayAnalysisJava = "overlay_analysis_java",
OverlayAnalysisJavascript = "overlay_analysis_javascript",
/**
* When set, chooses the default CodeQL CLI version as the highest version that is both enabled by
* feature flags and present as an overlay-base database in the Actions cache for the configured
* languages. Falls back to the highest feature flagged version if no intersecting overlay-base
* database exists in the cache.
*/
OverlayAnalysisMatchCodeqlVersion = "overlay_analysis_match_codeql_version",
/**
* Like `OverlayAnalysisMatchCodeqlVersion`, but only logs a diagnostic with the version that
* would have been chosen instead of actually changing the default CodeQL CLI version.
* `OverlayAnalysisMatchCodeqlVersion` overrides this flag.
*/
OverlayAnalysisMatchCodeqlVersionDryRun = "overlay_analysis_match_codeql_version_dry_run",
/**
* Lowers the overlay minimum available disk space to 8 GB. The lowest enabled limit wins; if
* none are enabled, the default applies.
@@ -214,11 +200,6 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
minimumVersion: undefined,
},
[Feature.CleanupTrapCaches]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
minimumVersion: undefined,
},
[Feature.ConfigFileRepositoryProperty]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CONFIG_FILE_REPOSITORY_PROPERTY",
@@ -367,16 +348,6 @@ export const featureConfig = {
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION_RUBY,
},
// Other overlay-related feature flags
[Feature.OverlayAnalysisMatchCodeqlVersion]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MATCH_CODEQL_VERSION",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMatchCodeqlVersionDryRun]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MATCH_CODEQL_VERSION_DRY_RUN",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk8Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_8_GB",

View File

@@ -2,7 +2,6 @@ import test from "ava";
import * as sinon from "sinon";
import * as api from "../api-client";
import { getRunnerLogger } from "../logging";
import { parseRepositoryNwo } from "../repository";
import { RecordingLogger, setupTests } from "../testing-utils";
@@ -19,7 +18,7 @@ test.serial(
url: "",
data: {},
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
await t.throwsAsync(
properties.loadPropertiesFromApi(logger, mockRepositoryNwo),
@@ -39,7 +38,7 @@ test.serial(
url: "",
data: [{}],
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
await t.throwsAsync(
properties.loadPropertiesFromApi(logger, mockRepositoryNwo),
@@ -63,7 +62,7 @@ test.serial(
{ property_name: "also-not-used-by-us", value: ["A", "B", "C"] },
],
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
await t.notThrowsAsync(
properties.loadPropertiesFromApi(logger, mockRepositoryNwo),
@@ -86,7 +85,7 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => {
{ property_name: "unknown-property", value: "something" },
] satisfies properties.GitHubPropertiesResponse,
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
const response = await properties.loadPropertiesFromApi(
logger,
@@ -113,7 +112,7 @@ test.serial("loadPropertiesFromApi parses true boolean property", async (t) => {
{ property_name: "github-codeql-extra-queries", value: "+queries" },
] satisfies properties.GitHubPropertiesResponse,
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const warningSpy = sinon.spy(logger, "warning");
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
const response = await properties.loadPropertiesFromApi(
@@ -141,7 +140,7 @@ test.serial(
},
] satisfies properties.GitHubPropertiesResponse,
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const warningSpy = sinon.spy(logger, "warning");
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
const response = await properties.loadPropertiesFromApi(
@@ -164,7 +163,7 @@ test.serial(
url: "",
data: [{ property_name: "github-codeql-extra-queries", value: 123 }],
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
await t.throwsAsync(
properties.loadPropertiesFromApi(logger, mockRepositoryNwo),
@@ -190,7 +189,7 @@ test.serial(
},
] satisfies properties.GitHubPropertiesResponse,
});
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const warningSpy = sinon.spy(logger, "warning");
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
const response = await properties.loadPropertiesFromApi(

View File

@@ -8,11 +8,12 @@ import {
FeatureWithoutCLI,
initFeatures,
} from "../feature-flags";
import { getRunnerLogger } from "../logging";
import type { Logger } from "../logging";
import { parseRepositoryNwo } from "../repository";
import {
LoggedMessage,
mockCodeQLVersion,
RecordingLogger,
setupActionsVars,
} from "../testing-utils";
import { ToolsFeature } from "../tools-features";
@@ -51,7 +52,7 @@ export function assertAllFeaturesUndefinedInApi(
export function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),
logger: Logger = new RecordingLogger(),
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion,
): FeatureEnablement {
setupActionsVars(tmpDir, tmpDir);

View File

@@ -5,9 +5,8 @@ import * as ava from "ava";
import test from "ava";
import * as fingerprints from "./fingerprints";
import { getRunnerLogger } from "./logging";
import * as sarif from "./sarif";
import { setupTests } from "./testing-utils";
import { RecordingLogger, setupTests } from "./testing-utils";
import * as util from "./util";
setupTests(test);
@@ -131,7 +130,7 @@ function testResolveUriToFile(uri: any, index: any, artifactsURIs: any[]) {
location,
artifacts,
process.cwd(),
getRunnerLogger(true),
new RecordingLogger(),
);
}
@@ -218,7 +217,7 @@ test("addFingerprints", async (t) => {
await fingerprints.addFingerprints(
input,
sourceRoot,
getRunnerLogger(true),
new RecordingLogger(),
),
expected,
);
@@ -246,7 +245,7 @@ test("missingRegions", async (t) => {
await fingerprints.addFingerprints(
input,
sourceRoot,
getRunnerLogger(true),
new RecordingLogger(),
),
expected,
);

View File

@@ -1,3 +1,6 @@
import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
@@ -8,10 +11,10 @@ import * as apiClient from "./api-client";
import * as codeql from "./codeql";
import * as configUtils from "./config-utils";
import * as debugArtifacts from "./debug-artifacts";
import { getTempDiagnosticsRoot } from "./diagnostics";
import { EnvVar } from "./environment";
import { Feature } from "./feature-flags";
import * as initActionPostHelper from "./init-action-post-helper";
import { getRunnerLogger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import * as overlayStatus from "./overlay/status";
import { parseRepositoryNwo } from "./repository";
@@ -20,6 +23,7 @@ import {
createFeatures,
createTestConfig,
DEFAULT_ACTIONS_VARS,
getCodeQLForTesting,
getTestEnv,
makeMacro,
makeVersionInfo,
@@ -35,6 +39,47 @@ const NUM_BYTES_PER_GIB = 1024 * 1024 * 1024;
setupTests(test);
test.serial("generateFailedSarif picks up unwritten diagnostics", async (t) =>
util.withTmpDir(async (tmpDir) => {
const logger = new RecordingLogger();
const tempDiagDir = getTempDiagnosticsRoot(tmpDir);
fs.mkdirSync(tempDiagDir, { recursive: true });
const codeqlObject = await getCodeQLForTesting();
const diagnosticsExport = sinon
.stub(codeqlObject, "diagnosticsExport")
.resolves();
sinon.stub(codeql, "getCodeQL").resolves(codeqlObject);
const sarifFile = path.join(tmpDir, "failed.sarif");
const result = await initActionPostHelper.generateFailedSarif(
logger,
createFeatures([Feature.ExportDiagnosticsEnabled]),
createTestConfig({
tempDir: tmpDir,
// Explicitly undefined to ensure we get the "no database" path.
dbLocation: undefined,
}),
"category",
"",
sarifFile,
);
t.truthy(result);
t.is(diagnosticsExport.callCount, 1);
t.true(
diagnosticsExport.calledOnceWith(
sarifFile,
"category",
sinon.match.any,
tempDiagDir,
),
);
}),
);
test.serial("init-post action with debug mode off", async (t) => {
return await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -62,7 +107,7 @@ test.serial("init-post action with debug mode off", async (t) => {
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(uploadAllAvailableDebugArtifactsSpy.notCalled);
@@ -86,7 +131,7 @@ test.serial("init-post action with debug mode on", async (t) => {
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(uploadAllAvailableDebugArtifactsSpy.called);
@@ -383,7 +428,7 @@ test.serial(
createFeatures([Feature.OverlayAnalysisStatusSave]),
"success",
getTestEnv(),
getRunnerLogger(true),
new RecordingLogger(),
);
t.true(
@@ -453,7 +498,7 @@ test.serial(
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
new RecordingLogger(),
);
t.true(
@@ -497,7 +542,7 @@ test.serial("does not save overlay status when build successful", async (t) => {
createFeatures([Feature.OverlayAnalysisStatusSave]),
"success",
env,
getRunnerLogger(true),
new RecordingLogger(),
);
t.true(
@@ -536,7 +581,7 @@ test.serial(
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
new RecordingLogger(),
);
t.true(
@@ -589,7 +634,7 @@ async function runOverlayPostStep({
createFeatures([Feature.OverlayAnalysisStatusSave]),
jobStatus,
env,
getRunnerLogger(true),
new RecordingLogger(),
);
return { saveOverlayStatusStub };
@@ -737,7 +782,7 @@ async function testFailedSarifUpload(
.withArgs("matrix")
.returns(JSON.stringify(matrix));
const codeqlObject = await codeql.getCodeQLForTesting();
const codeqlObject = await getCodeQLForTesting();
sinon.stub(codeql, "getCodeQL").resolves(codeqlObject);
sinon.stub(codeqlObject, "getVersion").resolves(makeVersionInfo("2.17.6"));
const databaseExportDiagnosticsStub = sinon.stub(
@@ -764,7 +809,7 @@ async function testFailedSarifUpload(
config,
parseRepositoryNwo("github/codeql-action"),
createFeatures(features),
getRunnerLogger(true),
new RecordingLogger(),
);
if (expectUpload) {
t.deepEqual(result, {
@@ -833,7 +878,7 @@ async function mockRiskAssessmentEnv(matrix: string) {
.stub(apiClient, "getGitHubVersion")
.resolves({ type: util.GitHubVariant.GHES, version: "3.0.0" });
const codeqlObject = await codeql.getCodeQLForTesting();
const codeqlObject = await getCodeQLForTesting();
const databaseExportDiagnostics = sinon
.stub(codeqlObject, "databaseExportDiagnostics")
.resolves();

View File

@@ -24,6 +24,7 @@ import {
type UploadDebugArtifactsConfig,
} from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching";
import { getTempDiagnosticsRoot } from "./diagnostics";
import { EnvVar, ReadOnlyEnv } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
@@ -177,7 +178,16 @@ async function prepareFailedSarif(
}
}
async function generateFailedSarif(
/**
* Generates a SARIF file for a failed run, i.e. an analysis where no SARIF was produced as a result of the analysis.
*
* Not intended to be called directly, other than by `prepareFailedSarif`.
*
* @param category The analysis category to include in the SARIF.
* @param checkoutPath The path where the repository was checked out at.
* @param sarifFile The SARIF file to produce.
*/
export async function generateFailedSarif(
logger: Logger,
features: FeatureEnablement,
config: ActionConfig<"codeQLCmd" | "tempDir">,
@@ -194,11 +204,26 @@ async function generateFailedSarif(
}
// If there is no database or the feature flag is off, we run 'export diagnostics'
if (
databasePath === undefined ||
!(await features.getValue(Feature.ExportDiagnosticsEnabled, codeql))
) {
await codeql.diagnosticsExport(sarifFile, category, config);
const exportDiagnosticsEnabled = await features.getValue(
Feature.ExportDiagnosticsEnabled,
codeql,
);
if (databasePath === undefined || !exportDiagnosticsEnabled) {
// If we don't have a database path, then we may have diagnostics that were written to a
// temporary location. Get the path that they would have been written to and check whether
// it exists. If so, pass it on to the CLI. The CLI will not complain if we give it a
// path that doesn't exist, so we are just being extra defensive here.
// We also don't pass the path to the CLI if `exportDiagnosticsEnabled` is not enabled, to preserve
// its intended behaviour of controlling whether diagnostics are included in the SARIF or not.
let diagnosticDir: string | undefined = getTempDiagnosticsRoot(
config.tempDir,
);
if (!exportDiagnosticsEnabled || !fs.existsSync(diagnosticDir)) {
diagnosticDir = undefined;
}
await codeql.diagnosticsExport(sarifFile, category, config, diagnosticDir);
} else {
// We call 'database export-diagnostics' to find any per-database diagnostics.
await codeql.databaseExportDiagnostics(databasePath, sarifFile, category);

View File

@@ -40,7 +40,7 @@ import {
addDiagnostic,
addNoLanguageDiagnostic,
flushDiagnostics,
logUnwrittenDiagnostics,
logTemporaryDiagnostics,
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
@@ -484,6 +484,7 @@ async function run(
if (statusReportBase !== undefined) {
await sendStatusReport(statusReportBase);
}
logTemporaryDiagnostics(actionState);
return;
}
@@ -766,14 +767,15 @@ async function run(
);
}
// Write diagnostics to the database that we previously stored in memory because the database
// did not exist until now.
flushDiagnostics(config);
// Move diagnostics to the database that we previously kept in a temporary location
// because the database did not exist until now
flushDiagnostics(actionState, config);
// We save the config here instead of at the end of `initConfig` because we
// may have updated the config returned from `initConfig`, e.g. to revert to
// `OverlayDatabaseMode.None` if we failed to download an overlay-base
// database.
config.successfullyInitialised = true;
await configUtils.saveConfig(config, logger);
core.setOutput("codeql-path", config.codeQLCmd);
@@ -797,7 +799,7 @@ async function run(
);
return;
} finally {
logUnwrittenDiagnostics();
logTemporaryDiagnostics(actionState);
}
await sendCompletedStatusReport(
startedAt,

17
src/logging.test.ts Normal file
View File

@@ -0,0 +1,17 @@
import test from "ava";
import { joinMessageStrings } from "./logging";
import { setupTests } from "./testing-utils";
setupTests(test);
test("joinMessageStrings", async (t) => {
// For strings and errors, it is the identity function.
t.deepEqual(joinMessageStrings("Hello"), "Hello");
const error = new Error("Some error");
t.deepEqual(joinMessageStrings(error), error);
// For arrays of strings, we get a join-ed string.
t.deepEqual(joinMessageStrings(["foo", "bar"]), "foo bar");
});

View File

@@ -1,45 +1,92 @@
import * as core from "@actions/core";
export interface Logger {
debug: (message: string) => void;
info: (message: string) => void;
warning: (message: string | Error) => void;
error: (message: string | Error) => void;
/** Types that all of our logging functions accept. */
export type Loggable = string | string[];
/** Some logging functions also accept errors directly. */
export type LoggableError = Loggable | Error;
export interface Logger {
/**
* Logs {@link message} at debug-level.
* In Actions, this will only be output if workflow debugging is enabled.
*
* @param message
* A string or an array of strings. If an array of strings is provided, they are joined using
* space as the delimiter and printed as a single line.
*/
debug: (message: Loggable) => void;
/**
* Logs {@link message} at info-level.
* In Actions, this will always be output.
*
* @param message
* A string or an array of strings. If an array of strings is provided, they are joined using
* space as the delimiter and printed as a single line.
*/
info: (message: Loggable) => void;
/**
* Logs {@link message} at warning-level.
* In Actions, this will always be output and highlighted as a warning. It will additionally
* result in a warning-level workflow annotation.
*
* @param message
* A string or an array of strings. If an array of strings is provided, they are joined using
* space as the delimiter and printed as a single line.
*/
warning: (message: LoggableError) => void;
/**
* Logs {@link message} at error-level.
* In Actions, this will always be output and highlighted as an error. It will additionally
* result in an error-level workflow annotation.
*
* @param message
* A string or an array of strings. If an array of strings is provided, they are joined using
* space as the delimiter and printed as a single line.
*/
error: (message: LoggableError) => void;
/**
* Gets a value indicating whether debugging is enabled.
*/
isDebug: () => boolean;
startGroup: (name: string) => void;
endGroup: () => void;
}
/** If `message` is an array of strings, the array is `join`-ed into one string separated by spaces. */
export const joinMessageStrings = <T>(
message: string[] | string | T,
): string | T => {
if (Array.isArray(message)) {
return message.join(" ");
} else {
return message;
}
};
/**
* Wraps a logging function so that `joinMessageStrings` is applied to the input before
* calling the wrapped logging function.
*/
const autoJoinMessageStrings = <T>(fn: (msg: string | T) => void) => {
return (message: string[] | string | T) => {
return fn(joinMessageStrings(message));
};
};
export function getActionsLogger(): Logger {
return {
debug: core.debug,
info: core.info,
warning: core.warning,
error: core.error,
debug: autoJoinMessageStrings(core.debug),
info: autoJoinMessageStrings(core.info),
warning: autoJoinMessageStrings(core.warning),
error: autoJoinMessageStrings(core.error),
isDebug: core.isDebug,
startGroup: core.startGroup,
endGroup: core.endGroup,
};
}
export function getRunnerLogger(debugMode: boolean): Logger {
return {
// eslint-disable-next-line no-console
debug: debugMode ? console.debug : () => undefined,
// eslint-disable-next-line no-console
info: console.info,
// eslint-disable-next-line no-console
warning: console.warn,
// eslint-disable-next-line no-console
error: console.error,
isDebug: () => debugMode,
startGroup: () => undefined,
endGroup: () => undefined,
};
}
export function withGroup<T>(groupName: string, f: () => T): T {
core.startGroup(groupName);
try {

View File

@@ -11,11 +11,11 @@ import type { ResolveDatabaseOutput } from "../codeql";
import * as environment from "../environment";
import * as gitUtils from "../git-utils";
import { BuiltInLanguage } from "../languages";
import { getRunnerLogger } from "../logging";
import {
createTestConfig,
makeMacro,
mockCodeQLVersion,
RecordingLogger,
setupTests,
} from "../testing-utils";
import * as utils from "../util";
@@ -63,7 +63,7 @@ const testDownloadOverlayBaseDatabaseFromCache = makeMacro({
const dbLocation = path.join(tmpDir, "db");
await fs.promises.mkdir(dbLocation, { recursive: true });
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const testCase = { ...defaultDownloadTestCase, ...partialTestCase };
const config = createTestConfig({
dbLocation,
@@ -214,7 +214,7 @@ testDownloadOverlayBaseDatabaseFromCache.serial(
);
test.serial("overlay-base database cache keys remain stable", async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const config = createTestConfig({ languages: ["python", "javascript"] });
const codeQlVersion = "2.23.0";
const commitOid = "abc123def456";
@@ -261,7 +261,7 @@ test.serial("overlay-base database cache keys remain stable", async (t) => {
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases returns unique versions sorted latest first",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(apiClient, "listActionsCaches").resolves([
@@ -287,7 +287,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases returns empty list when no caches exist",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(apiClient, "listActionsCaches").resolves([]);
@@ -303,7 +303,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases returns empty list when cache keys are unparseable",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(apiClient, "listActionsCaches").resolves([
@@ -324,7 +324,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases returns the single version when only one cache exists",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(apiClient, "listActionsCaches").resolves([
@@ -344,7 +344,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases resolves language aliases",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
// The alias `c++` should be resolved to "cpp" and match cache entries keyed with "cpp"
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
@@ -365,7 +365,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases de-duplicates resolved language aliases",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
const listActionsCachesStub = sinon
@@ -391,7 +391,7 @@ test.serial(
test.serial(
"getCodeQlVersionsForOverlayBaseDatabases ignores nightly versions with build metadata",
async (t) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(apiClient, "listActionsCaches").resolves([

View File

@@ -6,8 +6,11 @@ import * as sinon from "sinon";
import * as actionsUtil from "../actions-util";
import * as gitUtils from "../git-utils";
import { getRunnerLogger } from "../logging";
import { createTestConfig, setupTests } from "../testing-utils";
import {
createTestConfig,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { withTmpDir } from "../util";
import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from ".";
@@ -27,7 +30,7 @@ test.serial(
),
);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const config = createTestConfig({ dbLocation });
// Mock the getFileOidsUnderPath function to return base OIDs
@@ -91,7 +94,7 @@ test.serial(
),
);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const config = createTestConfig({ dbLocation });
// Mock the getFileOidsUnderPath function to return base OIDs
@@ -169,7 +172,7 @@ test.serial(
),
);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const config = createTestConfig({ dbLocation });
// Mock the getFileOidsUnderPath function to return base OIDs
@@ -232,7 +235,7 @@ test.serial(
),
);
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const config = createTestConfig({ dbLocation });
// Base OIDs (sourceRoot-relative paths)

View File

@@ -14,7 +14,6 @@ import * as diagnostics from "./diagnostics";
import { ActionsEnvVars, Env, EnvVar, ReadOnlyEnv } from "./environment";
import { Feature } from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
import { MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION } from "./per-language-bundles";
import * as setupCodeql from "./setup-codeql";
@@ -22,6 +21,7 @@ import * as tar from "./tar";
import {
LINKED_CLI_VERSION,
LoggedMessage,
RecordingLogger,
SAMPLE_DEFAULT_CLI_VERSION,
SAMPLE_DOTCOM_API_DETAILS,
checkExpectedLogMessages,
@@ -117,7 +117,7 @@ test.serial("convert to semver", (t) => {
try {
const parsedVersion = setupCodeql.convertToSemVer(
version,
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(parsedVersion, expectedVersion);
} catch (e) {
@@ -145,7 +145,7 @@ test.serial(
GitHubVariant.DOTCOM,
false,
features,
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(source, {
@@ -231,7 +231,7 @@ for (const {
GitHubVariant.DOTCOM,
tarSupportsZstd,
features,
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(source.toolsVersion, LINKED_CLI_VERSION.cliVersion);
@@ -597,7 +597,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(result.toolsVersion, "local");
@@ -634,7 +634,7 @@ for (const toolsInput of ["nightly", "nightly-latest"]) {
GitHubVariant.DOTCOM,
true, // tarSupportsZstd
createFeatures([Feature.PerLanguageBundles]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(source, {
@@ -694,7 +694,7 @@ test.serial(
GitHubVariant.DOTCOM,
true, // tarSupportsZstd
features,
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(source.sourceType, "download");
@@ -733,7 +733,7 @@ for (const perLanguageBundles of [false, true]) {
? [Feature.ForceNightly, Feature.PerLanguageBundles]
: [Feature.ForceNightly],
),
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(source.sourceType, "download");
@@ -848,7 +848,7 @@ test.serial(
GitHubVariant.DOTCOM,
true,
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
),
{
message:
@@ -1020,7 +1020,7 @@ test.serial(
t.is(
setupCodeql.tryGetTagNameFromUrl(
"https://github.com/org/codeql-bundle-testing/releases/download/codeql-bundle-v2.19.0/codeql-bundle-linux64.tar.zst",
getRunnerLogger(true),
new RecordingLogger(),
),
"codeql-bundle-v2.19.0",
);
@@ -1032,7 +1032,7 @@ test.serial(
(t) => {
sinon.stub(toolcache, "findAllVersions").returns([]);
t.is(
setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)),
setupCodeql.getLatestToolcacheVersion(new RecordingLogger()),
undefined,
);
},
@@ -1044,7 +1044,7 @@ test.serial(
const testVersions = ["2.3.1", "3.2.1", "1.2.3"];
sinon.stub(toolcache, "findAllVersions").returns(testVersions);
t.is(setupCodeql.getLatestToolcacheVersion(getRunnerLogger(true)), "3.2.1");
t.is(setupCodeql.getLatestToolcacheVersion(new RecordingLogger()), "3.2.1");
},
);
@@ -1070,7 +1070,7 @@ async function fakeOverlayBaseCacheKey(
}
test.serial(
"getCodeQLSource uses overlay-aware default version when requested for a PR",
"getCodeQLSource uses overlay-aware default version for a Code Scanning PR",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -1097,8 +1097,8 @@ test.serial(
SAMPLE_DOTCOM_API_DETAILS,
GitHubVariant.DOTCOM,
false,
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
createFeatures([]),
new RecordingLogger(),
);
t.assert(listStub.calledOnce);
@@ -1109,7 +1109,95 @@ test.serial(
);
test.serial(
"getCodeQLSource skips overlay-aware default version when not requested",
"getCodeQLSource skips the Actions cache when only one default version is enabled",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
process.env[EnvVar.CODE_SCANNING_REF] = "refs/heads/feature-branch";
process.env[EnvVar.CODE_SCANNING_BASE_BRANCH] = "main";
sinon.stub(api, "getAutomationID").resolves("test/");
const listStub = sinon.stub(api, "listActionsCaches").resolves([
{
key: await fakeOverlayBaseCacheKey(
"javascript",
SAMPLE_DEFAULT_CLI_VERSION.enabledVersions[0].cliVersion,
"abc-1-1",
),
},
]);
sinon
.stub(toolcache, "find")
.withArgs(
"CodeQL",
SAMPLE_DEFAULT_CLI_VERSION.enabledVersions[0].cliVersion,
)
.returns("/path/to/codeql");
const source = await setupCodeql.getCodeQLSource(
undefined,
SAMPLE_DEFAULT_CLI_VERSION,
["javascript"],
undefined, // otherLanguagePacksReason
true,
SAMPLE_DOTCOM_API_DETAILS,
GitHubVariant.DOTCOM,
false,
createFeatures([]),
new RecordingLogger(),
);
t.assert(listStub.notCalled);
t.is(source.sourceType, "toolcache");
t.is(
source.toolsVersion,
SAMPLE_DEFAULT_CLI_VERSION.enabledVersions[0].cliVersion,
);
});
},
);
test.serial(
"getCodeQLSource skips overlay-aware default version outside pull request analysis",
async (t) => {
sinon.stub(actionsUtil, "isAnalyzingPullRequest").returns(false);
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon.stub(api, "getAutomationID").resolves("test/");
const listStub = sinon.stub(api, "listActionsCaches").resolves([
{
key: await fakeOverlayBaseCacheKey("javascript", "2.20.1", "abc-1-1"),
},
]);
sinon
.stub(toolcache, "find")
.withArgs("CodeQL", "2.20.2")
.returns("/path/to/codeql-2.20.2");
const source = await setupCodeql.getCodeQLSource(
undefined,
overlayMatchEnabledVersions,
["javascript"],
undefined, // otherLanguagePacksReason
true,
SAMPLE_DOTCOM_API_DETAILS,
GitHubVariant.DOTCOM,
false,
createFeatures([]),
new RecordingLogger(),
);
t.assert(listStub.notCalled);
t.is(source.sourceType, "toolcache");
t.is(source.toolsVersion, "2.20.2");
});
},
);
test.serial(
"getCodeQLSource skips overlay-aware default version when not requested for Code Scanning",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -1136,8 +1224,8 @@ test.serial(
SAMPLE_DOTCOM_API_DETAILS,
GitHubVariant.DOTCOM,
false,
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
createFeatures([]),
new RecordingLogger(),
);
t.assert(listStub.notCalled);
@@ -1175,7 +1263,7 @@ test.serial(
GitHubVariant.DOTCOM,
true, // tarSupportsZstd
createFeatures([Feature.PerLanguageBundles]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(source.sourceType, "download");
@@ -1226,7 +1314,7 @@ test.serial(
GitHubVariant.DOTCOM,
true, // tarSupportsZstd
features,
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(source.sourceType, "download");
@@ -1296,7 +1384,7 @@ for (const fallback of [false, true]) {
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([Feature.PerLanguageBundles]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.true(fetchRelease.calledTwice);
@@ -1561,7 +1649,7 @@ for (const error of [
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([Feature.PerLanguageBundles]),
getRunnerLogger(true),
new RecordingLogger(),
),
{ is: error },
);
@@ -1597,7 +1685,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
),
{ is: error },
);
@@ -1608,7 +1696,7 @@ test.serial(
);
test.serial(
"getEnabledVersionsWithOverlayBaseDatabases returns flag-enabled versions present in cache, sorted desc",
"getEnabledVersionsWithOverlayBaseDatabases returns enabled versions present in cache, sorted desc",
async (t) => {
sinon.stub(api, "getAutomationID").resolves("test/");
sinon.stub(api, "listActionsCaches").resolves([
@@ -1626,21 +1714,24 @@ test.serial(
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
]);
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result =
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
new RecordingLogger(),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
]);
});
},
);
test.serial(
"getEnabledVersionsWithOverlayBaseDatabases returns empty when no cached version is flag-enabled",
"getEnabledVersionsWithOverlayBaseDatabases returns empty when no cached version is enabled",
async (t) => {
sinon.stub(api, "getAutomationID").resolves("test/");
sinon.stub(api, "listActionsCaches").resolves([
@@ -1652,8 +1743,7 @@ test.serial(
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(result, []);
},
@@ -1669,8 +1759,7 @@ const noLanguagesMacro = makeMacro({
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
rawLanguages,
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(result, []);
t.assert(
@@ -1694,8 +1783,7 @@ test.serial(
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(result, []);
},
@@ -1714,8 +1802,7 @@ test.serial(
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.deepEqual(result, [
{ cliVersion: "2.20.2", tagName: "codeql-bundle-v2.20.2" },
@@ -1723,78 +1810,6 @@ test.serial(
},
);
test.serial(
"getEnabledVersionsWithOverlayBaseDatabases does not list caches when both gates are off",
async (t) => {
const listStub = sinon.stub(api, "listActionsCaches").resolves([]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([]),
getRunnerLogger(true),
);
t.deepEqual(result, []);
t.assert(
listStub.notCalled,
"Should not list Actions caches when both gating feature flags are off.",
);
},
);
test.serial(
"getEnabledVersionsWithOverlayBaseDatabases dry-run returns empty but lists caches",
async (t) => {
sinon.stub(api, "getAutomationID").resolves("test/");
const listStub = sinon.stub(api, "listActionsCaches").resolves([
{
key: await fakeOverlayBaseCacheKey("javascript", "2.20.1", "abc-1-1"),
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersionDryRun]),
getRunnerLogger(true),
);
t.deepEqual(
result,
[],
"Dry-run should return an empty list so the caller falls back.",
);
t.assert(
listStub.calledOnce,
"Dry-run should still list Actions caches to populate the diagnostic.",
);
},
);
test.serial(
"getEnabledVersionsWithOverlayBaseDatabases match flag wins over dry-run",
async (t) => {
sinon.stub(api, "getAutomationID").resolves("test/");
sinon.stub(api, "listActionsCaches").resolves([
{
key: await fakeOverlayBaseCacheKey("javascript", "2.20.1", "abc-1-1"),
},
]);
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
overlayMatchEnabledVersions,
["javascript"],
createFeatures([
Feature.OverlayAnalysisMatchCodeqlVersion,
Feature.OverlayAnalysisMatchCodeqlVersionDryRun,
]),
getRunnerLogger(true),
);
t.deepEqual(result, [
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
]);
},
);
/** The CLI version that the toolcache cleanup tests download. */
const CLEANUP_CLI_VERSION = "2.21.0";
/** The bundle version that the toolcache cleanup tests download. */
@@ -2093,7 +2108,7 @@ test.serial(
env: new ReadOnlyEnv({
[ActionsEnvVars.RUNNER_TOOL_CACHE]: injectedRoot,
}),
logger: getRunnerLogger(true),
logger: new RecordingLogger(),
});
t.deepEqual(result, {
@@ -2248,7 +2263,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(
@@ -2288,7 +2303,7 @@ test.serial(
undefined, // otherLanguagePacksReason
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRunnerLogger(true),
new RecordingLogger(),
),
{ is: error },
);
@@ -2403,7 +2418,7 @@ test.serial(
"isToolcacheOnWorkspaceFilesystem assumes a different filesystem when it cannot tell",
async (t) => {
await withTmpDir(async (tmpDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
setupActionsVars(tmpDir, tmpDir);
t.true(toolsDownload.isToolcacheOnWorkspaceFilesystem(logger));

View File

@@ -183,29 +183,16 @@ async function findOverridingToolsInCache(
/**
* Returns the sorted set of enabled versions that have cached overlay-base databases for the
* given languages, or an empty list if neither the `OverlayAnalysisMatchCodeqlVersion` nor the
* `OverlayAnalysisMatchCodeqlVersionDryRun` feature flag is enabled. When only the dry-run flag
* is enabled, this performs the lookup and emits a telemetry diagnostic with the version that
* would have been chosen, but still returns an empty list so the caller falls back.
* given languages.
*/
export async function getEnabledVersionsWithOverlayBaseDatabases(
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
features: FeatureEnablement,
logger: Logger,
): Promise<CodeQLVersionInfo[]> {
if (rawLanguages === undefined || rawLanguages.length === 0) {
return [];
}
const isEnabled = await features.getValue(
Feature.OverlayAnalysisMatchCodeqlVersion,
);
const isDryRun =
!isEnabled &&
(await features.getValue(Feature.OverlayAnalysisMatchCodeqlVersionDryRun));
if (!isEnabled && !isDryRun) {
return [];
}
let cachedVersions: string[] | undefined;
try {
@@ -226,66 +213,35 @@ export async function getEnabledVersionsWithOverlayBaseDatabases(
}
const cachedVersionsSet = new Set(cachedVersions);
const overlayVersions = defaultCliVersion.enabledVersions.filter((v) =>
return defaultCliVersion.enabledVersions.filter((v) =>
cachedVersionsSet.has(v.cliVersion),
);
if (overlayVersions.length === 0) {
return [];
}
const isCachedVersionDifferent =
overlayVersions[0].cliVersion !==
defaultCliVersion.enabledVersions[0].cliVersion;
if (isCachedVersionDifferent) {
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/overlay-aware-default-codeql-version",
"Overlay-aware default CodeQL version selection",
{
cachedVersions,
enabledVersions: defaultCliVersion.enabledVersions.map(
(v) => v.cliVersion,
),
isDryRun,
overlayAwareVersion: overlayVersions[0].cliVersion,
},
),
);
}
if (isDryRun) {
logger.debug(
`Overlay-aware default CodeQL version selection is running in dry-run mode. Would have used version ${overlayVersions[0].cliVersion}.`,
);
return [];
}
return overlayVersions;
}
/**
* Resolves the newest enabled default CLI version that has a cached overlay-base database for the
* relevant languages, if running a Code Scanning analysis for a pull request and one exists.
* Otherwise, falls back to the newest enabled default CLI version.
* relevant languages, if this is a Code Scanning analysis of a pull request, multiple default
* versions are enabled, and such a version exists. Otherwise, falls back to the newest enabled
* default CLI version.
*/
async function resolveDefaultCliVersion(
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
useOverlayAwareDefaultCliVersion: boolean,
features: FeatureEnablement,
logger: Logger,
): Promise<CodeQLVersionInfo> {
if (!useOverlayAwareDefaultCliVersion || !isAnalyzingPullRequest()) {
if (
!useOverlayAwareDefaultCliVersion ||
// Listing the Actions cache can't change which version we use if only one version is enabled.
defaultCliVersion.enabledVersions.length === 1 ||
!isAnalyzingPullRequest()
) {
return defaultCliVersion.enabledVersions[0];
}
const overlayVersions = await getEnabledVersionsWithOverlayBaseDatabases(
defaultCliVersion,
rawLanguages,
features,
logger,
);
if (overlayVersions.length > 0) {
@@ -482,7 +438,6 @@ export async function getCodeQLSource(
defaultCliVersion,
rawLanguages,
useOverlayAwareDefaultCliVersion,
features,
logger,
);
cliVersion = version.cliVersion;
@@ -505,7 +460,6 @@ export async function getCodeQLSource(
defaultCliVersion,
rawLanguages,
useOverlayAwareDefaultCliVersion,
features,
logger,
);
cliVersion = version.cliVersion;

View File

@@ -13,7 +13,7 @@ import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
import { UnvalidatedObject, validateSchema } from "./json";
import { makeFromSchema } from "./json/testing-util";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger, Logger } from "./logging";
import { Logger } from "./logging";
import { BundlePlatform, getBundlePlatform } from "./platform";
import * as repository from "./setup/repository";
import * as startProxyExports from "./start-proxy";
@@ -67,7 +67,7 @@ test("getCredentials prefers registriesCredentials over registrySecrets", async
]);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
registrySecrets,
registryCredentials,
undefined,
@@ -84,7 +84,7 @@ test("getCredentials throws an error when configurations are not an array", asyn
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
registryCredentials,
undefined,
@@ -103,7 +103,7 @@ test("getCredentials throws error when credential is not an object", async (t) =
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
testCredential,
undefined,
@@ -124,7 +124,7 @@ test("getCredentials throws error when credential is missing type", async (t) =>
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
testCredential,
undefined,
@@ -147,7 +147,7 @@ test("getCredentials throws error when credential missing host and url", async (
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
testCredential,
undefined,
@@ -161,7 +161,7 @@ test("getCredentials throws error when credential missing host and url", async (
test("getCredentials filters by language when specified", async (t) => {
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON(mixedCredentials),
BuiltInLanguage.java,
@@ -172,7 +172,7 @@ test("getCredentials filters by language when specified", async (t) => {
test("getCredentials returns all for a language when specified", async (t) => {
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON([...mixedCredentials, gitSourceCredential]),
BuiltInLanguage.go,
@@ -192,7 +192,7 @@ test("getCredentials returns all goproxy_servers for Go when specified", async (
];
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON(multipleGoproxyServers),
BuiltInLanguage.go,
@@ -221,7 +221,7 @@ test("getCredentials returns all maven_repositories for Java when specified", as
];
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON(multipleMavenRepositories),
BuiltInLanguage.java,
@@ -239,7 +239,7 @@ test("getCredentials returns all credentials when no language specified", async
const credentialsInput = toEncodedJSON(mixedCredentials);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
undefined,
@@ -269,7 +269,7 @@ test("getCredentials throws an error when non-printable characters are used", as
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
undefined,
@@ -300,7 +300,7 @@ for (const oidcSchemaInfo of startProxyExports.oidcSchemas) {
t.throws(
() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
undefined,
@@ -324,7 +324,7 @@ test("getCredentials accepts OIDC configurations", (t) => {
);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON(oidcConfigurations),
BuiltInLanguage.csharp,
@@ -514,7 +514,7 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
]);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
BuiltInLanguage.java,
@@ -533,7 +533,7 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
};
t.throws(() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON([{ ...baseInvalid, "replaces-base": null }]),
BuiltInLanguage.java,
@@ -541,7 +541,7 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
);
t.throws(() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON([{ ...baseInvalid, "replaces-base": 123 }]),
BuiltInLanguage.java,
@@ -549,7 +549,7 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
);
t.throws(() =>
startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
toEncodedJSON([{ ...baseInvalid, "replaces-base": "true" }]),
BuiltInLanguage.java,
@@ -565,7 +565,7 @@ test("getCredentials returns only ALWAYS_ENABLED_REGISTRY_TYPE credentials for A
]);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
BuiltInLanguage.actions,
@@ -597,7 +597,7 @@ test("getCredentials always returns ALWAYS_ENABLED_REGISTRY_TYPE credentials for
// Test all languages.
for (const language of Object.values(BuiltInLanguage)) {
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
new RecordingLogger(),
undefined,
credentialsInput,
language,

View File

@@ -6,7 +6,6 @@ import * as actionsUtil from "./actions-util";
import { Config } from "./config-utils";
import { EnvVar, RegistryProxyVars } from "./environment";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import { ToolsSource } from "./setup-codeql";
import type { Registry } from "./start-proxy";
import {
@@ -154,7 +153,7 @@ test.serial("createStatusReportBase", async (t) => {
languages: [BuiltInLanguage.java, BuiltInLanguage.swift],
}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
);
@@ -203,7 +202,7 @@ test.serial("createStatusReportBase - empty configuration", async (t) => {
new Date("May 19, 2023 05:19:00"),
{},
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
);
if (t.truthy(statusReport)) {
@@ -225,7 +224,7 @@ test.serial("createStatusReportBase - partial configuration", async (t) => {
languages: ["go"],
},
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
);
if (t.truthy(statusReport)) {
@@ -248,7 +247,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -264,7 +263,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -281,7 +280,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -297,7 +296,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -314,7 +313,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -330,7 +329,7 @@ test.serial("createStatusReportBase_firstParty", async (t) => {
new Date("May 19, 2023 05:19:00"),
createTestConfig({}),
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
)
@@ -411,7 +410,7 @@ const testCreateInitWithConfigStatusReport = makeMacro({
new Date("May 19, 2023 05:19:00"),
config,
{ numAvailableBytes: 100, numTotalBytes: 500 },
getRunnerLogger(false),
new RecordingLogger(),
"failure cause",
"exception stack trace",
);

View File

@@ -3,9 +3,8 @@ import * as stream from "stream";
import test from "ava";
import { getRunnerLogger } from "./logging";
import { extractTarZst } from "./tar";
import { setupTests } from "./testing-utils";
import { RecordingLogger, setupTests } from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
@@ -17,7 +16,7 @@ test("extractTarZst rejects if the input stream errors", async (t) => {
archive,
path.join(tmpDir, "dest"),
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
);
archive.destroy(

View File

@@ -30,7 +30,7 @@ import {
featureConfig,
FeatureEnablement,
} from "./feature-flags";
import { Logger } from "./logging";
import { joinMessageStrings, Loggable, LoggableError, Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { getBundlePlatform } from "./platform";
import { ActionName } from "./status-report";
@@ -675,12 +675,13 @@ export class RecordingLogger implements Logger {
constructor(private readonly logToConsole: boolean = true) {}
private addMessage(level: LogLevel, message: string | Error): void {
this.messages.push({ type: level, message });
private addMessage(level: LogLevel, message: LoggableError): void {
const joinedMessage = joinMessageStrings(message);
this.messages.push({ type: level, message: joinedMessage });
if (this.logToConsole) {
// eslint-disable-next-line no-console
console.debug(message);
console.debug(joinedMessage);
}
}
@@ -701,19 +702,19 @@ export class RecordingLogger implements Logger {
return true;
}
debug(message: string) {
debug(message: Loggable) {
this.addMessage("debug", message);
}
info(message: string) {
info(message: Loggable) {
this.addMessage("info", message);
}
warning(message: string | Error) {
warning(message: LoggableError) {
this.addMessage("warning", message);
}
error(message: string | Error) {
error(message: LoggableError) {
this.addMessage("error", message);
}
@@ -1048,6 +1049,7 @@ export function createTestConfig(overrides: Partial<Config>): Config {
overlayModeSetExplicitly: false,
repositoryProperties: {},
enableFileCoverageInformation: true,
successfullyInitialised: false,
} satisfies Config,
overrides,
);
@@ -1058,3 +1060,15 @@ export function makeTestToken(length: number = 36) {
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return chars.repeat(Math.ceil(length / chars.length)).slice(0, length);
}
/**
* Get a real, newly created CodeQL instance for testing. The instance refers to
* a non-existent placeholder codeql command, so tests that use this function
* should also stub the toolrunner.ToolRunner constructor.
*/
export async function getCodeQLForTesting(
cmd = "codeql-for-testing",
logger: Logger = new RecordingLogger(),
): Promise<codeql.CodeQL> {
return codeql.getCodeQLForCmd(logger, cmd, false);
}

View File

@@ -10,9 +10,8 @@ import { https } from "follow-redirects";
import nock from "nock";
import * as sinon from "sinon";
import { getRunnerLogger } from "./logging";
import * as tar from "./tar";
import { setupTests } from "./testing-utils";
import { RecordingLogger, setupTests } from "./testing-utils";
import { downloadAndExtract } from "./tools-download";
import * as util from "./util";
@@ -34,7 +33,7 @@ test.serial(
undefined,
{},
undefined,
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
@@ -72,7 +71,7 @@ test.serial(
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
@@ -105,7 +104,7 @@ test.serial(
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
),
{
instanceOf: util.HTTPError,
@@ -149,7 +148,7 @@ test.serial(
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
@@ -189,7 +188,7 @@ test.serial(
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
);
t.is(
@@ -237,7 +236,7 @@ test.serial(
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
new RecordingLogger(),
);
t.assert(Number.isInteger(statusReport.totalDurationMs));

View File

@@ -4,11 +4,12 @@ import * as path from "path";
import test from "ava";
import * as sinon from "sinon";
import { CodeQL, getCodeQLForTesting } from "./codeql";
import type { CodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { BuiltInLanguage } from "./languages";
import {
createTestConfig,
getCodeQLForTesting,
makeVersionInfo,
RecordingLogger,
setupTests,

View File

@@ -6,29 +6,26 @@ import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as apiClient from "./api-client";
import {
getTrapCachingExtractorConfigArgs,
getTrapCachingExtractorConfigArgsForLang,
createStubCodeQL,
} from "./codeql";
import * as configUtils from "./config-utils";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import {
createFeatures,
callee,
createTestConfig,
getRecordingLogger,
makeVersionInfo,
setupTests,
} from "./testing-utils";
import {
cleanupTrapCaches,
downloadTrapCaches,
getLanguagesSupportingCaching,
uploadTrapCaches,
warnIfTrapCacheCleanupEnvVarIsSet,
} from "./trap-caching";
import * as util from "./util";
@@ -200,83 +197,27 @@ test.serial(
},
);
test.serial("cleanup removes only old CodeQL TRAP caches", async (t) => {
await util.withTmpDir(async (tmpDir) => {
// This config specifies that we are analyzing JavaScript and Ruby, but not Swift.
const config = getTestConfigWithTempDir(tmpDir);
sinon.stub(gitUtils, "getRef").resolves("refs/heads/main");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const listStub = sinon.stub(apiClient, "listActionsCaches").resolves([
// Should be kept, since it's not relevant to CodeQL. In reality, the API shouldn't return
// this in the first place, but this is a defensive check.
{
id: 1,
key: "some-other-key",
created_at: "2024-05-23T14:25:00Z",
size_in_bytes: 100 * 1024 * 1024,
},
// Should be kept, since it's the newest TRAP cache for JavaScript
{
id: 2,
key: "codeql-trap-1-2.0.0-javascript-newest",
created_at: "2024-04-23T14:25:00Z",
size_in_bytes: 50 * 1024 * 1024,
},
// Should be cleaned up
{
id: 3,
key: "codeql-trap-1-2.0.0-javascript-older",
created_at: "2024-03-22T14:25:00Z",
size_in_bytes: 200 * 1024 * 1024,
},
// Should be cleaned up
{
id: 4,
key: "codeql-trap-1-2.0.0-javascript-oldest",
created_at: "2024-02-21T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since it's the newest TRAP cache for Ruby
{
id: 5,
key: "codeql-trap-1-2.0.0-ruby-newest",
created_at: "2024-02-20T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since we aren't analyzing Swift
{
id: 6,
key: "codeql-trap-1-2.0.0-swift-newest",
created_at: "2024-02-22T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
// Should be kept, since we aren't analyzing Swift
{
id: 7,
key: "codeql-trap-1-2.0.0-swift-older",
created_at: "2024-02-21T14:25:00Z",
size_in_bytes: 300 * 1024 * 1024,
},
]);
const deleteStub = sinon.stub(apiClient, "deleteActionsCache").resolves();
const statusReport = await cleanupTrapCaches(
config,
createFeatures([Feature.CleanupTrapCaches]),
getRunnerLogger(true),
// `CODEQL_ACTION_CLEANUP_TRAP_CACHES` has no effect with either value, so we warn even when it's
// set to `false`.
for (const value of ["true", "false"]) {
test(`warnIfTrapCacheCleanupEnvVarIsSet warns when CODEQL_ACTION_CLEANUP_TRAP_CACHES is '${value}'`, async (t) => {
const target = callee(warnIfTrapCacheCleanupEnvVarIsSet)
.withEnv((env) => env.set("CODEQL_ACTION_CLEANUP_TRAP_CACHES", value))
.withArgs()
.logs(
t,
"CODEQL_ACTION_CLEANUP_TRAP_CACHES environment variable no longer has any effect",
);
await target.passes(t.is, undefined);
t.deepEqual(
target.getLogger().messages.map((m) => m.type),
["warning"],
);
t.is(listStub.callCount, 1);
t.assert(listStub.calledWithExactly("codeql-trap", "refs/heads/main"));
t.deepEqual(statusReport, {
trap_cache_cleanup_size_bytes: 500 * 1024 * 1024,
});
t.is(deleteStub.callCount, 2);
t.assert(deleteStub.calledWithExactly(3));
t.assert(deleteStub.calledWithExactly(4));
});
}
test("warnIfTrapCacheCleanupEnvVarIsSet doesn't warn when CODEQL_ACTION_CLEANUP_TRAP_CACHES is unset", async (t) => {
const target = callee(warnIfTrapCacheCleanupEnvVarIsSet).withArgs();
await target.passes(t.is, undefined);
t.deepEqual(target.getLogger().messages, []);
});

View File

@@ -3,22 +3,14 @@ import * as path from "path";
import * as actionsCache from "@actions/cache";
import { type ActionState } from "./action-common";
import * as actionsUtil from "./actions-util";
import * as apiClient from "./api-client";
import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { Feature, FeatureEnablement } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { Language } from "./languages";
import { Logger } from "./logging";
import {
asHTTPError,
getEnv,
getErrorMessage,
tryGetFolderBytes,
waitForResultWithTimeLimit,
} from "./util";
import { getEnv, tryGetFolderBytes, waitForResultWithTimeLimit } from "./util";
// This constant should be bumped if we make a breaking change
// to how the CodeQL Action stores or retrieves the TRAP cache,
@@ -178,113 +170,25 @@ export async function uploadTrapCaches(
return true;
}
export interface TrapCacheCleanupStatusReport {
trap_cache_cleanup_error?: string;
trap_cache_cleanup_size_bytes?: number;
trap_cache_cleanup_skipped_because?: string;
}
export async function cleanupTrapCaches(
config: Config,
features: FeatureEnablement,
logger: Logger,
): Promise<TrapCacheCleanupStatusReport> {
const env = getEnv();
if (!(await features.getValue(Feature.CleanupTrapCaches))) {
return {
trap_cache_cleanup_skipped_because: "feature disabled",
};
}
logger.warning(
"TRAP cache cleanup is deprecated and will be removed in May 2026. " +
"We recommend instead disabling TRAP caching by passing the `trap-caching: false` input to the `init` Action.",
);
if (!(await gitUtils.isAnalyzingDefaultBranch(env, config.repositoryRoot))) {
return {
trap_cache_cleanup_skipped_because: "not analyzing default branch",
};
}
try {
let totalBytesCleanedUp = 0;
const allCaches = await apiClient.listActionsCaches(
CODEQL_TRAP_CACHE_PREFIX,
await gitUtils.getRef(env, config.repositoryRoot),
/**
* Warns if the `CODEQL_ACTION_CLEANUP_TRAP_CACHES` environment variable is set to a non-empty
* value. That variable controlled the removed TRAP cache cleanup feature, so setting it has no
* effect.
*/
export function warnIfTrapCacheCleanupEnvVarIsSet({
env,
logger,
}: ActionState<["Logger", "ReadOnlyEnv"]>): void {
if (env.getOptional("CODEQL_ACTION_CLEANUP_TRAP_CACHES") !== undefined) {
logger.warning(
"TRAP cache cleanup has been removed, so the CODEQL_ACTION_CLEANUP_TRAP_CACHES environment " +
"variable no longer has any effect and can be removed from your workflow. If you used TRAP " +
"cache cleanup to limit Actions cache usage, we recommend disabling TRAP caching instead by " +
"passing the `trap-caching: false` input to the `init` Action.",
);
for (const language of config.languages) {
if (config.trapCaches[language]) {
const cachesToRemove = await getTrapCachesForLanguage(
allCaches,
language,
logger,
);
// Dates returned by the API are in ISO 8601 format, so we can sort them lexicographically
cachesToRemove.sort((a, b) => a.created_at.localeCompare(b.created_at));
// Keep the most recent cache
const mostRecentCache = cachesToRemove.pop();
logger.debug(
`Keeping most recent TRAP cache (${JSON.stringify(mostRecentCache)})`,
);
if (cachesToRemove.length === 0) {
logger.info(`No TRAP caches to clean up for ${language}.`);
continue;
}
for (const cache of cachesToRemove) {
logger.debug(`Cleaning up TRAP cache (${JSON.stringify(cache)})`);
await apiClient.deleteActionsCache(cache.id);
}
const bytesCleanedUp = cachesToRemove.reduce(
(acc, item) => acc + item.size_in_bytes,
0,
);
totalBytesCleanedUp += bytesCleanedUp;
const megabytesCleanedUp = (bytesCleanedUp / (1024 * 1024)).toFixed(2);
logger.info(
`Cleaned up ${megabytesCleanedUp} MiB of old TRAP caches for ${language}.`,
);
}
}
return { trap_cache_cleanup_size_bytes: totalBytesCleanedUp };
} catch (e) {
if (asHTTPError(e)?.status === 403) {
logger.warning(
"Could not cleanup TRAP caches as the token did not have the required permissions. " +
'To clean up TRAP caches, ensure the token has the "actions:write" permission. ' +
`See ${DocUrl.ASSIGNING_PERMISSIONS_TO_JOBS} for more information.`,
);
} else {
logger.info(`Failed to cleanup TRAP caches, continuing. Details: ${e}`);
}
return { trap_cache_cleanup_error: getErrorMessage(e) };
}
}
async function getTrapCachesForLanguage(
allCaches: apiClient.ActionsCacheItem[],
language: Language,
logger: Logger,
): Promise<Array<Required<apiClient.ActionsCacheItem>>> {
logger.debug(`Listing TRAP caches for ${language}`);
for (const cache of allCaches) {
if (!cache.created_at || !cache.id || !cache.key || !cache.size_in_bytes) {
throw new Error(
"An unexpected cache item was returned from the API that was missing one or " +
`more required fields: ${JSON.stringify(cache)}`,
);
}
}
return allCaches.filter((cache) => {
return cache.key?.includes(`-${language}-`);
}) as Array<Required<apiClient.ActionsCacheItem>>;
}
export async function getLanguagesSupportingCaching(
codeql: CodeQL,
languages: Language[],

View File

@@ -10,9 +10,9 @@ import * as analyses from "./analyses";
import { AnalysisKind, CodeQuality, CodeScanning } from "./analyses";
import * as api from "./api-client";
import * as diffUtils from "./diff-informed-analysis-utils";
import { getRunnerLogger, Logger } from "./logging";
import { Logger } from "./logging";
import * as sarif from "./sarif";
import { setupTests } from "./testing-utils";
import { RecordingLogger, setupTests } from "./testing-utils";
import * as uploadLib from "./upload-lib";
import { UploadPayload } from "./upload-lib/types";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
@@ -29,7 +29,7 @@ test.serial("validateSarifFileSchema - valid", (t) => {
uploadLib.validateSarifFileSchema(
uploadLib.readSarifFileOrThrow(inputFile),
inputFile,
getRunnerLogger(true),
new RecordingLogger(),
),
);
});
@@ -40,7 +40,7 @@ test.serial("validateSarifFileSchema - invalid", (t) => {
uploadLib.validateSarifFileSchema(
uploadLib.readSarifFileOrThrow(inputFile),
inputFile,
getRunnerLogger(true),
new RecordingLogger(),
),
);
});
@@ -179,7 +179,7 @@ test.serial("finding SARIF files", async (t) => {
}
const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
getRunnerLogger(true),
new RecordingLogger(),
tmpDir,
);
@@ -200,7 +200,7 @@ test.serial("getGroupedSarifFilePaths - Risk Assessment files", async (t) => {
fs.writeFileSync(sarifPath, "");
const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
getRunnerLogger(true),
new RecordingLogger(),
sarifPath,
);
@@ -218,7 +218,7 @@ test.serial("getGroupedSarifFilePaths - Code Quality file", async (t) => {
fs.writeFileSync(sarifPath, "");
const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
getRunnerLogger(true),
new RecordingLogger(),
sarifPath,
);
@@ -236,7 +236,7 @@ test.serial("getGroupedSarifFilePaths - Code Scanning file", async (t) => {
fs.writeFileSync(sarifPath, "");
const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
getRunnerLogger(true),
new RecordingLogger(),
sarifPath,
);
@@ -254,7 +254,7 @@ test.serial("getGroupedSarifFilePaths - Other file", async (t) => {
fs.writeFileSync(sarifPath, "");
const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths(
getRunnerLogger(true),
new RecordingLogger(),
sarifPath,
);
@@ -932,7 +932,7 @@ function uploadPayloadFixtures(analysis: analyses.AnalysisConfig) {
owner: mockData.owner,
repo: mockData.repo,
},
getRunnerLogger(true),
new RecordingLogger(),
analysis,
);
@@ -1019,7 +1019,7 @@ function runFilterAlertsByDiffRange(
diffRanges: diffUtils.DiffThunkRange[],
): Partial<sarif.Log> {
sinon.stub(diffUtils, "readDiffRangesJsonFile").returns(diffRanges);
return uploadLib.filterAlertsByDiffRange(getRunnerLogger(true), input);
return uploadLib.filterAlertsByDiffRange(new RecordingLogger(), input);
}
test.serial(

View File

@@ -5,8 +5,12 @@ import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import { AnalysisKind, getAnalysisConfig } from "./analyses";
import { getRunnerLogger } from "./logging";
import { createFeatures, makeMacro, setupTests } from "./testing-utils";
import {
createFeatures,
makeMacro,
RecordingLogger,
setupTests,
} from "./testing-utils";
import { UploadResult } from "./upload-lib";
import * as uploadLib from "./upload-lib";
import { postProcessAndUploadSarif } from "./upload-sarif";
@@ -52,7 +56,7 @@ const postProcessAndUploadSarifMacro = makeMacro({
expectedResult: Partial<Record<AnalysisKind, UploadSarifExpectedResult>>,
) => {
await util.withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const testPath = sarifPath(tempDir);
const features = createFeatures([]);
@@ -210,7 +214,7 @@ test.serial(
"postProcessAndUploadSarif doesn't upload if upload is disabled",
async (t) => {
await util.withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const features = createFeatures([]);
const toFullPath = (filename: string) => path.join(tempDir, filename);
@@ -244,7 +248,7 @@ test.serial(
"postProcessAndUploadSarif writes post-processed SARIF files if output directory is provided",
async (t) => {
await util.withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const logger = new RecordingLogger();
const features = createFeatures([]);
const toFullPath = (filename: string) => path.join(tempDir, filename);

View File

@@ -10,8 +10,7 @@ import * as sinon from "sinon";
import * as api from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { getTestEnv, setupTests } from "./testing-utils";
import { RecordingLogger, setupTests, getTestEnv } from "./testing-utils";
import * as util from "./util";
setupTests(test);
@@ -92,7 +91,7 @@ for (const {
test("getMemoryFlag() throws if the ram input is < 0 or NaN", async (t) => {
for (const input of ["-1", "hello!"]) {
t.throws(() => util.getMemoryFlag(input, getRunnerLogger(true)));
t.throws(() => util.getMemoryFlag(input, new RecordingLogger()));
}
});
@@ -109,13 +108,13 @@ test("getThreadsFlag() should return the correct --threads flag", (t) => {
];
for (const [input, expectedFlag] of tests) {
const flag = util.getThreadsFlag(input, getRunnerLogger(true));
const flag = util.getThreadsFlag(input, new RecordingLogger());
t.deepEqual(flag, expectedFlag);
}
});
test("getThreadsFlag() throws if the threads input is not an integer", (t) => {
t.throws(() => util.getThreadsFlag("hello!", getRunnerLogger(true)));
t.throws(() => util.getThreadsFlag("hello!", new RecordingLogger()));
});
test.serial(
@@ -385,7 +384,7 @@ test("getCgroupCpuCountFromCpus calculates the number of CPUs correctly", async
const testCpuFile = `${tmpDir}/cpus-file`;
fs.writeFileSync(testCpuFile, "1, 9-10\n", "utf-8");
t.deepEqual(
util.getCgroupCpuCountFromCpus(testCpuFile, getRunnerLogger(true)),
util.getCgroupCpuCountFromCpus(testCpuFile, new RecordingLogger()),
3,
);
});
@@ -396,7 +395,7 @@ test("getCgroupCpuCountFromCpus returns undefined if the CPU file doesn't exist"
const testCpuFile = `${tmpDir}/cpus-file`;
t.false(fs.existsSync(testCpuFile));
t.deepEqual(
util.getCgroupCpuCountFromCpus(testCpuFile, getRunnerLogger(true)),
util.getCgroupCpuCountFromCpus(testCpuFile, new RecordingLogger()),
undefined,
);
});
@@ -407,7 +406,7 @@ test("getCgroupCpuCountFromCpus returns undefined if the CPU file exists but is
const testCpuFile = `${tmpDir}/cpus-file`;
fs.writeFileSync(testCpuFile, "\n", "utf-8");
t.deepEqual(
util.getCgroupCpuCountFromCpus(testCpuFile, getRunnerLogger(true)),
util.getCgroupCpuCountFromCpus(testCpuFile, new RecordingLogger()),
undefined,
);
});
@@ -417,7 +416,7 @@ test.serial(
"checkDiskUsage succeeds and produces positive numbers",
async (t) => {
process.env["GITHUB_WORKSPACE"] = os.tmpdir();
const diskUsage = await util.checkDiskUsage(getRunnerLogger(true));
const diskUsage = await util.checkDiskUsage(new RecordingLogger());
if (t.truthy(diskUsage)) {
t.true(diskUsage.numAvailableBytes > 0);
t.true(diskUsage.numTotalBytes > 0);

View File

@@ -900,6 +900,21 @@ export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* Approximates whether {@link error} is like a {@link NodeJS.ErrnoException}.
* We only typically care about the `code` property, so that's what we check for here.
*/
export function isErrnoException(
error: unknown,
): error is NodeJS.ErrnoException {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string"
);
}
export function prettyPrintPack(pack: Pack) {
return `${pack.name}${pack.version ? `@${pack.version}` : ""}${
pack.path ? `:${pack.path}` : ""

View File

@@ -3,10 +3,11 @@ import * as yaml from "js-yaml";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { createStubCodeQL, getCodeQLForTesting } from "./codeql";
import { createStubCodeQL } from "./codeql";
import { ActionsEnvVars, EnvVar } from "./environment";
import {
checkExpectedLogMessages,
getCodeQLForTesting,
createTestConfig,
getRecordingLogger,
getTestEnv,