mirror of
https://github.com/github/codeql-action.git
synced 2026-01-04 05:30:16 +08:00
Introduce actions-util.ts
This commit is contained in:
39
src/actions-util.test.ts
Normal file
39
src/actions-util.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import test from "ava";
|
||||
|
||||
import { getRef, prepareLocalRunEnvironment } from "./actions-util";
|
||||
import { setupTests } from "./testing-utils";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getRef() throws on the empty string", (t) => {
|
||||
process.env["GITHUB_REF"] = "";
|
||||
t.throws(getRef);
|
||||
});
|
||||
|
||||
test("prepareEnvironment() when a local run", (t) => {
|
||||
const origLocalRun = process.env.CODEQL_LOCAL_RUN;
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = "false";
|
||||
process.env.GITHUB_JOB = "YYY";
|
||||
|
||||
prepareLocalRunEnvironment();
|
||||
|
||||
// unchanged
|
||||
t.deepEqual(process.env.GITHUB_JOB, "YYY");
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = "true";
|
||||
|
||||
prepareLocalRunEnvironment();
|
||||
|
||||
// unchanged
|
||||
t.deepEqual(process.env.GITHUB_JOB, "YYY");
|
||||
|
||||
process.env.GITHUB_JOB = "";
|
||||
|
||||
prepareLocalRunEnvironment();
|
||||
|
||||
// updated
|
||||
t.deepEqual(process.env.GITHUB_JOB, "UNKNOWN-JOB");
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = origLocalRun;
|
||||
});
|
||||
309
src/actions-util.ts
Normal file
309
src/actions-util.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as toolrunnner from "@actions/exec/lib/toolrunner";
|
||||
|
||||
import * as api from "./api-client";
|
||||
import * as sharedEnv from "./shared-environment";
|
||||
import { isLocalRun, GITHUB_DOTCOM_URL } from "./util";
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
const value = process.env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
core.debug(`${paramName}=${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all required environment variables are set in the context of a local run.
|
||||
*/
|
||||
export function prepareLocalRunEnvironment() {
|
||||
if (!isLocalRun()) {
|
||||
return;
|
||||
}
|
||||
|
||||
core.debug("Action is running locally.");
|
||||
if (!process.env.GITHUB_JOB) {
|
||||
core.exportVariable("GITHUB_JOB", "UNKNOWN-JOB");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SHA of the commit that is currently checked out.
|
||||
*/
|
||||
export async function getCommitOid(): Promise<string> {
|
||||
// Try to use git to get the current commit SHA. If that fails then
|
||||
// log but otherwise silently fall back to using the SHA from the environment.
|
||||
// The only time these two values will differ is during analysis of a PR when
|
||||
// the workflow has changed the current commit to the head commit instead of
|
||||
// the merge commit, which must mean that git is available.
|
||||
// Even if this does go wrong, it's not a huge problem for the alerts to
|
||||
// reported on the merge commit.
|
||||
try {
|
||||
let commitOid = "";
|
||||
await new toolrunnner.ToolRunner("git", ["rev-parse", "HEAD"], {
|
||||
silent: true,
|
||||
listeners: {
|
||||
stdout: (data) => {
|
||||
commitOid += data.toString();
|
||||
},
|
||||
stderr: (data) => {
|
||||
process.stderr.write(data);
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
return commitOid.trim();
|
||||
} catch (e) {
|
||||
core.info(
|
||||
`Failed to call git to get current commit. Continuing with data from environment: ${e}`
|
||||
);
|
||||
return getRequiredEnvParam("GITHUB_SHA");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the currently executing workflow.
|
||||
*/
|
||||
async function getWorkflowPath(): Promise<string> {
|
||||
const repo_nwo = getRequiredEnvParam("GITHUB_REPOSITORY").split("/");
|
||||
const owner = repo_nwo[0];
|
||||
const repo = repo_nwo[1];
|
||||
const run_id = Number(getRequiredEnvParam("GITHUB_RUN_ID"));
|
||||
|
||||
const apiClient = api.getActionsApiClient();
|
||||
const runsResponse = await apiClient.request(
|
||||
"GET /repos/:owner/:repo/actions/runs/:run_id",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
run_id,
|
||||
}
|
||||
);
|
||||
const workflowUrl = runsResponse.data.workflow_url;
|
||||
|
||||
const workflowResponse = await apiClient.request(`GET ${workflowUrl}`);
|
||||
|
||||
return workflowResponse.data.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workflow run ID.
|
||||
*/
|
||||
export function getWorkflowRunID(): number {
|
||||
const workflowRunID = parseInt(getRequiredEnvParam("GITHUB_RUN_ID"), 10);
|
||||
if (Number.isNaN(workflowRunID)) {
|
||||
throw new Error("GITHUB_RUN_ID must define a non NaN workflow run ID");
|
||||
}
|
||||
return workflowRunID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the analysis key paramter for the current job.
|
||||
*
|
||||
* This will combine the workflow path and current job name.
|
||||
* Computing this the first time requires making requests to
|
||||
* the github API, but after that the result will be cached.
|
||||
*/
|
||||
export async function getAnalysisKey(): Promise<string> {
|
||||
const analysisKeyEnvVar = "CODEQL_ACTION_ANALYSIS_KEY";
|
||||
|
||||
let analysisKey = process.env[analysisKeyEnvVar];
|
||||
if (analysisKey !== undefined) {
|
||||
return analysisKey;
|
||||
}
|
||||
|
||||
const workflowPath = await getWorkflowPath();
|
||||
const jobName = getRequiredEnvParam("GITHUB_JOB");
|
||||
|
||||
analysisKey = `${workflowPath}:${jobName}`;
|
||||
core.exportVariable(analysisKeyEnvVar, analysisKey);
|
||||
return analysisKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ref currently being analyzed.
|
||||
*/
|
||||
export function getRef(): string {
|
||||
// Will be in the form "refs/heads/master" on a push event
|
||||
// or in the form "refs/pull/N/merge" on a pull_request event
|
||||
const ref = getRequiredEnvParam("GITHUB_REF");
|
||||
|
||||
// For pull request refs we want to convert from the 'merge' ref
|
||||
// to the 'head' ref, as that is what we want to analyse.
|
||||
// There should have been some code earlier in the workflow to do
|
||||
// the checkout, but we have no way of verifying that here.
|
||||
const pull_ref_regex = /refs\/pull\/(\d+)\/merge/;
|
||||
if (pull_ref_regex.test(ref)) {
|
||||
return ref.replace(pull_ref_regex, "refs/pull/$1/head");
|
||||
} else {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
|
||||
type ActionName = "init" | "autobuild" | "finish" | "upload-sarif";
|
||||
type ActionStatus = "starting" | "aborted" | "success" | "failure";
|
||||
|
||||
export interface StatusReportBase {
|
||||
// ID of the workflow run containing the action run
|
||||
workflow_run_id: 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 if 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = process.env["GITHUB_SHA"] || "";
|
||||
const ref = getRef();
|
||||
const workflowRunIDStr = process.env["GITHUB_RUN_ID"];
|
||||
let workflowRunID = -1;
|
||||
if (workflowRunIDStr) {
|
||||
workflowRunID = parseInt(workflowRunIDStr, 10);
|
||||
}
|
||||
const workflowName = process.env["GITHUB_WORKFLOW"] || "";
|
||||
const jobName = process.env["GITHUB_JOB"] || "";
|
||||
const analysis_key = await getAnalysisKey();
|
||||
let workflowStartedAt = process.env[sharedEnv.CODEQL_WORKFLOW_STARTED_AT];
|
||||
if (workflowStartedAt === undefined) {
|
||||
workflowStartedAt = actionStartedAt.toISOString();
|
||||
core.exportVariable(
|
||||
sharedEnv.CODEQL_WORKFLOW_STARTED_AT,
|
||||
workflowStartedAt
|
||||
);
|
||||
}
|
||||
|
||||
const statusReport: StatusReportBase = {
|
||||
workflow_run_id: workflowRunID,
|
||||
workflow_name: workflowName,
|
||||
job_name: jobName,
|
||||
analysis_key,
|
||||
commit_oid: commitOid,
|
||||
ref,
|
||||
action_name: actionName,
|
||||
action_oid: "unknown", // TODO decide if it's possible to fill this in
|
||||
started_at: workflowStartedAt,
|
||||
action_started_at: actionStartedAt.toISOString(),
|
||||
status,
|
||||
};
|
||||
|
||||
// Add optional parameters
|
||||
if (cause) {
|
||||
statusReport.cause = cause;
|
||||
}
|
||||
if (exception) {
|
||||
statusReport.exception = exception;
|
||||
}
|
||||
if (status === "success" || status === "failure" || status === "aborted") {
|
||||
statusReport.completed_at = new Date().toISOString();
|
||||
}
|
||||
const matrix: string | undefined = core.getInput("matrix");
|
||||
if (matrix) {
|
||||
statusReport.matrix_vars = matrix;
|
||||
}
|
||||
|
||||
return statusReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
ignoreFailures?: boolean
|
||||
): Promise<boolean> {
|
||||
if (getRequiredEnvParam("GITHUB_SERVER_URL") !== GITHUB_DOTCOM_URL) {
|
||||
core.debug("Not sending status report to GitHub Enterprise");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isLocalRun()) {
|
||||
core.debug("Not sending status report because this is a local run");
|
||||
return true;
|
||||
}
|
||||
|
||||
const statusReportJSON = JSON.stringify(statusReport);
|
||||
core.debug(`Sending status report: ${statusReportJSON}`);
|
||||
|
||||
const nwo = getRequiredEnvParam("GITHUB_REPOSITORY");
|
||||
const [owner, repo] = nwo.split("/");
|
||||
const client = api.getActionsApiClient();
|
||||
const statusResponse = await client.request(
|
||||
"PUT /repos/:owner/:repo/code-scanning/analysis/status",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
data: statusReportJSON,
|
||||
}
|
||||
);
|
||||
|
||||
if (!ignoreFailures) {
|
||||
// If the status report request fails with a 403 or a 404, then this is a deliberate
|
||||
// message from the endpoint that the SARIF upload can be expected to fail too,
|
||||
// so the action should fail to avoid wasting actions minutes.
|
||||
//
|
||||
// Other failure responses (or lack thereof) could be transitory and should not
|
||||
// cause the action to fail.
|
||||
if (statusResponse.status === 403) {
|
||||
core.setFailed(
|
||||
"The repo on which this action is running is not opted-in to CodeQL code scanning."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (statusResponse.status === 404) {
|
||||
core.setFailed(
|
||||
"Not authorized to used the CodeQL code scanning feature on this repo."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { AnalysisStatusReport, runAnalyze } from "./analyze";
|
||||
import { getConfig } from "./config-utils";
|
||||
import { getActionsLogger } from "./logging";
|
||||
@@ -7,7 +8,7 @@ import { parseRepositoryNwo } from "./repository";
|
||||
import * as util from "./util";
|
||||
|
||||
interface FinishStatusReport
|
||||
extends util.StatusReportBase,
|
||||
extends actionsUtil.StatusReportBase,
|
||||
AnalysisStatusReport {}
|
||||
|
||||
async function sendStatusReport(
|
||||
@@ -19,7 +20,7 @@ async function sendStatusReport(
|
||||
stats?.analyze_failure_language !== undefined || error !== undefined
|
||||
? "failure"
|
||||
: "success";
|
||||
const statusReportBase = await util.createStatusReportBase(
|
||||
const statusReportBase = await actionsUtil.createStatusReportBase(
|
||||
"finish",
|
||||
status,
|
||||
startedAt,
|
||||
@@ -30,17 +31,21 @@ async function sendStatusReport(
|
||||
...statusReportBase,
|
||||
...(stats || {}),
|
||||
};
|
||||
await util.sendStatusReport(statusReport);
|
||||
await actionsUtil.sendStatusReport(statusReport);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const startedAt = new Date();
|
||||
let stats: AnalysisStatusReport | undefined = undefined;
|
||||
try {
|
||||
util.prepareLocalRunEnvironment();
|
||||
actionsUtil.prepareLocalRunEnvironment();
|
||||
if (
|
||||
!(await util.sendStatusReport(
|
||||
await util.createStatusReportBase("finish", "starting", startedAt),
|
||||
!(await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"finish",
|
||||
"starting",
|
||||
startedAt
|
||||
),
|
||||
true
|
||||
))
|
||||
) {
|
||||
@@ -48,7 +53,7 @@ async function run() {
|
||||
}
|
||||
const logger = getActionsLogger();
|
||||
const config = await getConfig(
|
||||
util.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
logger
|
||||
);
|
||||
if (config === undefined) {
|
||||
@@ -57,16 +62,16 @@ async function run() {
|
||||
);
|
||||
}
|
||||
stats = await runAnalyze(
|
||||
parseRepositoryNwo(util.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
await util.getCommitOid(),
|
||||
util.getRef(),
|
||||
await util.getAnalysisKey(),
|
||||
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||
util.getWorkflowRunID(),
|
||||
parseRepositoryNwo(actionsUtil.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
await actionsUtil.getCommitOid(),
|
||||
actionsUtil.getRef(),
|
||||
await actionsUtil.getAnalysisKey(),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||
actionsUtil.getWorkflowRunID(),
|
||||
core.getInput("checkout_path"),
|
||||
core.getInput("matrix"),
|
||||
core.getInput("token"),
|
||||
util.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
core.getInput("upload") === "true",
|
||||
"actions",
|
||||
core.getInput("output"),
|
||||
|
||||
@@ -3,7 +3,8 @@ import * as github from "@actions/github";
|
||||
import consoleLogLevel from "console-log-level";
|
||||
import * as path from "path";
|
||||
|
||||
import { getRequiredEnvParam, isLocalRun } from "./util";
|
||||
import { getRequiredEnvParam } from "./actions-util";
|
||||
import { isLocalRun } from "./util";
|
||||
|
||||
export const getApiClient = function (
|
||||
githubAuth: string,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { determineAutobuildLanguage, runAutobuild } from "./autobuild";
|
||||
import * as config_utils from "./config-utils";
|
||||
import { Language } from "./languages";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import * as util from "./util";
|
||||
|
||||
interface AutobuildStatusReport extends util.StatusReportBase {
|
||||
interface AutobuildStatusReport extends actionsUtil.StatusReportBase {
|
||||
// Comma-separated set of languages being autobuilt
|
||||
autobuild_languages: string;
|
||||
// Language that failed autobuilding (or undefined if all languages succeeded).
|
||||
@@ -23,7 +23,7 @@ async function sendCompletedStatusReport(
|
||||
failingLanguage !== undefined || cause !== undefined
|
||||
? "failure"
|
||||
: "success";
|
||||
const statusReportBase = await util.createStatusReportBase(
|
||||
const statusReportBase = await actionsUtil.createStatusReportBase(
|
||||
"autobuild",
|
||||
status,
|
||||
startedAt,
|
||||
@@ -35,7 +35,7 @@ async function sendCompletedStatusReport(
|
||||
autobuild_languages: allLanguages.join(","),
|
||||
autobuild_failure: failingLanguage,
|
||||
};
|
||||
await util.sendStatusReport(statusReport);
|
||||
await actionsUtil.sendStatusReport(statusReport);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
@@ -43,10 +43,14 @@ async function run() {
|
||||
const startedAt = new Date();
|
||||
let language: Language | undefined = undefined;
|
||||
try {
|
||||
util.prepareLocalRunEnvironment();
|
||||
actionsUtil.prepareLocalRunEnvironment();
|
||||
if (
|
||||
!(await util.sendStatusReport(
|
||||
await util.createStatusReportBase("autobuild", "starting", startedAt),
|
||||
!(await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"autobuild",
|
||||
"starting",
|
||||
startedAt
|
||||
),
|
||||
true
|
||||
))
|
||||
) {
|
||||
@@ -54,7 +58,7 @@ async function run() {
|
||||
}
|
||||
|
||||
const config = await config_utils.getConfig(
|
||||
util.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
logger
|
||||
);
|
||||
if (config === undefined) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as stream from "stream";
|
||||
import * as globalutil from "util";
|
||||
import uuidV4 from "uuid/v4";
|
||||
|
||||
import { getRequiredEnvParam } from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import * as defaults from "./defaults.json"; // Referenced from codeql-action-sync-tool!
|
||||
import { errorMatchers } from "./error-matcher";
|
||||
@@ -125,7 +126,7 @@ function getCodeQLActionRepository(mode: util.Mode): string {
|
||||
// Actions do not know their own repository name,
|
||||
// so we currently use this hack to find the name based on where our files are.
|
||||
// This can be removed once the change to the runner in https://github.com/actions/runner/pull/585 is deployed.
|
||||
const runnerTemp = util.getRequiredEnvParam("RUNNER_TEMP");
|
||||
const runnerTemp = getRequiredEnvParam("RUNNER_TEMP");
|
||||
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
|
||||
const relativeScriptPath = path.relative(actionsDirectory, __filename);
|
||||
// This handles the case where the Action does not come from an Action repository,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { CodeQL } from "./codeql";
|
||||
import * as configUtils from "./config-utils";
|
||||
import { initCodeQL, initConfig, injectWindowsTracer, runInit } from "./init";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { parseRepositoryNwo } from "./repository";
|
||||
import * as util from "./util";
|
||||
|
||||
interface InitSuccessStatusReport extends util.StatusReportBase {
|
||||
interface InitSuccessStatusReport extends actionsUtil.StatusReportBase {
|
||||
// Comma-separated list of languages that analysis was run for
|
||||
// This may be from the workflow file or may be calculated from repository contents
|
||||
languages: string;
|
||||
@@ -27,7 +27,7 @@ async function sendSuccessStatusReport(
|
||||
startedAt: Date,
|
||||
config: configUtils.Config
|
||||
) {
|
||||
const statusReportBase = await util.createStatusReportBase(
|
||||
const statusReportBase = await actionsUtil.createStatusReportBase(
|
||||
"init",
|
||||
"success",
|
||||
startedAt
|
||||
@@ -58,7 +58,7 @@ async function sendSuccessStatusReport(
|
||||
queries,
|
||||
};
|
||||
|
||||
await util.sendStatusReport(statusReport);
|
||||
await actionsUtil.sendStatusReport(statusReport);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
@@ -68,10 +68,10 @@ async function run() {
|
||||
let codeql: CodeQL;
|
||||
|
||||
try {
|
||||
util.prepareLocalRunEnvironment();
|
||||
actionsUtil.prepareLocalRunEnvironment();
|
||||
if (
|
||||
!(await util.sendStatusReport(
|
||||
await util.createStatusReportBase("init", "starting", startedAt),
|
||||
!(await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase("init", "starting", startedAt),
|
||||
true
|
||||
))
|
||||
) {
|
||||
@@ -81,9 +81,9 @@ async function run() {
|
||||
codeql = await initCodeQL(
|
||||
core.getInput("tools"),
|
||||
core.getInput("token"),
|
||||
util.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
util.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
util.getRequiredEnvParam("RUNNER_TOOL_CACHE"),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TOOL_CACHE"),
|
||||
"actions",
|
||||
logger
|
||||
);
|
||||
@@ -91,20 +91,25 @@ async function run() {
|
||||
core.getInput("languages"),
|
||||
core.getInput("queries"),
|
||||
core.getInput("config-file"),
|
||||
parseRepositoryNwo(util.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
util.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
util.getRequiredEnvParam("RUNNER_TOOL_CACHE"),
|
||||
parseRepositoryNwo(actionsUtil.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TEMP"),
|
||||
actionsUtil.getRequiredEnvParam("RUNNER_TOOL_CACHE"),
|
||||
codeql,
|
||||
util.getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
core.getInput("token"),
|
||||
util.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
logger
|
||||
);
|
||||
} catch (e) {
|
||||
core.setFailed(e.message);
|
||||
console.log(e);
|
||||
await util.sendStatusReport(
|
||||
await util.createStatusReportBase("init", "aborted", startedAt, e.message)
|
||||
await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"init",
|
||||
"aborted",
|
||||
startedAt,
|
||||
e.message
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -142,8 +147,8 @@ async function run() {
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
console.log(error);
|
||||
await util.sendStatusReport(
|
||||
await util.createStatusReportBase(
|
||||
await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"init",
|
||||
"failure",
|
||||
startedAt,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { parseRepositoryNwo } from "./repository";
|
||||
import * as upload_lib from "./upload-lib";
|
||||
import * as util from "./util";
|
||||
|
||||
interface UploadSarifStatusReport
|
||||
extends util.StatusReportBase,
|
||||
extends actionsUtil.StatusReportBase,
|
||||
upload_lib.UploadStatusReport {}
|
||||
|
||||
async function sendSuccessStatusReport(
|
||||
startedAt: Date,
|
||||
uploadStats: upload_lib.UploadStatusReport
|
||||
) {
|
||||
const statusReportBase = await util.createStatusReportBase(
|
||||
const statusReportBase = await actionsUtil.createStatusReportBase(
|
||||
"upload-sarif",
|
||||
"success",
|
||||
startedAt
|
||||
@@ -22,14 +22,18 @@ async function sendSuccessStatusReport(
|
||||
...statusReportBase,
|
||||
...uploadStats,
|
||||
};
|
||||
await util.sendStatusReport(statusReport);
|
||||
await actionsUtil.sendStatusReport(statusReport);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const startedAt = new Date();
|
||||
if (
|
||||
!(await util.sendStatusReport(
|
||||
await util.createStatusReportBase("upload-sarif", "starting", startedAt),
|
||||
!(await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"upload-sarif",
|
||||
"starting",
|
||||
startedAt
|
||||
),
|
||||
true
|
||||
))
|
||||
) {
|
||||
@@ -39,16 +43,16 @@ async function run() {
|
||||
try {
|
||||
const uploadStats = await upload_lib.upload(
|
||||
core.getInput("sarif_file"),
|
||||
parseRepositoryNwo(util.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
await util.getCommitOid(),
|
||||
util.getRef(),
|
||||
await util.getAnalysisKey(),
|
||||
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||
util.getWorkflowRunID(),
|
||||
parseRepositoryNwo(actionsUtil.getRequiredEnvParam("GITHUB_REPOSITORY")),
|
||||
await actionsUtil.getCommitOid(),
|
||||
actionsUtil.getRef(),
|
||||
await actionsUtil.getAnalysisKey(),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||
actionsUtil.getWorkflowRunID(),
|
||||
core.getInput("checkout_path"),
|
||||
core.getInput("matrix"),
|
||||
core.getInput("token"),
|
||||
util.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
actionsUtil.getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
"actions",
|
||||
getActionsLogger()
|
||||
);
|
||||
@@ -56,8 +60,8 @@ async function run() {
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
console.log(error);
|
||||
await util.sendStatusReport(
|
||||
await util.createStatusReportBase(
|
||||
await actionsUtil.sendStatusReport(
|
||||
await actionsUtil.createStatusReportBase(
|
||||
"upload-sarif",
|
||||
"failure",
|
||||
startedAt,
|
||||
|
||||
@@ -67,11 +67,6 @@ test("getThreadsFlag() throws if the threads input is not an integer", (t) => {
|
||||
t.throws(() => util.getThreadsFlag("hello!", getRunnerLogger(true)));
|
||||
});
|
||||
|
||||
test("getRef() throws on the empty string", (t) => {
|
||||
process.env["GITHUB_REF"] = "";
|
||||
t.throws(util.getRef);
|
||||
});
|
||||
|
||||
test("isLocalRun() runs correctly", (t) => {
|
||||
const origLocalRun = process.env.CODEQL_LOCAL_RUN;
|
||||
|
||||
@@ -93,34 +88,6 @@ test("isLocalRun() runs correctly", (t) => {
|
||||
process.env.CODEQL_LOCAL_RUN = origLocalRun;
|
||||
});
|
||||
|
||||
test("prepareEnvironment() when a local run", (t) => {
|
||||
const origLocalRun = process.env.CODEQL_LOCAL_RUN;
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = "false";
|
||||
process.env.GITHUB_JOB = "YYY";
|
||||
|
||||
util.prepareLocalRunEnvironment();
|
||||
|
||||
// unchanged
|
||||
t.deepEqual(process.env.GITHUB_JOB, "YYY");
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = "true";
|
||||
|
||||
util.prepareLocalRunEnvironment();
|
||||
|
||||
// unchanged
|
||||
t.deepEqual(process.env.GITHUB_JOB, "YYY");
|
||||
|
||||
process.env.GITHUB_JOB = "";
|
||||
|
||||
util.prepareLocalRunEnvironment();
|
||||
|
||||
// updated
|
||||
t.deepEqual(process.env.GITHUB_JOB, "UNKNOWN-JOB");
|
||||
|
||||
process.env.CODEQL_LOCAL_RUN = origLocalRun;
|
||||
});
|
||||
|
||||
test("getExtraOptionsEnvParam() succeeds on valid JSON with invalid options (for now)", (t) => {
|
||||
const origExtraOptions = process.env.CODEQL_ACTION_EXTRA_OPTIONS;
|
||||
|
||||
|
||||
307
src/util.ts
307
src/util.ts
@@ -1,13 +1,9 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as toolrunnner from "@actions/exec/lib/toolrunner";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
import * as api from "./api-client";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import * as sharedEnv from "./shared-environment";
|
||||
|
||||
/**
|
||||
* Are we running on actions, or not.
|
||||
@@ -19,18 +15,6 @@ export type Mode = "actions" | "runner";
|
||||
*/
|
||||
export const GITHUB_DOTCOM_URL = "https://github.com";
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
const value = process.env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
core.debug(`${paramName}=${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the extra options for the codeql commands.
|
||||
*/
|
||||
@@ -57,297 +41,6 @@ export function isLocalRun(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all required environment variables are set in the context of a local run.
|
||||
*/
|
||||
export function prepareLocalRunEnvironment() {
|
||||
if (!isLocalRun()) {
|
||||
return;
|
||||
}
|
||||
|
||||
core.debug("Action is running locally.");
|
||||
if (!process.env.GITHUB_JOB) {
|
||||
core.exportVariable("GITHUB_JOB", "UNKNOWN-JOB");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SHA of the commit that is currently checked out.
|
||||
*/
|
||||
export async function getCommitOid(): Promise<string> {
|
||||
// Try to use git to get the current commit SHA. If that fails then
|
||||
// log but otherwise silently fall back to using the SHA from the environment.
|
||||
// The only time these two values will differ is during analysis of a PR when
|
||||
// the workflow has changed the current commit to the head commit instead of
|
||||
// the merge commit, which must mean that git is available.
|
||||
// Even if this does go wrong, it's not a huge problem for the alerts to
|
||||
// reported on the merge commit.
|
||||
try {
|
||||
let commitOid = "";
|
||||
await new toolrunnner.ToolRunner("git", ["rev-parse", "HEAD"], {
|
||||
silent: true,
|
||||
listeners: {
|
||||
stdout: (data) => {
|
||||
commitOid += data.toString();
|
||||
},
|
||||
stderr: (data) => {
|
||||
process.stderr.write(data);
|
||||
},
|
||||
},
|
||||
}).exec();
|
||||
return commitOid.trim();
|
||||
} catch (e) {
|
||||
core.info(
|
||||
`Failed to call git to get current commit. Continuing with data from environment: ${e}`
|
||||
);
|
||||
return getRequiredEnvParam("GITHUB_SHA");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the currently executing workflow.
|
||||
*/
|
||||
async function getWorkflowPath(): Promise<string> {
|
||||
const repo_nwo = getRequiredEnvParam("GITHUB_REPOSITORY").split("/");
|
||||
const owner = repo_nwo[0];
|
||||
const repo = repo_nwo[1];
|
||||
const run_id = Number(getRequiredEnvParam("GITHUB_RUN_ID"));
|
||||
|
||||
const apiClient = api.getActionsApiClient();
|
||||
const runsResponse = await apiClient.request(
|
||||
"GET /repos/:owner/:repo/actions/runs/:run_id",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
run_id,
|
||||
}
|
||||
);
|
||||
const workflowUrl = runsResponse.data.workflow_url;
|
||||
|
||||
const workflowResponse = await apiClient.request(`GET ${workflowUrl}`);
|
||||
|
||||
return workflowResponse.data.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workflow run ID.
|
||||
*/
|
||||
export function getWorkflowRunID(): number {
|
||||
const workflowRunID = parseInt(getRequiredEnvParam("GITHUB_RUN_ID"), 10);
|
||||
if (Number.isNaN(workflowRunID)) {
|
||||
throw new Error("GITHUB_RUN_ID must define a non NaN workflow run ID");
|
||||
}
|
||||
return workflowRunID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the analysis key paramter for the current job.
|
||||
*
|
||||
* This will combine the workflow path and current job name.
|
||||
* Computing this the first time requires making requests to
|
||||
* the github API, but after that the result will be cached.
|
||||
*/
|
||||
export async function getAnalysisKey(): Promise<string> {
|
||||
const analysisKeyEnvVar = "CODEQL_ACTION_ANALYSIS_KEY";
|
||||
|
||||
let analysisKey = process.env[analysisKeyEnvVar];
|
||||
if (analysisKey !== undefined) {
|
||||
return analysisKey;
|
||||
}
|
||||
|
||||
const workflowPath = await getWorkflowPath();
|
||||
const jobName = getRequiredEnvParam("GITHUB_JOB");
|
||||
|
||||
analysisKey = `${workflowPath}:${jobName}`;
|
||||
core.exportVariable(analysisKeyEnvVar, analysisKey);
|
||||
return analysisKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ref currently being analyzed.
|
||||
*/
|
||||
export function getRef(): string {
|
||||
// Will be in the form "refs/heads/master" on a push event
|
||||
// or in the form "refs/pull/N/merge" on a pull_request event
|
||||
const ref = getRequiredEnvParam("GITHUB_REF");
|
||||
|
||||
// For pull request refs we want to convert from the 'merge' ref
|
||||
// to the 'head' ref, as that is what we want to analyse.
|
||||
// There should have been some code earlier in the workflow to do
|
||||
// the checkout, but we have no way of verifying that here.
|
||||
const pull_ref_regex = /refs\/pull\/(\d+)\/merge/;
|
||||
if (pull_ref_regex.test(ref)) {
|
||||
return ref.replace(pull_ref_regex, "refs/pull/$1/head");
|
||||
} else {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
|
||||
type ActionName = "init" | "autobuild" | "finish" | "upload-sarif";
|
||||
type ActionStatus = "starting" | "aborted" | "success" | "failure";
|
||||
|
||||
export interface StatusReportBase {
|
||||
// ID of the workflow run containing the action run
|
||||
workflow_run_id: 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 if 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = process.env["GITHUB_SHA"] || "";
|
||||
const ref = getRef();
|
||||
const workflowRunIDStr = process.env["GITHUB_RUN_ID"];
|
||||
let workflowRunID = -1;
|
||||
if (workflowRunIDStr) {
|
||||
workflowRunID = parseInt(workflowRunIDStr, 10);
|
||||
}
|
||||
const workflowName = process.env["GITHUB_WORKFLOW"] || "";
|
||||
const jobName = process.env["GITHUB_JOB"] || "";
|
||||
const analysis_key = await getAnalysisKey();
|
||||
let workflowStartedAt = process.env[sharedEnv.CODEQL_WORKFLOW_STARTED_AT];
|
||||
if (workflowStartedAt === undefined) {
|
||||
workflowStartedAt = actionStartedAt.toISOString();
|
||||
core.exportVariable(
|
||||
sharedEnv.CODEQL_WORKFLOW_STARTED_AT,
|
||||
workflowStartedAt
|
||||
);
|
||||
}
|
||||
|
||||
const statusReport: StatusReportBase = {
|
||||
workflow_run_id: workflowRunID,
|
||||
workflow_name: workflowName,
|
||||
job_name: jobName,
|
||||
analysis_key,
|
||||
commit_oid: commitOid,
|
||||
ref,
|
||||
action_name: actionName,
|
||||
action_oid: "unknown", // TODO decide if it's possible to fill this in
|
||||
started_at: workflowStartedAt,
|
||||
action_started_at: actionStartedAt.toISOString(),
|
||||
status,
|
||||
};
|
||||
|
||||
// Add optional parameters
|
||||
if (cause) {
|
||||
statusReport.cause = cause;
|
||||
}
|
||||
if (exception) {
|
||||
statusReport.exception = exception;
|
||||
}
|
||||
if (status === "success" || status === "failure" || status === "aborted") {
|
||||
statusReport.completed_at = new Date().toISOString();
|
||||
}
|
||||
const matrix: string | undefined = core.getInput("matrix");
|
||||
if (matrix) {
|
||||
statusReport.matrix_vars = matrix;
|
||||
}
|
||||
|
||||
return statusReport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
ignoreFailures?: boolean
|
||||
): Promise<boolean> {
|
||||
if (getRequiredEnvParam("GITHUB_SERVER_URL") !== GITHUB_DOTCOM_URL) {
|
||||
core.debug("Not sending status report to GitHub Enterprise");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isLocalRun()) {
|
||||
core.debug("Not sending status report because this is a local run");
|
||||
return true;
|
||||
}
|
||||
|
||||
const statusReportJSON = JSON.stringify(statusReport);
|
||||
core.debug(`Sending status report: ${statusReportJSON}`);
|
||||
|
||||
const nwo = getRequiredEnvParam("GITHUB_REPOSITORY");
|
||||
const [owner, repo] = nwo.split("/");
|
||||
const client = api.getActionsApiClient();
|
||||
const statusResponse = await client.request(
|
||||
"PUT /repos/:owner/:repo/code-scanning/analysis/status",
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
data: statusReportJSON,
|
||||
}
|
||||
);
|
||||
|
||||
if (!ignoreFailures) {
|
||||
// If the status report request fails with a 403 or a 404, then this is a deliberate
|
||||
// message from the endpoint that the SARIF upload can be expected to fail too,
|
||||
// so the action should fail to avoid wasting actions minutes.
|
||||
//
|
||||
// Other failure responses (or lack thereof) could be transitory and should not
|
||||
// cause the action to fail.
|
||||
if (statusResponse.status === 403) {
|
||||
core.setFailed(
|
||||
"The repo on which this action is running is not opted-in to CodeQL code scanning."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (statusResponse.status === 404) {
|
||||
core.setFailed(
|
||||
"Not authorized to used the CodeQL code scanning feature on this repo."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of all the tool names contained in the given sarif contents.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user