Pull out a dedicated status report file

This commit is contained in:
Henry Mercer 2023-08-07 13:42:43 +01:00
parent c55207f0a2
commit c6d284324b
41 changed files with 768 additions and 669 deletions

View file

@ -5,7 +5,7 @@ import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { computeAutomationID, createStatusReportBase } from "./api-client";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { setupActionsVars, setupTests } from "./testing-utils";
import { initializeEnvironment, withTmpDir } from "./util";
@ -267,53 +267,3 @@ test("isAnalyzingDefaultBranch()", async (t) => {
getAdditionalInputStub.restore();
});
});
test("createStatusReportBase", async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
process.env["GITHUB_REF"] = "refs/heads/main";
process.env["GITHUB_SHA"] = "a".repeat(40);
process.env["GITHUB_RUN_ID"] = "100";
process.env["GITHUB_RUN_ATTEMPT"] = "2";
process.env["GITHUB_REPOSITORY"] = "octocat/HelloWorld";
process.env["CODEQL_ACTION_ANALYSIS_KEY"] = "analysis-key";
process.env["RUNNER_OS"] = "macOS";
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
getRequiredInput.withArgs("matrix").resolves("input/matrix");
const statusReport = await createStatusReportBase(
"init",
"failure",
new Date("May 19, 2023 05:19:00"),
"failure cause",
"exception stack trace",
);
t.assert(typeof statusReport.job_run_uuid === "string");
t.assert(statusReport.workflow_run_id === 100);
t.assert(statusReport.workflow_run_attempt === 2);
t.assert(
statusReport.workflow_name === (process.env["GITHUB_WORKFLOW"] || ""),
);
t.assert(statusReport.job_name === (process.env["GITHUB_JOB"] || ""));
t.assert(statusReport.analysis_key === "analysis-key");
t.assert(statusReport.commit_oid === process.env["GITHUB_SHA"]);
t.assert(statusReport.ref === process.env["GITHUB_REF"]);
t.assert(statusReport.action_name === "init");
t.assert(statusReport.action_oid === "unknown");
t.assert(
statusReport.started_at === process.env[EnvVar.WORKFLOW_STARTED_AT],
);
t.assert(
statusReport.action_started_at ===
new Date("May 19, 2023 05:19:00").toISOString(),
);
t.assert(statusReport.status === "failure");
t.assert(statusReport.cause === "failure cause");
t.assert(statusReport.exception === "exception stack trace");
t.assert(statusReport.runner_os === process.env["RUNNER_OS"]);
t.assert(typeof statusReport.action_version === "string");
});
});

View file

@ -239,126 +239,6 @@ function getRefFromEnv(): string {
return refEnv;
}
export type ActionName =
| "init"
| "autobuild"
| "finish"
| "upload-sarif"
| "init-post"
| "resolve-environment";
export type ActionStatus =
| "starting"
| "aborted"
| "success"
| "failure"
| "user-error";
// Any status report may include an array of EventReports associated with it.
export interface EventReport {
/** An enumerable description of the event. */
event: string;
/** Time this event started. */
started_at: string;
/** Time this event ended. */
completed_at: 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;
}
export interface StatusReportBase {
/**
* 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;
/** ID of the workflow run containing the action run. */
workflow_run_id: number;
/** Attempt number of the run containing the action run. */
workflow_run_attempt: number;
/** Workflow name. Converted to analysis_name further down the pipeline.. */
workflow_name: string;
/** Job name from the workflow. */
job_name: string;
/** Analysis key, normally composed from the workflow path and job name. */
analysis_key: string;
/** Value of the matrix for this instantiation of the job. */
matrix_vars?: string;
/** Commit oid that the workflow was triggered on. */
commit_oid: string;
/** Ref that the workflow was triggered on. */
ref: string;
/** Name of the action being executed. */
action_name: ActionName;
/** Version of the action being executed, as a ref. */
action_ref?: string;
/** Version of the action being executed, as a commit oid. */
action_oid: string;
/** Time the first action started. Normally the init action. */
started_at: string;
/** Time this action started. */
action_started_at: string;
/** Time this action completed, or undefined if not yet completed. */
completed_at?: string;
/** State this action is currently in. */
status: ActionStatus;
/**
* 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;
/**
* Information about the enablement of the ML-powered JS query pack.
*
* @see {@link util.getMlPoweredJsQueriesStatus}
*/
ml_powered_javascript_queries?: string;
/** Cause of the failure (or undefined if status is not failure). */
cause?: string;
/** Stack trace of the failure (or undefined if status is not failure). */
exception?: string;
/** Action runner operating system (context runner.os). */
runner_os: string;
/** Action runner hardware architecture (context runner.arch). */
runner_arch?: string;
/** Action runner operating system release (x.y.z from os.release()). */
runner_os_release?: string;
/** Action version (x.y.z from package.json). */
action_version: string;
/** CodeQL CLI version (x.y.z from the CLI). */
codeql_version?: string;
}
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 UserError ? "user-error" : "failure";
} else {
return "success";
}
}
export function getActionVersion(): string {
return pkg.version!;
}

