Files
codeql-action/src/init.ts
T
2026-03-10 15:41:40 +00:00

419 lines
13 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import {
getOptionalInput,
isAnalyzingPullRequest,
isDefaultSetup,
isSelfHostedRunner,
} from "./actions-util";
import { GitHubApiDetails } from "./api-client";
import { CodeQL, setupCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { EnvVar } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
FeatureEnablement,
} from "./feature-flags";
import {
RepositoryProperties,
RepositoryPropertyName,
} from "./feature-flags/properties";
import { KnownLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { ToolsSource } from "./setup-codeql";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
import * as util from "./util";
export async function initCodeQL(
toolsInput: string | undefined,
apiDetails: GitHubApiDetails,
tempDir: string,
variant: util.GitHubVariant,
defaultCliVersion: CodeQLDefaultVersionInfo,
features: FeatureEnablement,
logger: Logger,
): Promise<{
codeql: CodeQL;
toolsDownloadStatusReport?: ToolsDownloadStatusReport;
toolsSource: ToolsSource;
toolsVersion: string;
zstdAvailability: ZstdAvailability;
}> {
logger.startGroup("Setup CodeQL tools");
const {
codeql,
toolsDownloadStatusReport,
toolsSource,
toolsVersion,
zstdAvailability,
} = await setupCodeQL(
toolsInput,
apiDetails,
tempDir,
variant,
defaultCliVersion,
features,
logger,
true,
);
await codeql.printVersion();
logger.endGroup();
return {
codeql,
toolsDownloadStatusReport,
toolsSource,
toolsVersion,
zstdAvailability,
};
}
export async function initConfig(
features: FeatureEnablement,
inputs: configUtils.InitConfigInputs,
): Promise<configUtils.Config> {
return await withGroupAsync("Load language configuration", async () => {
return await configUtils.initConfig(features, inputs);
});
}
export async function runDatabaseInitCluster(
databaseInitEnvironment: Record<string, string | undefined>,
codeql: CodeQL,
config: configUtils.Config,
sourceRoot: string,
processName: string | undefined,
qlconfigFile: string | undefined,
logger: Logger,
): Promise<void> {
fs.mkdirSync(config.dbLocation, { recursive: true });
await configUtils.wrapEnvironment(
databaseInitEnvironment,
async () =>
await codeql.databaseInitCluster(
config,
sourceRoot,
processName,
qlconfigFile,
logger,
),
);
}
/**
* Check whether all query packs are compatible with the overlay analysis
* support in the CodeQL CLI. If the check fails, this function will log a
* warning and returns false.
*
* @param codeql A CodeQL instance.
* @param logger A logger.
* @returns `true` if all query packs are compatible with overlay analysis,
* `false` otherwise.
*/
export async function checkPacksForOverlayCompatibility(
codeql: CodeQL,
config: configUtils.Config,
logger: Logger,
): Promise<boolean> {
const codeQlOverlayVersion = (await codeql.getVersion()).overlayVersion;
if (codeQlOverlayVersion === undefined) {
logger.warning("The CodeQL CLI does not support overlay analysis.");
return false;
}
for (const language of config.languages) {
const suitePath = util.getGeneratedSuitePath(config, language);
const packDirs = await codeql.resolveQueriesStartingPacks([suitePath]);
if (
packDirs.some(
(packDir) =>
!checkPackForOverlayCompatibility(
packDir,
codeQlOverlayVersion,
logger,
),
)
) {
return false;
}
}
return true;
}
/** Interface for `qlpack.yml` file contents. */
interface QlPack {
buildMetadata?: string;
}
/**
* Check a single pack for its overlay compatibility. If the check fails, this
* function will log a warning and returns false.
*
* @param packDir Path to the directory containing the pack.
* @param codeQlOverlayVersion The overlay version of the CodeQL CLI.
* @param logger A logger.
* @returns `true` if the pack is compatible with overlay analysis, `false`
* otherwise.
*/
function checkPackForOverlayCompatibility(
packDir: string,
codeQlOverlayVersion: number,
logger: Logger,
): boolean {
try {
let qlpackPath = path.join(packDir, "qlpack.yml");
if (!fs.existsSync(qlpackPath)) {
qlpackPath = path.join(packDir, "codeql-pack.yml");
}
const qlpackContents = yaml.load(
fs.readFileSync(qlpackPath, "utf8"),
) as QlPack;
if (!qlpackContents.buildMetadata) {
// This is a source-only pack, and overlay compatibility checks apply only
// to precompiled packs.
return true;
}
const packInfoPath = path.join(packDir, ".packinfo");
if (!fs.existsSync(packInfoPath)) {
logger.warning(
`The query pack at ${packDir} does not have a .packinfo file, ` +
"so it cannot support overlay analysis. Recompiling the query pack " +
"with the latest CodeQL CLI should solve this problem.",
);
return false;
}
const packInfoFileContents = JSON.parse(
fs.readFileSync(packInfoPath, "utf8"),
);
const packOverlayVersion = packInfoFileContents.overlayVersion;
if (typeof packOverlayVersion !== "number") {
logger.warning(
`The .packinfo file for the query pack at ${packDir} ` +
"does not have the overlayVersion field, which indicates that " +
"the pack is not compatible with overlay analysis.",
);
return false;
}
if (packOverlayVersion !== codeQlOverlayVersion) {
logger.warning(
`The query pack at ${packDir} was compiled with ` +
`overlay version ${packOverlayVersion}, but the CodeQL CLI ` +
`supports overlay version ${codeQlOverlayVersion}. The ` +
"query pack needs to be recompiled to support overlay analysis.",
);
return false;
}
} catch (e) {
logger.warning(
`Error while checking pack at ${packDir} ` +
`for overlay compatibility: ${util.getErrorMessage(e)}`,
);
return false;
}
return true;
}
/**
* If we are running python 3.12+ on windows, we need to switch to python 3.11.
* This check happens in a powershell script.
*/
export async function checkInstallPython311(
languages: Language[],
codeql: CodeQL,
) {
if (
languages.includes(KnownLanguage.python) &&
process.platform === "win32" &&
!(await codeql.getVersion()).features?.supportsPython312
) {
const script = path.resolve(
__dirname,
"../python-setup",
"check_python12.ps1",
);
await new toolrunner.ToolRunner(await io.which("powershell", true), [
script,
]).exec();
}
}
export function cleanupDatabaseClusterDirectory(
config: configUtils.Config,
logger: Logger,
options: { disableExistingDirectoryWarning?: boolean } = {},
// We can't stub the fs module in tests, so we allow the caller to override the rmSync function
// for testing.
rmSync = fs.rmSync,
): void {
if (
fs.existsSync(config.dbLocation) &&
(fs.statSync(config.dbLocation).isFile() ||
fs.readdirSync(config.dbLocation).length > 0)
) {
if (!options.disableExistingDirectoryWarning) {
logger.warning(
`The database cluster directory ${config.dbLocation} must be empty. Attempting to clean it up.`,
);
}
try {
rmSync(config.dbLocation, {
force: true,
maxRetries: 3,
recursive: true,
});
logger.info(
`Cleaned up database cluster directory ${config.dbLocation}.`,
);
} catch (e) {
const blurb = `The CodeQL Action requires an empty database cluster directory. ${
getOptionalInput("db-location")
? `This is currently configured to be ${config.dbLocation}. `
: `By default, this is located at ${config.dbLocation}. ` +
"You can customize it using the 'db-location' input to the init Action. "
}An attempt was made to clean up the directory, but this failed.`;
// Hosted runners are automatically cleaned up, so this error should not occur for hosted runners.
if (isSelfHostedRunner()) {
throw new util.ConfigurationError(
`${blurb} This can happen if another process is using the directory or the directory is owned by a different user. ` +
`Please clean up the directory manually and rerun the job. Details: ${util.getErrorMessage(
e,
)}`,
);
} else {
throw new Error(
`${blurb} This shouldn't typically happen on hosted runners. ` +
"If you are using an advanced setup, please check your workflow, otherwise we " +
`recommend rerunning the job. Details: ${util.getErrorMessage(e)}`,
);
}
}
}
}
export async function getFileCoverageInformationEnabled(
debugMode: boolean,
codeql: CodeQL,
features: FeatureEnablement,
repositoryProperties: RepositoryProperties,
): Promise<{
enabled: boolean;
enabledByRepositoryProperty: boolean;
showDeprecationWarning: boolean;
}> {
// Always enable file coverage information in debug mode
if (debugMode) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// We're most interested in speeding up PRs, and we want to keep
// submitting file coverage information for the default branch since
// it is used to populate the status page.
if (!isAnalyzingPullRequest()) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// If the user has explicitly opted out via an environment variable, don't
// show the deprecation warning.
if (
(process.env[EnvVar.FILE_COVERAGE_ON_PRS] || "").toLocaleLowerCase() ===
"true"
) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
// Allow repositories to opt in to file coverage information on PRs
// using a repository property. In this case, don't show the deprecation
// warning since the repository has explicitly opted in.
if (
repositoryProperties[RepositoryPropertyName.FILE_COVERAGE_ON_PRS] === true
) {
return {
enabled: true,
enabledByRepositoryProperty: true,
showDeprecationWarning: false,
};
}
// If the feature is disabled, then maintain the previous behavior of
// unconditionally computing file coverage information, but warn that
// file coverage on PRs will be disabled in a future release.
if (!(await features.getValue(Feature.SkipFileCoverageOnPrs, codeql))) {
return {
enabled: true,
enabledByRepositoryProperty: false,
showDeprecationWarning: true,
};
}
// Otherwise, disable file coverage information on PRs to speed up analysis.
return {
enabled: false,
enabledByRepositoryProperty: false,
showDeprecationWarning: false,
};
}
/**
* Log a warning about the deprecation of file coverage information on PRs, including how to opt
* back in via an environment variable or repository property.
*/
export function logFileCoverageOnPrsDeprecationWarning(logger: Logger): void {
if (process.env[EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION]) {
return;
}
const repositoryOwnerType: string | undefined =
github.context.payload.repository?.owner.type;
let message =
"Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests " +
"to improve analysis performance. File coverage information will still be computed on non-PR analyses.";
const envVarOptOut =
"set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`.";
const repoPropertyOptOut =
"create a custom repository property with the name " +
'`github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to ' +
"`true` in the repository's settings.";
if (repositoryOwnerType === "Organization") {
// Org-owned repo: can use the repository property
if (isDefaultSetup()) {
message += `\n\nTo opt out of this change, ${repoPropertyOptOut}`;
} else {
message += `\n\nTo opt out of this change, ${envVarOptOut} Alternatively, ${repoPropertyOptOut}`;
}
} else if (isDefaultSetup()) {
// User-owned repo on default setup: no repo property available and
// no way to set env vars, so need to switch to advanced setup.
message += `\n\nTo opt out of this change, switch to an advanced setup workflow and ${envVarOptOut}`;
} else {
// User-owned repo on advanced setup: can set the env var
message += `\n\nTo opt out of this change, ${envVarOptOut}`;
}
logger.warning(message);
core.exportVariable(EnvVar.DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION, "true");
}