mirror of
https://github.com/github/codeql-action.git
synced 2026-11-08 22:19:50 +00:00
Download a bundle containing only the single language being analyzed, rather than the combined bundle that contains every language, when that is both safe and beneficial. Per-language bundles are substantially smaller, so this saves download time and disk space on the runner. Eligibility is decided in one place, since it is easy for these conditions to drift apart. Per-language bundles are never added to the toolcache, because a bundle for one language must not be reused for a job that analyzes another. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d4c7b873-8278-4a36-b67c-6cc5ea3b2316
763 lines
27 KiB
TypeScript
763 lines
27 KiB
TypeScript
import * as os from "os";
|
|
|
|
import * as core from "@actions/core";
|
|
import * as uuid from "uuid";
|
|
|
|
import type { ActionState } from "./action-common";
|
|
import {
|
|
getWorkflowEventName,
|
|
getOptionalInput,
|
|
getWorkflowRunID,
|
|
getWorkflowRunAttempt,
|
|
getActionVersion,
|
|
getRequiredInput,
|
|
isSelfHostedRunner,
|
|
} from "./actions-util";
|
|
import { getAnalysisKey, getApiClient } from "./api-client";
|
|
import {
|
|
getCachedCodeQlVersion,
|
|
getCommandCacheFilePath,
|
|
} from "./cli/output-cache";
|
|
import type { Config } from "./config/action-config";
|
|
import type { ComputedInput, InputName } from "./config/inputs";
|
|
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
|
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
|
import { DocUrl } from "./doc-url";
|
|
import { EnvVar, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
|
|
import { getRef } from "./git-utils";
|
|
import * as json from "./json";
|
|
import type { Logger } from "./logging";
|
|
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
|
import { getRepositoryNwo } from "./repository";
|
|
import type { ToolsSource } from "./setup-codeql";
|
|
import { registryBaseSchema } from "./start-proxy/types";
|
|
import {
|
|
ConfigurationError,
|
|
getRequiredEnvParam,
|
|
isInTestMode,
|
|
GITHUB_DOTCOM_URL,
|
|
DiskUsage,
|
|
assertNever,
|
|
BuildMode,
|
|
getErrorMessage,
|
|
getTestingEnvironment,
|
|
asHTTPError,
|
|
} from "./util";
|
|
|
|
export enum ActionName {
|
|
Analyze = "finish",
|
|
Autobuild = "autobuild",
|
|
Init = "init",
|
|
InitPost = "init-post",
|
|
ResolveEnvironment = "resolve-environment",
|
|
SetupCodeQL = "setup-codeql",
|
|
StartProxy = "start-proxy",
|
|
UploadSarif = "upload-sarif",
|
|
}
|
|
|
|
/**
|
|
* Maps an `ActionName` to its display name. Usually that is the same, except
|
|
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
|
|
*/
|
|
export function getDisplayActionName(actionName: ActionName): string {
|
|
if (actionName === ActionName.Analyze) {
|
|
return "analyze";
|
|
}
|
|
return actionName;
|
|
}
|
|
|
|
/**
|
|
* Either creates a UUIDv4 for the analysis or retrieves an existing one from the
|
|
* environment and returns it.
|
|
* If a new UUID is generated, it is also exported as an environment variable.
|
|
*/
|
|
export function getJobUUID(
|
|
action: ActionState<["Logger", "ReadOnlyEnv", "Actions"]>,
|
|
) {
|
|
// Check if we already have a UUID for the analysis and return it if so.
|
|
const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID);
|
|
|
|
if (existingJobRunUuid !== undefined && uuid.validate(existingJobRunUuid)) {
|
|
action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`);
|
|
return existingJobRunUuid;
|
|
}
|
|
|
|
// Otherwise generate a new UUID.
|
|
const jobRunUuid = uuid.v4();
|
|
action.logger.info(`Job run UUID is ${jobRunUuid}.`);
|
|
|
|
action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
|
|
return jobRunUuid;
|
|
}
|
|
|
|
/**
|
|
* @returns a boolean indicating whether the analysis is considered to be first party.
|
|
*
|
|
* This is based on whether the init action has been used, which is only used for first party analysis.
|
|
* When a SARIF file has been generated by other means and submitted using the upload action, this is
|
|
* considered to be a third party analysis and is treated differently when calculating SLOs. To ensure
|
|
* misconfigured workflows are not treated as third party, only the upload-sarif action can return false.
|
|
*/
|
|
function isFirstPartyAnalysis(actionName: ActionName): boolean {
|
|
if (actionName !== ActionName.UploadSarif) {
|
|
return true;
|
|
}
|
|
return process.env[EnvVar.INIT_ACTION_HAS_RUN] === "true";
|
|
}
|
|
|
|
/**
|
|
* @returns true if the analysis is considered to be third party.
|
|
*/
|
|
export function isThirdPartyAnalysis(actionName: ActionName): boolean {
|
|
return !isFirstPartyAnalysis(actionName);
|
|
}
|
|
|
|
export type ActionStatus =
|
|
| "aborted" // Only used in the init Action, if init failed before initializing the tracer due to something other than a configuration error.
|
|
| "failure"
|
|
| "starting"
|
|
| "success"
|
|
| "user-error";
|
|
|
|
/** Overall status of the entire job. String values match the Hydro schema. */
|
|
export enum JobStatus {
|
|
UnknownStatus = "JOB_STATUS_UNKNOWN",
|
|
SuccessStatus = "JOB_STATUS_SUCCESS",
|
|
FailureStatus = "JOB_STATUS_FAILURE",
|
|
ConfigErrorStatus = "JOB_STATUS_CONFIGURATION_ERROR",
|
|
}
|
|
|
|
export interface StatusReportBase {
|
|
/** Name of the action being executed. */
|
|
action_name: ActionName;
|
|
/** Version of the action being executed, as a commit oid. */
|
|
action_oid: string;
|
|
/** Version of the action being executed, as a ref. */
|
|
action_ref?: string;
|
|
/** Time this action started. */
|
|
action_started_at: string;
|
|
/** Action version (x.y.z from package.json). */
|
|
action_version: string;
|
|
/** The name of the Actions event that triggered the workflow. */
|
|
actions_event_name?: string;
|
|
/** Comma-separated list of the kinds of analyses we are performing. */
|
|
analysis_kinds?: string;
|
|
/** Analysis key, normally composed from the workflow path and job name. */
|
|
analysis_key: string;
|
|
/** Build mode, if specified. */
|
|
build_mode?: BuildMode;
|
|
/** Cause of the failure (or undefined if status is not failure). */
|
|
cause?: string;
|
|
/** CodeQL CLI version (x.y.z from the CLI). */
|
|
codeql_version?: string;
|
|
/** Commit oid that the workflow was triggered on. */
|
|
commit_oid: string;
|
|
/** Time this action completed, or undefined if not yet completed. */
|
|
completed_at?: string;
|
|
/** A mapping of input names to their computed values. */
|
|
computed_inputs: Partial<Record<InputName, ComputedInput>>;
|
|
/** Stack trace of the failure (or undefined if status is not failure). */
|
|
exception?: string;
|
|
/** Whether this is a first-party (CodeQL) run of the action. */
|
|
first_party_analysis: boolean;
|
|
/** Job name from the workflow. */
|
|
job_name: string;
|
|
/**
|
|
* UUID representing the job run that this status report belongs to. We
|
|
* generate our own UUID here because Actions currently does not expose a
|
|
* unique job run identifier. This UUID will allow us to more easily match
|
|
* reports from different steps in the same workflow job.
|
|
*
|
|
* If and when Actions does expose a unique job ID, we plan to populate a
|
|
* separate int field, `job_run_id`, with the Actions-generated identifier,
|
|
* as it will allow us to more easily join our telemetry data with Actions
|
|
* telemetry tables.
|
|
*/
|
|
job_run_uuid: string;
|
|
/**
|
|
* Comma-separated list of languages that analysis was run for.
|
|
*
|
|
* This may be from the workflow file or may be calculated from the contents of the repository.
|
|
*/
|
|
languages?: string;
|
|
/** Value of the matrix for this instantiation of the job. */
|
|
matrix_vars?: string;
|
|
/**
|
|
* Information about the enablement of the ML-powered JS query pack.
|
|
*
|
|
* @see {@link util.getMlPoweredJsQueriesStatus}
|
|
*/
|
|
ml_powered_javascript_queries?: string;
|
|
/** Ref that the workflow was triggered on. */
|
|
ref: string;
|
|
/**
|
|
* A comma-separated list of private registry types which are configured for CodeQL.
|
|
* This only includes registry types we support (as determined by the `start-proxy` action),
|
|
* not all that are configured.
|
|
*/
|
|
registry_types?: string;
|
|
/** Action runner hardware architecture (context runner.arch). */
|
|
runner_arch?: string;
|
|
/** Available disk space on the runner, in bytes. */
|
|
runner_available_disk_space_bytes?: number;
|
|
/**
|
|
* Version of the runner image, for workflows running on GitHub-hosted runners. Absent otherwise.
|
|
*/
|
|
runner_image_version?: string;
|
|
/** Action runner operating system (context runner.os). */
|
|
runner_os: string;
|
|
/** Action runner operating system release (x.y.z from os.release()). */
|
|
runner_os_release?: string;
|
|
/** Total disk space on the runner, in bytes. */
|
|
runner_total_disk_space_bytes?: number;
|
|
/** Time the first action started. Normally the init action. */
|
|
started_at: string;
|
|
/** State this action is currently in. */
|
|
status: ActionStatus;
|
|
/** Whether this run is part of a steady-state, and not new, default setup run. */
|
|
steady_state_default_setup: boolean;
|
|
/**
|
|
* Testing environment: Set if non-production environment.
|
|
* The server accepts one of the following values:
|
|
* `["", "qa-rc", "qa-rc-1", "qa-rc-2", "qa-experiment-1", "qa-experiment-2", "qa-experiment-3"]`.
|
|
*/
|
|
testing_environment: string;
|
|
/** Workflow name. Converted to analysis_name further down the pipeline.. */
|
|
workflow_name: string;
|
|
/** Attempt number of the run containing the action run. */
|
|
workflow_run_attempt: number;
|
|
/** ID of the workflow run containing the action run. */
|
|
workflow_run_id: number;
|
|
}
|
|
|
|
export interface DatabaseCreationTimings {
|
|
scanned_language_extraction_duration_ms?: number;
|
|
trap_import_duration_ms?: number;
|
|
}
|
|
|
|
export function getActionsStatus(
|
|
error?: unknown,
|
|
otherFailureCause?: string,
|
|
): ActionStatus {
|
|
if (error || otherFailureCause) {
|
|
return error instanceof ConfigurationError ? "user-error" : "failure";
|
|
} else {
|
|
return "success";
|
|
}
|
|
}
|
|
|
|
export function getJobStatusDisplayName(status: JobStatus): string {
|
|
switch (status) {
|
|
case JobStatus.SuccessStatus:
|
|
return "success";
|
|
case JobStatus.FailureStatus:
|
|
return "failure";
|
|
case JobStatus.ConfigErrorStatus:
|
|
return "configuration error";
|
|
case JobStatus.UnknownStatus:
|
|
return "unknown";
|
|
default:
|
|
assertNever(status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the overall job status environment variable to configuration error
|
|
* or failure, unless it's already been set to one of these values in a
|
|
* previous step.
|
|
*/
|
|
function setJobStatusIfUnsuccessful(actionStatus: ActionStatus) {
|
|
if (actionStatus === "user-error") {
|
|
core.exportVariable(
|
|
EnvVar.JOB_STATUS,
|
|
process.env[EnvVar.JOB_STATUS] ?? JobStatus.ConfigErrorStatus,
|
|
);
|
|
} else if (actionStatus === "failure" || actionStatus === "aborted") {
|
|
core.exportVariable(
|
|
EnvVar.JOB_STATUS,
|
|
process.env[EnvVar.JOB_STATUS] ?? JobStatus.FailureStatus,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Any status report may include an array of EventReports associated with it.
|
|
export interface EventReport {
|
|
/** Time this event ended. */
|
|
completed_at: string;
|
|
/** An enumerable description of the event. */
|
|
event: string;
|
|
/** eg: `success`, `failure`, `timeout`, etc. */
|
|
exit_status?: string;
|
|
/** If the event is language-specific. */
|
|
language?: string;
|
|
/**
|
|
* A generic JSON blob of data related to this event.
|
|
* Use Object.assign() to append additional fields to the object.
|
|
*/
|
|
properties?: object;
|
|
/** Time this event started. */
|
|
started_at: string;
|
|
}
|
|
|
|
/**
|
|
* Attempts to retrieve a list of private registry types from the `CODEQL_PROXY_URLS` environment
|
|
* variable and returns it as a comma-separated string if successful. Returns `undefined` otherwise.
|
|
*/
|
|
export function getRegistryTypesFromEnv(
|
|
logger: Logger,
|
|
env: ReadOnlyEnv = getEnv(),
|
|
): string | undefined {
|
|
// Try to get the value of the environment variable.
|
|
const value = env.getOptional(RegistryProxyVars.PROXY_URLS);
|
|
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
// Try to parse the JSON we expect to find in it and return the comma-separated list of
|
|
// (unique) registry types.
|
|
try {
|
|
const data = JSON.parse(value) as unknown;
|
|
|
|
// Check that the parsed JSON meets our expectations.
|
|
if (!json.isArray(data)) {
|
|
logger.debug(
|
|
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array, but got '${typeof data}'.`,
|
|
);
|
|
return undefined;
|
|
}
|
|
if (!json.validateArray(registryBaseSchema, data)) {
|
|
logger.debug(
|
|
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array of registry objects, but got something else.`,
|
|
);
|
|
return undefined;
|
|
}
|
|
|
|
const types = new Set(data.map((r) => r.type));
|
|
return Array.from(types).sort().join(",");
|
|
} catch (err) {
|
|
logger.debug(
|
|
`Failed to parse '${RegistryProxyVars.PROXY_URLS}': ${getErrorMessage(err)}.`,
|
|
);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compose a StatusReport.
|
|
*
|
|
* @param actionName The name of the action, e.g. 'init', 'finish', 'upload-sarif'
|
|
* @param status The status. Must be 'success', 'failure', or 'starting'
|
|
* @param actionStartedAt The time this action started executing.
|
|
* @param cause Cause of failure (only supply if status is 'failure')
|
|
* @param exception Exception (only supply if status is 'failure')
|
|
* @returns undefined if an exception was thrown.
|
|
*/
|
|
export async function createStatusReportBase(
|
|
actionName: ActionName,
|
|
status: ActionStatus,
|
|
actionStartedAt: Date,
|
|
config: Partial<Config> | undefined,
|
|
diskInfo: DiskUsage | undefined,
|
|
logger: Logger,
|
|
cause?: string,
|
|
exception?: string,
|
|
): Promise<StatusReportBase | undefined> {
|
|
try {
|
|
const commitOid =
|
|
getOptionalInput("sha") || process.env["GITHUB_SHA"] || "";
|
|
const ref = await getRef();
|
|
const jobRunUUID = process.env[EnvVar.JOB_RUN_UUID] || "";
|
|
const workflowRunID = getWorkflowRunID();
|
|
const workflowRunAttempt = getWorkflowRunAttempt();
|
|
const workflowName = process.env["GITHUB_WORKFLOW"] || "";
|
|
const jobName = process.env["GITHUB_JOB"] || "";
|
|
const analysis_key = await getAnalysisKey();
|
|
let workflowStartedAt = process.env[EnvVar.WORKFLOW_STARTED_AT];
|
|
if (workflowStartedAt === undefined) {
|
|
workflowStartedAt = actionStartedAt.toISOString();
|
|
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
|
|
}
|
|
const runnerOs = getRequiredEnvParam("RUNNER_OS");
|
|
const codeQlCliVersion = getCachedCodeQlVersion(
|
|
logger,
|
|
getCommandCacheFilePath(getEnv()),
|
|
);
|
|
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
|
|
const testingEnvironment = getTestingEnvironment();
|
|
// re-export the testing environment variable so that it is available to subsequent steps,
|
|
// even if it was only set for this step
|
|
if (testingEnvironment) {
|
|
core.exportVariable(EnvVar.TESTING_ENVIRONMENT, testingEnvironment);
|
|
}
|
|
const isSteadyStateDefaultSetupRun =
|
|
process.env["CODE_SCANNING_IS_STEADY_STATE_DEFAULT_SETUP"] === "true";
|
|
|
|
const statusReport: StatusReportBase = {
|
|
action_name: actionName,
|
|
action_oid: "unknown", // TODO decide if it's possible to fill this in
|
|
action_ref: actionRef,
|
|
action_started_at: actionStartedAt.toISOString(),
|
|
action_version: getActionVersion(),
|
|
analysis_kinds: config?.analysisKinds?.join(","),
|
|
analysis_key,
|
|
build_mode: config?.buildMode,
|
|
commit_oid: commitOid,
|
|
computed_inputs: {},
|
|
first_party_analysis: isFirstPartyAnalysis(actionName),
|
|
job_name: jobName,
|
|
job_run_uuid: jobRunUUID,
|
|
ref,
|
|
registry_types: getRegistryTypesFromEnv(logger),
|
|
runner_os: runnerOs,
|
|
started_at: workflowStartedAt,
|
|
status,
|
|
steady_state_default_setup: isSteadyStateDefaultSetupRun,
|
|
testing_environment: testingEnvironment || "",
|
|
workflow_name: workflowName,
|
|
workflow_run_attempt: workflowRunAttempt,
|
|
workflow_run_id: workflowRunID,
|
|
};
|
|
|
|
try {
|
|
statusReport.actions_event_name = getWorkflowEventName();
|
|
} catch (e) {
|
|
logger.warning(
|
|
`Could not determine the workflow event name: ${getErrorMessage(e)}.`,
|
|
);
|
|
}
|
|
|
|
if (config) {
|
|
statusReport.languages = config.languages?.join(",");
|
|
}
|
|
|
|
if (diskInfo) {
|
|
statusReport.runner_available_disk_space_bytes =
|
|
diskInfo.numAvailableBytes;
|
|
statusReport.runner_total_disk_space_bytes = diskInfo.numTotalBytes;
|
|
}
|
|
|
|
// Add optional parameters
|
|
if (cause) {
|
|
statusReport.cause = cause;
|
|
}
|
|
if (exception) {
|
|
statusReport.exception = exception;
|
|
}
|
|
if (
|
|
status === "success" ||
|
|
status === "failure" ||
|
|
status === "aborted" ||
|
|
status === "user-error"
|
|
) {
|
|
statusReport.completed_at = new Date().toISOString();
|
|
}
|
|
const matrix = getRequiredInput("matrix");
|
|
if (matrix) {
|
|
statusReport.matrix_vars = matrix;
|
|
}
|
|
if ("RUNNER_ARCH" in process.env) {
|
|
// Values other than X86, X64, ARM, or ARM64 are discarded server side
|
|
statusReport.runner_arch = process.env["RUNNER_ARCH"];
|
|
}
|
|
if (!(runnerOs === "Linux" && isSelfHostedRunner())) {
|
|
// We do not report the release number for Linux self-hosted runners
|
|
// because the custom build suffix may be private customer information.
|
|
statusReport.runner_os_release = os.release();
|
|
}
|
|
if (codeQlCliVersion !== undefined) {
|
|
statusReport.codeql_version = codeQlCliVersion.version;
|
|
}
|
|
const imageVersion = process.env["ImageVersion"];
|
|
if (imageVersion) {
|
|
statusReport.runner_image_version = imageVersion;
|
|
}
|
|
|
|
return statusReport;
|
|
} catch (e) {
|
|
logger.warning(
|
|
`Failed to gather information for telemetry: ${getErrorMessage(e)}. Will skip sending status report.`,
|
|
);
|
|
|
|
// Re-throw the exception in test mode. While testing, we want to know if something goes wrong here.
|
|
if (isInTestMode()) {
|
|
throw e;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const OUT_OF_DATE_MSG =
|
|
"CodeQL Action is out-of-date. Please upgrade to the latest version of `codeql-action`.";
|
|
const INCOMPATIBLE_MSG =
|
|
"CodeQL Action version is incompatible with the API endpoint. Please update to a compatible version of `codeql-action`.";
|
|
|
|
/**
|
|
* Send a status report to the code_scanning/analysis/status endpoint.
|
|
*
|
|
* Optionally checks the response from the API endpoint and sets the action
|
|
* as failed if the status report failed. This is only expected to be used
|
|
* when sending a 'starting' report.
|
|
*
|
|
* The `/code-scanning/analysis/status` endpoint is internal and it is not critical that it succeeds:
|
|
* https://github.com/github/codeql/issues/15462#issuecomment-1919186317
|
|
*
|
|
* Failures while calling this endpoint are logged as warings.
|
|
*/
|
|
export async function sendStatusReport<S extends StatusReportBase>(
|
|
statusReport: S,
|
|
): Promise<void> {
|
|
setJobStatusIfUnsuccessful(statusReport.status);
|
|
|
|
const statusReportJSON = JSON.stringify(statusReport);
|
|
core.debug(`Sending status report: ${statusReportJSON}`);
|
|
// If in test mode we don't want to upload the results
|
|
if (isInTestMode()) {
|
|
core.debug("In test mode. Status reports are not uploaded.");
|
|
return;
|
|
}
|
|
|
|
const nwo = getRepositoryNwo();
|
|
const client = getApiClient();
|
|
|
|
try {
|
|
await client.request(
|
|
"PUT /repos/:owner/:repo/code-scanning/analysis/status",
|
|
{
|
|
owner: nwo.owner,
|
|
repo: nwo.repo,
|
|
data: statusReportJSON,
|
|
},
|
|
);
|
|
} catch (e) {
|
|
const httpError = asHTTPError(e);
|
|
if (httpError !== undefined) {
|
|
switch (httpError.status) {
|
|
case 403:
|
|
if (
|
|
getWorkflowEventName() === "push" &&
|
|
process.env["GITHUB_ACTOR"] === "dependabot[bot]"
|
|
) {
|
|
core.warning(
|
|
'Workflows triggered by Dependabot on the "push" event run with read-only access. ' +
|
|
"Uploading CodeQL results requires write access. " +
|
|
'To use CodeQL with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. ' +
|
|
`See ${DocUrl.SCANNING_ON_PUSH} for more information on how to configure these events.`,
|
|
);
|
|
} else {
|
|
core.warning(
|
|
"This run of the CodeQL Action does not have permission to access the CodeQL Action API endpoints. " +
|
|
"This could be because the Action is running on a pull request from a fork. If not, " +
|
|
`please ensure the workflow has at least the 'security-events: read' permission. Details: ${httpError.message}`,
|
|
);
|
|
}
|
|
return;
|
|
case 404:
|
|
core.warning(httpError.message);
|
|
return;
|
|
case 422:
|
|
// schema incompatibility when reporting status
|
|
// this means that this action version is no longer compatible with the API
|
|
// we still want to continue as it is likely the analysis endpoint will work
|
|
if (getRequiredEnvParam("GITHUB_SERVER_URL") !== GITHUB_DOTCOM_URL) {
|
|
core.debug(INCOMPATIBLE_MSG);
|
|
} else {
|
|
core.debug(OUT_OF_DATE_MSG);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// something else has gone wrong and the request/response will be logged by octokit
|
|
// it's possible this is a transient error and we should continue scanning
|
|
core.warning(
|
|
`An unexpected error occurred when sending a status report: ${getErrorMessage(
|
|
e,
|
|
)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Fields of the init status report that can be sent before `config` is populated. */
|
|
export interface InitStatusReport extends StatusReportBase {
|
|
/** Value given by the user as the "tools" input. */
|
|
tools_input: string;
|
|
/** Version of the bundle used. */
|
|
tools_resolved_version: string;
|
|
/** Where the bundle originated from. */
|
|
tools_source: ToolsSource;
|
|
/** Comma-separated list of languages specified explicitly in the workflow file. */
|
|
workflow_languages: string;
|
|
}
|
|
|
|
/** Fields of the init status report that are populated using values from `config`. */
|
|
export interface InitWithConfigStatusReport extends InitStatusReport {
|
|
/** Comma-separated list of languages where the default queries are disabled. */
|
|
disable_default_queries: string;
|
|
/** Comma-separated list of paths, from the 'paths' config field. */
|
|
paths: string;
|
|
/** Comma-separated list of paths, from the 'paths-ignore' config field. */
|
|
paths_ignore: string;
|
|
/** Comma-separated list of queries sources, from the 'queries' config field or workflow input. */
|
|
queries: string;
|
|
/** Stringified JSON object of packs, from the 'packs' config field or workflow input. */
|
|
packs: string;
|
|
/** Comma-separated list of languages for which we are using TRAP caching. */
|
|
trap_cache_languages: string;
|
|
/** Size of TRAP caches that we downloaded, in bytes. */
|
|
trap_cache_download_size_bytes: number;
|
|
/** Time taken to download TRAP caches, in milliseconds. */
|
|
trap_cache_download_duration_ms: number;
|
|
/** Size of the overlay-base database that we downloaded, in bytes. */
|
|
overlay_base_database_download_size_bytes?: number;
|
|
/** Time taken to download the overlay-base database, in milliseconds. */
|
|
overlay_base_database_download_duration_ms?: number;
|
|
/** Stringified JSON object representing information about the results of restoring dependency caches. */
|
|
dependency_caching_restore_results?: DependencyCacheRestoreStatusReport;
|
|
/** Stringified JSON array of registry configuration objects, from the 'registries' config field
|
|
or workflow input. **/
|
|
registries: string;
|
|
/** Stringified JSON object representing a query-filters, from the 'query-filters' config field. **/
|
|
query_filters: string;
|
|
/** Path to the specified code scanning config file, from the 'config-file' config field. */
|
|
config_file: string;
|
|
}
|
|
|
|
/** Fields of the init status report populated when the tools source is `download`. */
|
|
export interface InitToolsDownloadFields {
|
|
/**
|
|
* Time taken to download the bundle, in milliseconds. Not populated when the bundle is downloaded
|
|
* and extracted concurrently.
|
|
*/
|
|
tools_download_duration_ms?: number;
|
|
/**
|
|
* Time taken to extract the bundle, in milliseconds. Not populated when the bundle is downloaded
|
|
* and extracted concurrently.
|
|
*/
|
|
tools_extraction_duration_ms?: number;
|
|
/**
|
|
* Total time taken to make the bundle available on disk, in milliseconds. This includes any time
|
|
* spent on a streaming attempt that failed and fell back to downloading before extracting.
|
|
*/
|
|
tools_total_duration_ms?: number;
|
|
/**
|
|
* Whether the relevant tools dotcom feature flags have been misconfigured.
|
|
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
|
|
tools_feature_flags_valid?: boolean;
|
|
/** The language of the single-language bundle that was downloaded, if any. */
|
|
tools_bundle_language?: string;
|
|
/**
|
|
* Whether we tried to download a single-language bundle, but it did not exist and we fell back to
|
|
* the combined bundle.
|
|
*/
|
|
tools_per_language_bundle_fallback?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Composes a `InitWithConfigStatusReport` from the given values.
|
|
*
|
|
* @param config The CodeQL Action configuration whose values should be added to the base status report.
|
|
* @param initStatusReport The base status report.
|
|
* @param configFile Optionally, the filename of the configuration file that was read.
|
|
* @param totalCacheSize The computed total TRAP cache size.
|
|
* @param overlayBaseDatabaseStats Statistics about the overlay database, if any.
|
|
* @returns
|
|
*/
|
|
export async function createInitWithConfigStatusReport(
|
|
config: Config,
|
|
initStatusReport: InitStatusReport,
|
|
configFile: string | undefined,
|
|
totalCacheSize: number,
|
|
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
|
|
dependencyCachingResults: DependencyCacheRestoreStatusReport | undefined,
|
|
): Promise<InitWithConfigStatusReport> {
|
|
const languages = config.languages.join(",");
|
|
const paths = (config.originalUserInput.paths || []).join(",");
|
|
const pathsIgnore = (config.originalUserInput["paths-ignore"] || []).join(
|
|
",",
|
|
);
|
|
const disableDefaultQueries = config.originalUserInput[
|
|
"disable-default-queries"
|
|
]
|
|
? languages
|
|
: "";
|
|
|
|
const queries: string[] = [];
|
|
let queriesInput = getOptionalInput("queries")?.trim();
|
|
if (queriesInput === undefined || queriesInput.startsWith("+")) {
|
|
queries.push(
|
|
...(config.originalUserInput.queries || []).map((q) => q.uses),
|
|
);
|
|
}
|
|
if (queriesInput !== undefined) {
|
|
queriesInput = queriesInput.startsWith("+")
|
|
? queriesInput.slice(1)
|
|
: queriesInput;
|
|
queries.push(...queriesInput.split(","));
|
|
}
|
|
|
|
let packs: Record<string, string[]> = {};
|
|
if (Array.isArray(config.computedConfig.packs)) {
|
|
packs[config.languages[0]] = config.computedConfig.packs;
|
|
} else if (config.computedConfig.packs !== undefined) {
|
|
packs = config.computedConfig.packs;
|
|
}
|
|
|
|
return {
|
|
...initStatusReport,
|
|
config_file: configFile ?? "",
|
|
disable_default_queries: disableDefaultQueries,
|
|
paths,
|
|
paths_ignore: pathsIgnore,
|
|
queries: queries.join(","),
|
|
packs: JSON.stringify(packs),
|
|
trap_cache_languages: Object.keys(config.trapCaches).join(","),
|
|
trap_cache_download_size_bytes: totalCacheSize,
|
|
trap_cache_download_duration_ms: Math.round(config.trapCacheDownloadTime),
|
|
overlay_base_database_download_size_bytes:
|
|
overlayBaseDatabaseStats?.databaseSizeBytes,
|
|
overlay_base_database_download_duration_ms:
|
|
overlayBaseDatabaseStats?.databaseDownloadDurationMs,
|
|
dependency_caching_restore_results: dependencyCachingResults,
|
|
query_filters: JSON.stringify(
|
|
config.originalUserInput["query-filters"] ?? [],
|
|
),
|
|
registries: JSON.stringify(
|
|
parseRegistriesWithoutCredentials(getOptionalInput("registries")) ?? [],
|
|
),
|
|
};
|
|
}
|
|
|
|
export async function sendUnhandledErrorStatusReport(
|
|
actionName: ActionName,
|
|
actionStartedAt: Date,
|
|
error: unknown,
|
|
logger: Logger,
|
|
): Promise<void> {
|
|
try {
|
|
// In the future, we may want to add a specific field for unhandled errors so we can
|
|
// create a dedicated monitor for them.
|
|
const statusReport = await createStatusReportBase(
|
|
actionName,
|
|
"failure",
|
|
actionStartedAt,
|
|
undefined,
|
|
undefined,
|
|
logger,
|
|
`Unhandled CodeQL Action error: ${getErrorMessage(error)}`,
|
|
error instanceof Error ? error.stack : undefined,
|
|
);
|
|
if (statusReport !== undefined) {
|
|
await sendStatusReport(statusReport);
|
|
}
|
|
} catch (e) {
|
|
logger.warning(
|
|
`Failed to send the unhandled error status report: ${getErrorMessage(e)}.`,
|
|
);
|
|
if (isInTestMode()) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|