View file

@ -5,6 +5,7 @@ import * as actionsUtil from "./actions-util";
import * as analyze from "./analyze";
import * as api from "./api-client";
import * as configUtils from "./config-utils";
import * as statusReport from "./status-report";
import {
setupTests,
setupActionsVars,
@ -27,9 +28,9 @@ test("analyze action with RAM & threads from environment variables", async (t) =
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["GITHUB_API_URL"] = "https://api.github.com";
sinon
.stub(api, "createStatusReportBase")
.resolves({} as actionsUtil.StatusReportBase);
sinon.stub(api, "sendStatusReport").resolves(true);
.stub(statusReport, "createStatusReportBase")
.resolves({} as statusReport.StatusReportBase);
sinon.stub(statusReport, "sendStatusReport").resolves(true);
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
const gitHubVersion: util.GitHubVersion = {

View file

@ -5,6 +5,7 @@ import * as actionsUtil from "./actions-util";
import * as analyze from "./analyze";
import * as api from "./api-client";
import * as configUtils from "./config-utils";
import * as statusReport from "./status-report";
import {
setupTests,
setupActionsVars,
@ -27,9 +28,9 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
process.env["GITHUB_API_URL"] = "https://api.github.com";
sinon
.stub(api, "createStatusReportBase")
.resolves({} as actionsUtil.StatusReportBase);
sinon.stub(api, "sendStatusReport").resolves(true);
.stub(statusReport, "createStatusReportBase")
.resolves({} as statusReport.StatusReportBase);
sinon.stub(statusReport, "sendStatusReport").resolves(true);
const gitHubVersion: util.GitHubVersion = {
type: util.GitHubVariant.DOTCOM,
};

View file

@ -5,7 +5,6 @@ import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import { DatabaseCreationTimings } from "./actions-util";
import {
CodeQLAnalysisError,
dbIsFinalized,
@ -15,7 +14,6 @@ import {
runQueries,
} from "./analyze";
import { getApiDetails, getGitHubVersion } from "./api-client";
import * as api from "./api-client";
import { runAutobuild } from "./autobuild";
import { getCodeQL } from "./codeql";
import { Config, getConfig, getMlPoweredJsQueriesStatus } from "./config-utils";
@ -25,6 +23,13 @@ import { Feature, Features } from "./feature-flags";
import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import * as statusReport from "./status-report";
import {
createStatusReportBase,
DatabaseCreationTimings,
getActionsStatus,
StatusReportBase,
} from "./status-report";
import { getTotalCacheSize, uploadTrapCaches } from "./trap-caching";
import * as uploadLib from "./upload-lib";
import { UploadResult } from "./upload-lib";
@ -36,8 +41,8 @@ interface AnalysisStatusReport
QueriesStatusReport {}
interface FinishStatusReport
extends actionsUtil.StatusReportBase,
actionsUtil.DatabaseCreationTimings,
extends StatusReportBase,
DatabaseCreationTimings,
AnalysisStatusReport {}
interface FinishWithTrapUploadStatusReport extends FinishStatusReport {
@ -57,18 +62,15 @@ export async function sendStatusReport(
didUploadTrapCaches: boolean,
logger: Logger,
) {
const status = actionsUtil.getActionsStatus(
error,
stats?.analyze_failure_language,
);
const statusReportBase = await api.createStatusReportBase(
const status = getActionsStatus(error, stats?.analyze_failure_language);
const statusReportBase = await createStatusReportBase(
"finish",
status,
startedAt,
error?.message,
error?.stack,
);
const statusReport: FinishStatusReport = {
const report: FinishStatusReport = {
...statusReportBase,
...(config
? {
@ -80,15 +82,15 @@ export async function sendStatusReport(
};
if (config && didUploadTrapCaches) {
const trapCacheUploadStatusReport: FinishWithTrapUploadStatusReport = {
...statusReport,
...report,
trap_cache_upload_duration_ms: Math.round(trapCacheUploadTime || 0),
trap_cache_upload_size_bytes: Math.round(
await getTotalCacheSize(config.trapCaches, logger),
),
};
await api.sendStatusReport(trapCacheUploadStatusReport);
await statusReport.sendStatusReport(trapCacheUploadStatusReport);
} else {
await api.sendStatusReport(statusReport);
await statusReport.sendStatusReport(report);
}
}
@ -181,8 +183,8 @@ async function run() {
const logger = getActionsLogger();
try {
if (
!(await api.sendStatusReport(
await api.createStatusReportBase("finish", "starting", startedAt),
!(await statusReport.sendStatusReport(
await createStatusReportBase("finish", "starting", startedAt),
))
) {
return;

View file

@ -6,7 +6,6 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import del from "del";
import * as yaml from "js-yaml";
import { DatabaseCreationTimings, EventReport } from "./actions-util";
import * as analysisPaths from "./analysis-paths";
import { CodeQL, getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
@ -18,6 +17,7 @@ import {
} from "./feature-flags";
import { isScannedLanguage, Language } from "./languages";
import { Logger } from "./logging";
import { DatabaseCreationTimings, EventReport } from "./status-report";
import { endTracingForCluster } from "./tracer-config";
import { validateSarifFileSchema } from "./upload-lib";
import * as util from "./util";

View file

@ -1,31 +1,14 @@
import * as os from "os";
import * as core from "@actions/core";
import * as githubUtils from "@actions/github/lib/utils";
import * as retry from "@octokit/plugin-retry";
import consoleLogLevel from "console-log-level";
import { getActionVersion, getRequiredInput } from "./actions-util";
import {
ActionName,
ActionStatus,
StatusReportBase,
getActionVersion,
getOptionalInput,
getRef,
getRequiredInput,
getWorkflowEventName,
getWorkflowRunAttempt,
getWorkflowRunID,
} from "./actions-util";
import { EnvVar } from "./environment";
import {
getCachedCodeQlVersion,
getRequiredEnvParam,
GITHUB_DOTCOM_URL,
GitHubVariant,
GitHubVersion,
isHTTPError,
isInTestMode,
parseGitHubUrl,
parseMatrixInput,
} from "./util";
@ -136,187 +119,6 @@ export async function getGitHubVersion(): Promise<GitHubVersion> {
return cachedGitHubVersion;
}
/**
* 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 startedAt 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')
*/
export async function createStatusReportBase(
actionName: ActionName,
status: ActionStatus,
actionStartedAt: Date,
cause?: string,
exception?: string,
): Promise<StatusReportBase> {
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();
const actionRef = process.env["GITHUB_ACTION_REF"];
const testingEnvironment = process.env[EnvVar.TESTING_ENVIRONMENT] || "";
// 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 statusReport: StatusReportBase = {
job_run_uuid: jobRunUUID,
workflow_run_id: workflowRunID,
workflow_run_attempt: workflowRunAttempt,
workflow_name: workflowName,
job_name: jobName,
analysis_key,
commit_oid: commitOid,
ref,
action_name: actionName,
action_ref: actionRef,
action_oid: "unknown", // TODO decide if it's possible to fill this in
started_at: workflowStartedAt,
action_started_at: actionStartedAt.toISOString(),
status,
testing_environment: testingEnvironment,
runner_os: runnerOs,
action_version: getActionVersion(),
};
// 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) {
// RUNNER_ARCH is available only in GHES 3.4 and later
// Values other than X86, X64, ARM, or ARM64 are discarded server side
statusReport.runner_arch = process.env["RUNNER_ARCH"];
}
if (runnerOs === "Windows" || runnerOs === "macOS") {
statusReport.runner_os_release = os.release();
}
if (codeQlCliVersion !== undefined) {
statusReport.codeql_version = codeQlCliVersion;
}
return statusReport;
}
const GENERIC_403_MSG =
"The repo on which this action is running is not opted-in to CodeQL code scanning.";
const GENERIC_404_MSG =
"Not authorized to use the CodeQL code scanning feature on this repo.";
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 code scanning 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.
*
* Returns whether sending the status report was successful of not.
*/
export async function sendStatusReport<S extends StatusReportBase>(
statusReport: S,
): Promise<boolean> {
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 true;
}
const nwo = getRequiredEnvParam("GITHUB_REPOSITORY");
const [owner, repo] = nwo.split("/");
const client = getApiClient();
try {
await client.request(
"PUT /repos/:owner/:repo/code-scanning/analysis/status",
{
owner,
repo,
data: statusReportJSON,
},
);
return true;
} catch (e) {
console.log(e);
if (isHTTPError(e)) {
switch (e.status) {
case 403:
if (
getWorkflowEventName() === "push" &&
process.env["GITHUB_ACTOR"] === "dependabot[bot]"
) {
core.setFailed(
'Workflows triggered by Dependabot on the "push" event run with read-only access. ' +
"Uploading Code Scanning results requires write access. " +
'To use Code Scanning 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 https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events.",
);
} else {
core.setFailed(e.message || GENERIC_403_MSG);
}
return false;
case 404:
core.setFailed(GENERIC_404_MSG);
return false;
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 true;
}
}
// 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.error(
"An unexpected error occurred when sending code scanning status report.",
);
return true;
}
}
/**
* Get the path of the currently executing workflow relative to the repository root.
*/

View file

@ -1,22 +1,22 @@
import * as core from "@actions/core";
import {
getActionsStatus,
getActionVersion,
getOptionalInput,
getTemporaryDirectory,
StatusReportBase,
} from "./actions-util";
import {
createStatusReportBase,
getGitHubVersion,
sendStatusReport,
} from "./api-client";
import { getGitHubVersion } from "./api-client";
import { determineAutobuildLanguages, runAutobuild } from "./autobuild";
import * as configUtils from "./config-utils";
import { EnvVar } from "./environment";
import { Language } from "./languages";
import { getActionsLogger } from "./logging";
import {
StatusReportBase,
getActionsStatus,
createStatusReportBase,
sendStatusReport,
} from "./status-report";
import {
checkGitHubVersionInRange,
initializeEnvironment,

View file

@ -6,22 +6,19 @@
import * as core from "@actions/core";
import {
getActionsStatus,
getTemporaryDirectory,
printDebugLogs,
StatusReportBase,
} from "./actions-util";
import {
createStatusReportBase,
getGitHubVersion,
sendStatusReport,
} from "./api-client";
import { getTemporaryDirectory, printDebugLogs } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import * as debugArtifacts from "./debug-artifacts";
import { Features } from "./feature-flags";
import * as initActionPostHelper from "./init-action-post-helper";
import { getActionsLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
StatusReportBase,
sendStatusReport,
createStatusReportBase,
getActionsStatus,
} from "./status-report";
import {
checkGitHubVersionInRange,
getRequiredEnvParam,

View file

@ -4,18 +4,12 @@ import * as core from "@actions/core";
import { v4 as uuidV4 } from "uuid";
import {
getActionsStatus,
getActionVersion,
getOptionalInput,
getRequiredInput,
getTemporaryDirectory,
StatusReportBase,
} from "./actions-util";
import {
createStatusReportBase,
getGitHubVersion,
sendStatusReport,
} from "./api-client";
import { getGitHubVersion } from "./api-client";
import { CodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { getMlPoweredJsQueriesStatus } from "./config-utils";
@ -26,6 +20,12 @@ import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import {
StatusReportBase,
createStatusReportBase,
getActionsStatus,
sendStatusReport,
} from "./status-report";
import { getTotalCacheSize } from "./trap-caching";
import {
checkForTimeout,

View file

@ -1,21 +1,21 @@
import * as core from "@actions/core";
import {
getActionsStatus,
getOptionalInput,
getRequiredInput,
getTemporaryDirectory,
} from "./actions-util";
import {
createStatusReportBase,
getGitHubVersion,
sendStatusReport,
} from "./api-client";
import { getGitHubVersion } from "./api-client";
import { CommandInvocationError } from "./codeql";
import * as configUtils from "./config-utils";
import { Language, resolveAlias } from "./languages";
import { getActionsLogger } from "./logging";
import { runResolveBuildEnvironment } from "./resolve-environment";
import {
sendStatusReport,
createStatusReportBase,
getActionsStatus,
} from "./status-report";
import { checkForTimeout, checkGitHubVersionInRange, wrapError } from "./util";
const ACTION_NAME = "resolve-environment";

60
src/status-report.test.ts Normal file
View file

@ -0,0 +1,60 @@
import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { EnvVar } from "./environment";
import { createStatusReportBase } from "./status-report";
import { setupTests, setupActionsVars } from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
test("createStatusReportBase", async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
process.env["CODEQL_ACTION_ANALYSIS_KEY"] = "analysis-key";
process.env["GITHUB_REF"] = "refs/heads/main";
process.env["GITHUB_REPOSITORY"] = "octocat/HelloWorld";
process.env["GITHUB_RUN_ATTEMPT"] = "2";
process.env["GITHUB_RUN_ID"] = "100";
process.env["GITHUB_SHA"] = "a".repeat(40);
process.env["RUNNER_OS"] = "macOS";
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
getRequiredInput.withArgs("matrix").resolves("input/matrix");
const statusReport = await createStatusReportBase(
"init",
"failure",
new Date("May 19, 2023 05:19:00"),
"failure cause",
"exception stack trace",
);
t.assert(typeof statusReport.job_run_uuid === "string");
t.assert(statusReport.workflow_run_id === 100);
t.assert(statusReport.workflow_run_attempt === 2);
t.assert(
statusReport.workflow_name === (process.env["GITHUB_WORKFLOW"] || ""),
);
t.assert(statusReport.job_name === (process.env["GITHUB_JOB"] || ""));
t.assert(statusReport.analysis_key === "analysis-key");
t.assert(statusReport.commit_oid === process.env["GITHUB_SHA"]);
t.assert(statusReport.ref === process.env["GITHUB_REF"]);
t.assert(statusReport.action_name === "init");
t.assert(statusReport.action_oid === "unknown");
t.assert(
statusReport.started_at === process.env[EnvVar.WORKFLOW_STARTED_AT],
);
t.assert(
statusReport.action_started_at ===
new Date("May 19, 2023 05:19:00").toISOString(),
);
t.assert(statusReport.status === "failure");
t.assert(statusReport.cause === "failure cause");
t.assert(statusReport.exception === "exception stack trace");
t.assert(statusReport.runner_os === process.env["RUNNER_OS"]);
t.assert(typeof statusReport.action_version === "string");
});
});

324
src/status-report.ts Normal file
View file

@ -0,0 +1,324 @@
import * as os from "os";
import * as core from "@actions/core";
import {
getWorkflowEventName,
getOptionalInput,
getRef,
getWorkflowRunID,
getWorkflowRunAttempt,
getActionVersion,
getRequiredInput,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { EnvVar } from "./environment";
import {
UserError,
isHTTPError,
getRequiredEnvParam,
getCachedCodeQlVersion,
isInTestMode,
GITHUB_DOTCOM_URL,
} from "./util";
export type ActionName =
| "init"
| "autobuild"
| "finish"
| "upload-sarif"
| "init-post"
| "resolve-environment";
export type ActionStatus =
| "starting"
| "aborted"
| "success"
| "failure"
| "user-error";
export interface StatusReportBase {
/**
* 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;
/** ID of the workflow run containing the action run. */
workflow_run_id: number;
/** Attempt number of the run containing the action run. */
workflow_run_attempt: number;
/** Workflow name. Converted to analysis_name further down the pipeline.. */
workflow_name: string;
/** Job name from the workflow. */
job_name: string;
/** Analysis key, normally composed from the workflow path and job name. */
analysis_key: string;
/** Value of the matrix for this instantiation of the job. */
matrix_vars?: string;
/** Commit oid that the workflow was triggered on. */
commit_oid: string;
/** Ref that the workflow was triggered on. */
ref: string;
/** Name of the action being executed. */
action_name: ActionName;
/** Version of the action being executed, as a ref. */
action_ref?: string;
/** Version of the action being executed, as a commit oid. */
action_oid: string;
/** Time the first action started. Normally the init action. */
started_at: string;
/** Time this action started. */
action_started_at: string;
/** Time this action completed, or undefined if not yet completed. */
completed_at?: string;
/** State this action is currently in. */
status: ActionStatus;
/**
* 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;
/**
* Information about the enablement of the ML-powered JS query pack.
*
* @see {@link util.getMlPoweredJsQueriesStatus}
*/
ml_powered_javascript_queries?: string;
/** Cause of the failure (or undefined if status is not failure). */
cause?: string;
/** Stack trace of the failure (or undefined if status is not failure). */
exception?: string;
/** Action runner operating system (context runner.os). */
runner_os: string;
/** Action runner hardware architecture (context runner.arch). */
runner_arch?: string;
/** Action runner operating system release (x.y.z from os.release()). */
runner_os_release?: string;
/** Action version (x.y.z from package.json). */
action_version: string;
/** CodeQL CLI version (x.y.z from the CLI). */
codeql_version?: string;
}
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 UserError ? "user-error" : "failure";
} else {
return "success";
}
}
// Any status report may include an array of EventReports associated with it.
export interface EventReport {
/** An enumerable description of the event. */
event: string;
/** Time this event started. */
started_at: string;
/** Time this event ended. */
completed_at: 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;
}
/**
* 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 startedAt 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')
*/
export async function createStatusReportBase(
actionName: ActionName,
status: ActionStatus,
actionStartedAt: Date,
cause?: string,
exception?: string,
): Promise<StatusReportBase> {
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();
const actionRef = process.env["GITHUB_ACTION_REF"];
const testingEnvironment = process.env[EnvVar.TESTING_ENVIRONMENT] || "";
// 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 statusReport: StatusReportBase = {
job_run_uuid: jobRunUUID,
workflow_run_id: workflowRunID,
workflow_run_attempt: workflowRunAttempt,
workflow_name: workflowName,
job_name: jobName,
analysis_key,
commit_oid: commitOid,
ref,
action_name: actionName,
action_ref: actionRef,
action_oid: "unknown", // TODO decide if it's possible to fill this in
started_at: workflowStartedAt,
action_started_at: actionStartedAt.toISOString(),
status,
testing_environment: testingEnvironment,
runner_os: runnerOs,
action_version: getActionVersion(),
};
// 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) {
// RUNNER_ARCH is available only in GHES 3.4 and later
// Values other than X86, X64, ARM, or ARM64 are discarded server side
statusReport.runner_arch = process.env["RUNNER_ARCH"];
}
if (runnerOs === "Windows" || runnerOs === "macOS") {
statusReport.runner_os_release = os.release();
}
if (codeQlCliVersion !== undefined) {
statusReport.codeql_version = codeQlCliVersion;
}
return statusReport;
}
const GENERIC_403_MSG =
"The repo on which this action is running is not opted-in to CodeQL code scanning.";
const GENERIC_404_MSG =
"Not authorized to use the CodeQL code scanning feature on this repo.";
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 code scanning 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.
*
* Returns whether sending the status report was successful of not.
*/
export async function sendStatusReport<S extends StatusReportBase>(
statusReport: S,
): Promise<boolean> {
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 true;
}
const nwo = getRequiredEnvParam("GITHUB_REPOSITORY");
const [owner, repo] = nwo.split("/");
const client = getApiClient();
try {
await client.request(
"PUT /repos/:owner/:repo/code-scanning/analysis/status",
{
owner,
repo,
data: statusReportJSON,
},
);
return true;
} catch (e) {
console.log(e);
if (isHTTPError(e)) {
switch (e.status) {
case 403:
if (
getWorkflowEventName() === "push" &&
process.env["GITHUB_ACTOR"] === "dependabot[bot]"
) {
core.setFailed(
'Workflows triggered by Dependabot on the "push" event run with read-only access. ' +
"Uploading Code Scanning results requires write access. " +
'To use Code Scanning 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 https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events.",
);
} else {
core.setFailed(e.message || GENERIC_403_MSG);
}
return false;
case 404:
core.setFailed(GENERIC_404_MSG);
return false;
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 true;
}
}
// 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.error(
"An unexpected error occurred when sending code scanning status report.",
);
return true;
}
}

View file

@ -2,9 +2,14 @@ import * as core from "@actions/core";
import * as actionsUtil from "./actions-util";
import { getActionVersion } from "./actions-util";
import { createStatusReportBase, sendStatusReport } from "./api-client";
import { getActionsLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
createStatusReportBase,
sendStatusReport,
StatusReportBase,
getActionsStatus,
} from "./status-report";
import * as upload_lib from "./upload-lib";
import {
getRequiredEnvParam,
@ -14,7 +19,7 @@ import {
} from "./util";
interface UploadSarifStatusReport
extends actionsUtil.StatusReportBase,
extends StatusReportBase,
upload_lib.UploadStatusReport {}
async function sendSuccessStatusReport(
@ -72,7 +77,7 @@ async function run() {
await sendStatusReport(
await createStatusReportBase(
"upload-sarif",
actionsUtil.getActionsStatus(error),
getActionsStatus(error),
startedAt,
message,
error.stack,