From 001f2d7fb5433813e54a8dc05d65c687d52e2c49 Mon Sep 17 00:00:00 2001 From: Chuan-kai Lin Date: Mon, 9 Dec 2024 13:48:28 -0800 Subject: [PATCH] Move Git functions to git-utils.ts --- src/actions-util.test.ts | 97 +++---- src/actions-util.ts | 406 ------------------------------ src/analyze-action-env.test.ts | 3 +- src/analyze-action-input.test.ts | 3 +- src/analyze.ts | 19 +- src/codeql.ts | 2 +- src/database-upload.test.ts | 13 +- src/database-upload.ts | 5 +- src/git-utils.ts | 416 +++++++++++++++++++++++++++++++ src/status-report.ts | 2 +- src/trap-caching.test.ts | 13 +- src/trap-caching.ts | 9 +- src/upload-lib.ts | 7 +- 13 files changed, 507 insertions(+), 488 deletions(-) create mode 100644 src/git-utils.ts diff --git a/src/actions-util.test.ts b/src/actions-util.test.ts index fb1a1842de..307df4af2b 100644 --- a/src/actions-util.test.ts +++ b/src/actions-util.test.ts @@ -8,6 +8,7 @@ import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; import { computeAutomationID } from "./api-client"; import { EnvVar } from "./environment"; +import * as gitUtils from "./git-utils"; import { setupActionsVars, setupTests } from "./testing-utils"; import { initializeEnvironment, withTmpDir } from "./util"; @@ -15,7 +16,7 @@ setupTests(test); test("getRef() throws on the empty string", async (t) => { process.env["GITHUB_REF"] = ""; - await t.throwsAsync(actionsUtil.getRef); + await t.throwsAsync(gitUtils.getRef); }); test("getRef() returns merge PR ref if GITHUB_SHA still checked out", async (t) => { @@ -26,10 +27,10 @@ test("getRef() returns merge PR ref if GITHUB_SHA still checked out", async (t) process.env["GITHUB_REF"] = expectedRef; process.env["GITHUB_SHA"] = currentSha; - const callback = sinon.stub(actionsUtil, "getCommitOid"); + const callback = sinon.stub(gitUtils, "getCommitOid"); callback.withArgs("HEAD").resolves(currentSha); - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, expectedRef); callback.restore(); }); @@ -43,11 +44,11 @@ test("getRef() returns merge PR ref if GITHUB_REF still checked out but sha has process.env["GITHUB_SHA"] = "b".repeat(40); const sha = "a".repeat(40); - const callback = sinon.stub(actionsUtil, "getCommitOid"); + const callback = sinon.stub(gitUtils, "getCommitOid"); callback.withArgs("refs/remotes/pull/1/merge").resolves(sha); callback.withArgs("HEAD").resolves(sha); - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, expectedRef); callback.restore(); }); @@ -59,11 +60,11 @@ test("getRef() returns head PR ref if GITHUB_REF no longer checked out", async ( process.env["GITHUB_REF"] = "refs/pull/1/merge"; process.env["GITHUB_SHA"] = "a".repeat(40); - const callback = sinon.stub(actionsUtil, "getCommitOid"); + const callback = sinon.stub(gitUtils, "getCommitOid"); callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40)); callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40)); - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, "refs/pull/1/head"); callback.restore(); }); @@ -80,11 +81,11 @@ test("getRef() returns ref provided as an input and ignores current HEAD", async process.env["GITHUB_REF"] = "refs/pull/1/merge"; process.env["GITHUB_SHA"] = "a".repeat(40); - const callback = sinon.stub(actionsUtil, "getCommitOid"); + const callback = sinon.stub(gitUtils, "getCommitOid"); callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40)); callback.withArgs("HEAD").resolves("b".repeat(40)); - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, "refs/pull/2/merge"); callback.restore(); getAdditionalInputStub.restore(); @@ -100,7 +101,7 @@ test("getRef() returns CODE_SCANNING_REF as a fallback for GITHUB_REF", async (t process.env["GITHUB_REF"] = ""; process.env["GITHUB_SHA"] = currentSha; - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, expectedRef); }); }); @@ -114,7 +115,7 @@ test("getRef() returns GITHUB_REF over CODE_SCANNING_REF if both are provided", process.env["GITHUB_REF"] = expectedRef; process.env["GITHUB_SHA"] = currentSha; - const actualRef = await actionsUtil.getRef(); + const actualRef = await gitUtils.getRef(); t.deepEqual(actualRef, expectedRef); }); }); @@ -127,7 +128,7 @@ test("getRef() throws an error if only `ref` is provided as an input", async (t) await t.throwsAsync( async () => { - await actionsUtil.getRef(); + await gitUtils.getRef(); }, { instanceOf: Error, @@ -148,7 +149,7 @@ test("getRef() throws an error if only `sha` is provided as an input", async (t) await t.throwsAsync( async () => { - await actionsUtil.getRef(); + await gitUtils.getRef(); }, { instanceOf: Error, @@ -219,7 +220,7 @@ test("initializeEnvironment", (t) => { test("isAnalyzingDefaultBranch()", async (t) => { process.env["GITHUB_EVENT_NAME"] = "push"; process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "true"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true); process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "false"; await withTmpDir(async (tmpDir) => { @@ -237,13 +238,13 @@ test("isAnalyzingDefaultBranch()", async (t) => { process.env["GITHUB_REF"] = "main"; process.env["GITHUB_SHA"] = "1234"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true); process.env["GITHUB_REF"] = "refs/heads/main"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true); process.env["GITHUB_REF"] = "feature"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), false); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false); fs.writeFileSync( envFile, @@ -253,7 +254,7 @@ test("isAnalyzingDefaultBranch()", async (t) => { ); process.env["GITHUB_EVENT_NAME"] = "schedule"; process.env["GITHUB_REF"] = "refs/heads/main"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true); const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput"); getAdditionalInputStub @@ -264,7 +265,7 @@ test("isAnalyzingDefaultBranch()", async (t) => { .resolves("0000000000000000000000000000000000000000"); process.env["GITHUB_EVENT_NAME"] = "schedule"; process.env["GITHUB_REF"] = "refs/heads/main"; - t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), false); + t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false); getAdditionalInputStub.restore(); }); }); @@ -274,7 +275,7 @@ test("determineBaseBranchHeadCommitOid non-pullrequest", async (t) => { process.env["GITHUB_EVENT_NAME"] = "hucairz"; process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a"; - const result = await actionsUtil.determineBaseBranchHeadCommitOid(__dirname); + const result = await gitUtils.determineBaseBranchHeadCommitOid(__dirname); t.deepEqual(result, undefined); t.deepEqual(0, infoStub.callCount); @@ -288,7 +289,7 @@ test("determineBaseBranchHeadCommitOid not git repository", async (t) => { process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a"; await withTmpDir(async (tmpDir) => { - await actionsUtil.determineBaseBranchHeadCommitOid(tmpDir); + await gitUtils.determineBaseBranchHeadCommitOid(tmpDir); }); t.deepEqual(1, infoStub.callCount); @@ -306,7 +307,7 @@ test("determineBaseBranchHeadCommitOid other error", async (t) => { process.env["GITHUB_EVENT_NAME"] = "pull_request"; process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a"; - const result = await actionsUtil.determineBaseBranchHeadCommitOid( + const result = await gitUtils.determineBaseBranchHeadCommitOid( path.join(__dirname, "../../i-dont-exist"), ); t.deepEqual(result, undefined); @@ -326,39 +327,39 @@ test("determineBaseBranchHeadCommitOid other error", async (t) => { }); test("decodeGitFilePath unquoted strings", async (t) => { - t.deepEqual(actionsUtil.decodeGitFilePath("foo"), "foo"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo bar"), "foo bar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\\\bar"), "foo\\\\bar"); - t.deepEqual(actionsUtil.decodeGitFilePath('foo\\"bar'), 'foo\\"bar'); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\001bar"), "foo\\001bar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\abar"), "foo\\abar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\bbar"), "foo\\bbar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\fbar"), "foo\\fbar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\nbar"), "foo\\nbar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\rbar"), "foo\\rbar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\tbar"), "foo\\tbar"); - t.deepEqual(actionsUtil.decodeGitFilePath("foo\\vbar"), "foo\\vbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo"), "foo"); + t.deepEqual(gitUtils.decodeGitFilePath("foo bar"), "foo bar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\\\bar"), "foo\\\\bar"); + t.deepEqual(gitUtils.decodeGitFilePath('foo\\"bar'), 'foo\\"bar'); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\001bar"), "foo\\001bar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\abar"), "foo\\abar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\bbar"), "foo\\bbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\fbar"), "foo\\fbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\nbar"), "foo\\nbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\rbar"), "foo\\rbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\tbar"), "foo\\tbar"); + t.deepEqual(gitUtils.decodeGitFilePath("foo\\vbar"), "foo\\vbar"); t.deepEqual( - actionsUtil.decodeGitFilePath("\\a\\b\\f\\n\\r\\t\\v"), + gitUtils.decodeGitFilePath("\\a\\b\\f\\n\\r\\t\\v"), "\\a\\b\\f\\n\\r\\t\\v", ); }); test("decodeGitFilePath quoted strings", async (t) => { - t.deepEqual(actionsUtil.decodeGitFilePath('"foo"'), "foo"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo bar"'), "foo bar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\\\bar"'), "foo\\bar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\"bar"'), 'foo"bar'); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\001bar"'), "foo\x01bar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\abar"'), "foo\x07bar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\bbar"'), "foo\bbar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\fbar"'), "foo\fbar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\nbar"'), "foo\nbar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\rbar"'), "foo\rbar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\tbar"'), "foo\tbar"); - t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\vbar"'), "foo\vbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo"'), "foo"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo bar"'), "foo bar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\\\bar"'), "foo\\bar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\"bar"'), 'foo"bar'); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\001bar"'), "foo\x01bar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\abar"'), "foo\x07bar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\bbar"'), "foo\bbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\fbar"'), "foo\fbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\nbar"'), "foo\nbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\rbar"'), "foo\rbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\tbar"'), "foo\tbar"); + t.deepEqual(gitUtils.decodeGitFilePath('"foo\\vbar"'), "foo\vbar"); t.deepEqual( - actionsUtil.decodeGitFilePath('"\\a\\b\\f\\n\\r\\t\\v"'), + gitUtils.decodeGitFilePath('"\\a\\b\\f\\n\\r\\t\\v"'), "\x07\b\f\n\r\t\v", ); }); diff --git a/src/actions-util.ts b/src/actions-util.ts index 941e0ba01b..4fe56e98cd 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -49,382 +49,6 @@ export function getTemporaryDirectory(): string { : getRequiredEnvParam("RUNNER_TEMP"); } -async function runGitCommand( - checkoutPath: string | undefined, - args: string[], - customErrorMessage: string, -): Promise { - let stdout = ""; - let stderr = ""; - core.debug(`Running git command: git ${args.join(" ")}`); - try { - await new toolrunner.ToolRunner(await safeWhich.safeWhich("git"), args, { - silent: true, - listeners: { - stdout: (data) => { - stdout += data.toString(); - }, - stderr: (data) => { - stderr += data.toString(); - }, - }, - cwd: checkoutPath, - }).exec(); - return stdout; - } catch (error) { - let reason = stderr; - if (stderr.includes("not a git repository")) { - reason = - "The checkout path provided to the action does not appear to be a git repository."; - } - core.info(`git call failed. ${customErrorMessage} Error: ${reason}`); - throw error; - } -} - -/** - * Gets the SHA of the commit that is currently checked out. - */ -export const getCommitOid = async function ( - checkoutPath: string, - ref = "HEAD", -): Promise { - // 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 { - const stdout = await runGitCommand( - checkoutPath, - ["rev-parse", ref], - "Continuing with commit SHA from user input or environment.", - ); - return stdout.trim(); - } catch { - return getOptionalInput("sha") || getRequiredEnvParam("GITHUB_SHA"); - } -}; - -/** - * If the action was triggered by a pull request, determine the commit sha at - * the head of the base branch, using the merge commit that this workflow analyzes. - * Returns undefined if run by other triggers or the base branch commit cannot be - * determined. - */ -export const determineBaseBranchHeadCommitOid = async function ( - checkoutPathOverride?: string, -): Promise { - if (getWorkflowEventName() !== "pull_request") { - return undefined; - } - - const mergeSha = getRequiredEnvParam("GITHUB_SHA"); - const checkoutPath = - checkoutPathOverride ?? getOptionalInput("checkout_path"); - - try { - let commitOid = ""; - let baseOid = ""; - let headOid = ""; - - const stdout = await runGitCommand( - checkoutPath, - ["show", "-s", "--format=raw", mergeSha], - "Will calculate the base branch SHA on the server.", - ); - - for (const data of stdout.split("\n")) { - if (data.startsWith("commit ") && commitOid === "") { - commitOid = data.substring(7); - } else if (data.startsWith("parent ")) { - if (baseOid === "") { - baseOid = data.substring(7); - } else if (headOid === "") { - headOid = data.substring(7); - } - } - } - - // Let's confirm our assumptions: We had a merge commit and the parsed parent data looks correct - if ( - commitOid === mergeSha && - headOid.length === 40 && - baseOid.length === 40 - ) { - return baseOid; - } - return undefined; - } catch { - return undefined; - } -}; - -/** - * Deepen the git history of HEAD by one level. Errors are logged. - * - * This function uses the `checkout_path` to determine the repository path and - * works only when called from `analyze` or `upload-sarif`. - */ -export const deepenGitHistory = async function () { - try { - await runGitCommand( - getOptionalInput("checkout_path"), - [ - "fetch", - "origin", - "HEAD", - "--no-tags", - "--no-recurse-submodules", - "--deepen=1", - ], - "Cannot deepen the shallow repository.", - ); - } catch { - // Errors are already logged by runGitCommand() - } -}; - -/** - * Fetch the given remote branch. Errors are logged. - * - * This function uses the `checkout_path` to determine the repository path and - * works only when called from `analyze` or `upload-sarif`. - */ -export const gitFetch = async function (branch: string, extraFlags: string[]) { - try { - await runGitCommand( - getOptionalInput("checkout_path"), - ["fetch", "--no-tags", ...extraFlags, "origin", `${branch}:${branch}`], - `Cannot fetch ${branch}.`, - ); - } catch { - // Errors are already logged by runGitCommand() - } -}; - -/** - * Repack the git repository, using with the given flags. Errors are logged. - * - * This function uses the `checkout_path` to determine the repository path and - * works only when called from `analyze` or `upload-sarif`. - */ -export const gitRepack = async function (flags: string[]) { - try { - await runGitCommand( - getOptionalInput("checkout_path"), - ["repack", ...flags], - "Cannot repack the repository.", - ); - } catch { - // Errors are already logged by runGitCommand() - } -}; - -/** - * Compute the all merge bases between the given refs. Returns an empty array - * if no merge base is found, or if there is an error. - * - * This function uses the `checkout_path` to determine the repository path and - * works only when called from `analyze` or `upload-sarif`. - */ -export const getAllGitMergeBases = async function ( - refs: string[], -): Promise { - try { - const stdout = await runGitCommand( - getOptionalInput("checkout_path"), - ["merge-base", "--all", ...refs], - `Cannot get merge base of ${refs}.`, - ); - return stdout.trim().split("\n"); - } catch { - return []; - } -}; - -/** - * Compute the diff hunk headers between the two given refs. - * - * This function uses the `checkout_path` to determine the repository path and - * works only when called from `analyze` or `upload-sarif`. - * - * @returns an array of diff hunk headers (one element per line), or undefined - * if the action was not triggered by a pull request, or if the diff could not - * be determined. - */ -export const getGitDiffHunkHeaders = async function ( - fromRef: string, - toRef: string, -): Promise { - let stdout = ""; - try { - stdout = await runGitCommand( - getOptionalInput("checkout_path"), - [ - "-c", - "core.quotePath=false", - "diff", - "--no-renames", - "--irreversible-delete", - "-U0", - fromRef, - toRef, - ], - `Cannot get diff from ${fromRef} to ${toRef}.`, - ); - } catch { - return undefined; - } - - const headers: string[] = []; - for (const line of stdout.split("\n")) { - if ( - line.startsWith("--- ") || - line.startsWith("+++ ") || - line.startsWith("@@ ") - ) { - headers.push(line); - } - } - return headers; -}; - -/** - * Decode, if necessary, a file path produced by Git. See - * https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath - * for details on how Git encodes file paths with special characters. - * - * This function works only for Git output with `core.quotePath=false`. - */ -export const decodeGitFilePath = function (filePath: string): string { - if (filePath.startsWith('"') && filePath.endsWith('"')) { - filePath = filePath.substring(1, filePath.length - 1); - return filePath.replace( - /\\([abfnrtv\\"]|[0-7]{1,3})/g, - (_match, seq: string) => { - switch (seq[0]) { - case "a": - return "\x07"; - case "b": - return "\b"; - case "f": - return "\f"; - case "n": - return "\n"; - case "r": - return "\r"; - case "t": - return "\t"; - case "v": - return "\v"; - case "\\": - return "\\"; - case '"': - return '"'; - default: - // Both String.fromCharCode() and String.fromCodePoint() works only - // for constructing an entire character at once. If a Unicode - // character is encoded as a sequence of escaped bytes, calling these - // methods sequentially on the individual byte values would *not* - // produce the original multi-byte Unicode character. As a result, - // this implementation works only with the Git option core.quotePath - // set to false. - return String.fromCharCode(parseInt(seq, 8)); - } - }, - ); - } - return filePath; -}; - -/** - * Get the ref currently being analyzed. - */ -export async function getRef(): Promise { - // 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 refInput = getOptionalInput("ref"); - const shaInput = getOptionalInput("sha"); - const checkoutPath = - getOptionalInput("checkout_path") || - getOptionalInput("source-root") || - getRequiredEnvParam("GITHUB_WORKSPACE"); - - const hasRefInput = !!refInput; - const hasShaInput = !!shaInput; - // If one of 'ref' or 'sha' are provided, both are required - if ((hasRefInput || hasShaInput) && !(hasRefInput && hasShaInput)) { - throw new ConfigurationError( - "Both 'ref' and 'sha' are required if one of them is provided.", - ); - } - - const ref = refInput || getRefFromEnv(); - const sha = shaInput || getRequiredEnvParam("GITHUB_SHA"); - - // If the ref is a user-provided input, we have to skip logic - // and assume that it is really where they want to upload the results. - if (refInput) { - return refInput; - } - - // For pull request refs we want to detect whether the workflow - // has run `git checkout HEAD^2` to analyze the 'head' ref rather - // than the 'merge' ref. If so, we want to convert the ref that - // we report back. - const pull_ref_regex = /refs\/pull\/(\d+)\/merge/; - if (!pull_ref_regex.test(ref)) { - return ref; - } - - const head = await getCommitOid(checkoutPath, "HEAD"); - - // in actions/checkout@v2+ we can check if git rev-parse HEAD == GITHUB_SHA - // in actions/checkout@v1 this may not be true as it checks out the repository - // using GITHUB_REF. There is a subtle race condition where - // git rev-parse GITHUB_REF != GITHUB_SHA, so we must check - // git rev-parse GITHUB_REF == git rev-parse HEAD instead. - const hasChangedRef = - sha !== head && - (await getCommitOid( - checkoutPath, - ref.replace(/^refs\/pull\//, "refs/remotes/pull/"), - )) !== head; - - if (hasChangedRef) { - const newRef = ref.replace(pull_ref_regex, "refs/pull/$1/head"); - core.debug( - `No longer on merge commit, rewriting ref from ${ref} to ${newRef}.`, - ); - return newRef; - } else { - return ref; - } -} - -function getRefFromEnv(): string { - // To workaround a limitation of Actions dynamic workflows not setting - // the GITHUB_REF in some cases, we accept also the ref within the - // CODE_SCANNING_REF variable. When possible, however, we prefer to use - // the GITHUB_REF as that is a protected variable and cannot be overwritten. - let refEnv: string; - try { - refEnv = getRequiredEnvParam("GITHUB_REF"); - } catch (e) { - // If the GITHUB_REF is not set, we try to rescue by getting the - // CODE_SCANNING_REF. - const maybeRef = process.env["CODE_SCANNING_REF"]; - if (maybeRef === undefined || maybeRef.length === 0) { - throw e; - } - refEnv = maybeRef; - } - return refEnv; -} - export function getActionVersion(): string { return pkg.version!; } @@ -472,36 +96,6 @@ export function getWorkflowEvent(): any { } } -function removeRefsHeadsPrefix(ref: string): string { - return ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref; -} - -/** - * Returns whether we are analyzing the default branch for the repository. - * - * This first checks the environment variable `CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH`. This - * environment variable can be set in cases where repository information might not be available, for - * example dynamic workflows. - */ -export async function isAnalyzingDefaultBranch(): Promise { - if (process.env.CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH === "true") { - return true; - } - - // Get the current ref and trim and refs/heads/ prefix - let currentRef = await getRef(); - currentRef = removeRefsHeadsPrefix(currentRef); - - const event = getWorkflowEvent(); - let defaultBranch = event?.repository?.default_branch; - - if (getWorkflowEventName() === "schedule") { - defaultBranch = removeRefsHeadsPrefix(getRefFromEnv()); - } - - return currentRef === defaultBranch; -} - export async function printDebugLogs(config: Config) { for (const language of config.languages) { const databaseDirectory = getCodeQLDatabasePath(config, language); diff --git a/src/analyze-action-env.test.ts b/src/analyze-action-env.test.ts index 73a97974df..9f6baa729c 100644 --- a/src/analyze-action-env.test.ts +++ b/src/analyze-action-env.test.ts @@ -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 gitUtils from "./git-utils"; import * as statusReport from "./status-report"; import { setupTests, @@ -31,7 +32,7 @@ test("analyze action with RAM & threads from environment variables", async (t) = .stub(statusReport, "createStatusReportBase") .resolves({} as statusReport.StatusReportBase); sinon.stub(statusReport, "sendStatusReport").resolves(); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const gitHubVersion: util.GitHubVersion = { type: util.GitHubVariant.DOTCOM, diff --git a/src/analyze-action-input.test.ts b/src/analyze-action-input.test.ts index 6afdbd9be4..1af32bcc6d 100644 --- a/src/analyze-action-input.test.ts +++ b/src/analyze-action-input.test.ts @@ -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 gitUtils from "./git-utils"; import * as statusReport from "./status-report"; import { setupTests, @@ -47,7 +48,7 @@ test("analyze action with RAM & threads from action inputs", async (t) => { optionalInputStub.withArgs("cleanup-level").returns("none"); optionalInputStub.withArgs("expect-error").returns("false"); sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); setupActionsVars(tmpDir, tmpDir); mockFeatureFlagApiEndpoint(200, {}); diff --git a/src/analyze.ts b/src/analyze.ts index a540527255..fa67211fa2 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -17,6 +17,7 @@ import * as configUtils from "./config-utils"; import { addDiagnostic, makeDiagnostic } from "./diagnostics"; import { EnvVar } from "./environment"; import { FeatureEnablement, Feature } from "./feature-flags"; +import * as gitUtils from "./git-utils"; import { isScannedLanguage, Language } from "./languages"; import { Logger, withGroupAsync } from "./logging"; import { DatabaseCreationTimings, EventReport } from "./status-report"; @@ -304,33 +305,33 @@ async function getPullRequestEditedDiffRanges( // Step 1: Deepen from the PR merge commit to the base branch head and the PR // topic branch head, so that the PR merge commit is no longer considered a // grafted commit. - await actionsUtil.deepenGitHistory(); + await gitUtils.deepenGitHistory(); // Step 2: Fetch the base branch shallow history. This step ensures that the // base branch name is present in the local repository. Normally the base // branch name would be added by Step 4. However, if the base branch head is // an ancestor of the PR topic branch head, Step 4 would fail without doing // anything, so we need to fetch the base branch explicitly. - await actionsUtil.gitFetch(baseRef, ["--depth=1"]); + await gitUtils.gitFetch(baseRef, ["--depth=1"]); // Step 3: Fetch the PR topic branch history, stopping when we reach commits // that are reachable from the base branch head. - await actionsUtil.gitFetch(headRef, [`--shallow-exclude=${baseRef}`]); + await gitUtils.gitFetch(headRef, [`--shallow-exclude=${baseRef}`]); // Step 4: Fetch the base branch history, stopping when we reach commits that // are reachable from the PR topic branch head. - await actionsUtil.gitFetch(baseRef, [`--shallow-exclude=${headRef}`]); + await gitUtils.gitFetch(baseRef, [`--shallow-exclude=${headRef}`]); // Step 5: Repack the history to remove the shallow grafts that were added by // the previous fetches. This step works around a bug that causes subsequent // deepening fetches to fail with "fatal: error in object: unshallow ". // See https://stackoverflow.com/q/63878612 - await actionsUtil.gitRepack(["-d"]); + await gitUtils.gitRepack(["-d"]); // Step 6: Deepen the history so that we have the merge bases between the base // branch and the PR topic branch. - await actionsUtil.deepenGitHistory(); + await gitUtils.deepenGitHistory(); // To compute the exact same diff as GitHub would compute for the PR, we need // to use the same merge base as GitHub. That is easy to do if there is only // one merge base, which is by far the most common case. If there are multiple // merge bases, we stop without producing a diff range. - const mergeBases = await actionsUtil.getAllGitMergeBases([baseRef, headRef]); + const mergeBases = await gitUtils.getAllGitMergeBases([baseRef, headRef]); logger.info(`Merge bases: ${mergeBases.join(", ")}`); if (mergeBases.length !== 1) { logger.info( @@ -340,7 +341,7 @@ async function getPullRequestEditedDiffRanges( return undefined; } - const diffHunkHeaders = await actionsUtil.getGitDiffHunkHeaders( + const diffHunkHeaders = await gitUtils.getGitDiffHunkHeaders( mergeBases[0], headRef, ); @@ -353,7 +354,7 @@ async function getPullRequestEditedDiffRanges( let changedFile = ""; for (const line of diffHunkHeaders) { if (line.startsWith("+++ ")) { - const filePath = actionsUtil.decodeGitFilePath(line.substring(4)); + const filePath = gitUtils.decodeGitFilePath(line.substring(4)); if (filePath.startsWith("b/")) { // The file was edited: track all hunks in the file changedFile = filePath.substring(2); diff --git a/src/codeql.ts b/src/codeql.ts index 61fdec615c..486c2d637a 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -10,7 +10,6 @@ import { CommandInvocationError, getActionVersion, getOptionalInput, - isAnalyzingDefaultBranch, runTool, } from "./actions-util"; import * as api from "./api-client"; @@ -24,6 +23,7 @@ import { Feature, FeatureEnablement, } from "./feature-flags"; +import { isAnalyzingDefaultBranch } from "./git-utils"; import { Language } from "./languages"; import { Logger } from "./logging"; import * as setupCodeql from "./setup-codeql"; diff --git a/src/database-upload.test.ts b/src/database-upload.test.ts index 8216a62f09..bf09dc6c97 100644 --- a/src/database-upload.test.ts +++ b/src/database-upload.test.ts @@ -10,6 +10,7 @@ import * as apiClient from "./api-client"; import { setCodeQL } from "./codeql"; import { Config } from "./config-utils"; import { uploadDatabases } from "./database-upload"; +import * as gitUtils from "./git-utils"; import { Language } from "./languages"; import { RepositoryNwo } from "./repository"; import { @@ -75,7 +76,7 @@ test("Abort database upload if 'upload-database' input set to false", async (t) .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("false"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const loggedMessages = []; await uploadDatabases( @@ -102,7 +103,7 @@ test("Abort database upload if running against GHES", async (t) => { .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const config = getTestConfig(tmpDir); config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" }; @@ -132,7 +133,7 @@ test("Abort database upload if not analyzing default branch", async (t) => { .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); const loggedMessages = []; await uploadDatabases( @@ -158,7 +159,7 @@ test("Don't crash if uploading a database fails", async (t) => { .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); await mockHttpRequests(500); @@ -194,7 +195,7 @@ test("Successfully uploading a database to github.com", async (t) => { .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); await mockHttpRequests(201); @@ -228,7 +229,7 @@ test("Successfully uploading a database to GHEC-DR", async (t) => { .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const databaseUploadSpy = await mockHttpRequests(201); diff --git a/src/database-upload.ts b/src/database-upload.ts index 4d661da90b..8fbd479c99 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -4,6 +4,7 @@ import * as actionsUtil from "./actions-util"; import { getApiClient, GitHubApiDetails } from "./api-client"; import { getCodeQL } from "./codeql"; import { Config } from "./config-utils"; +import * as gitUtils from "./git-utils"; import { Logger } from "./logging"; import { RepositoryNwo } from "./repository"; import * as util from "./util"; @@ -34,7 +35,7 @@ export async function uploadDatabases( return; } - if (!(await actionsUtil.isAnalyzingDefaultBranch())) { + if (!(await gitUtils.isAnalyzingDefaultBranch())) { // We only want to upload a database if we are analyzing the default branch. logger.debug("Not analyzing default branch. Skipping upload."); return; @@ -62,7 +63,7 @@ export async function uploadDatabases( const bundledDb = await bundleDb(config, language, codeql, language); const bundledDbSize = fs.statSync(bundledDb).size; const bundledDbReadStream = fs.createReadStream(bundledDb); - const commitOid = await actionsUtil.getCommitOid( + const commitOid = await gitUtils.getCommitOid( actionsUtil.getRequiredInput("checkout_path"), ); try { diff --git a/src/git-utils.ts b/src/git-utils.ts new file mode 100644 index 0000000000..55cb0994b1 --- /dev/null +++ b/src/git-utils.ts @@ -0,0 +1,416 @@ +import * as core from "@actions/core"; +import * as toolrunner from "@actions/exec/lib/toolrunner"; +import * as safeWhich from "@chrisgavin/safe-which"; + +import { + getOptionalInput, + getWorkflowEvent, + getWorkflowEventName, +} from "./actions-util"; +import { ConfigurationError, getRequiredEnvParam } from "./util"; + +async function runGitCommand( + checkoutPath: string | undefined, + args: string[], + customErrorMessage: string, +): Promise { + let stdout = ""; + let stderr = ""; + core.debug(`Running git command: git ${args.join(" ")}`); + try { + await new toolrunner.ToolRunner(await safeWhich.safeWhich("git"), args, { + silent: true, + listeners: { + stdout: (data) => { + stdout += data.toString(); + }, + stderr: (data) => { + stderr += data.toString(); + }, + }, + cwd: checkoutPath, + }).exec(); + return stdout; + } catch (error) { + let reason = stderr; + if (stderr.includes("not a git repository")) { + reason = + "The checkout path provided to the action does not appear to be a git repository."; + } + core.info(`git call failed. ${customErrorMessage} Error: ${reason}`); + throw error; + } +} + +/** + * Gets the SHA of the commit that is currently checked out. + */ +export const getCommitOid = async function ( + checkoutPath: string, + ref = "HEAD", +): Promise { + // 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 { + const stdout = await runGitCommand( + checkoutPath, + ["rev-parse", ref], + "Continuing with commit SHA from user input or environment.", + ); + return stdout.trim(); + } catch { + return getOptionalInput("sha") || getRequiredEnvParam("GITHUB_SHA"); + } +}; + +/** + * If the action was triggered by a pull request, determine the commit sha at + * the head of the base branch, using the merge commit that this workflow analyzes. + * Returns undefined if run by other triggers or the base branch commit cannot be + * determined. + */ +export const determineBaseBranchHeadCommitOid = async function ( + checkoutPathOverride?: string, +): Promise { + if (getWorkflowEventName() !== "pull_request") { + return undefined; + } + + const mergeSha = getRequiredEnvParam("GITHUB_SHA"); + const checkoutPath = + checkoutPathOverride ?? getOptionalInput("checkout_path"); + + try { + let commitOid = ""; + let baseOid = ""; + let headOid = ""; + + const stdout = await runGitCommand( + checkoutPath, + ["show", "-s", "--format=raw", mergeSha], + "Will calculate the base branch SHA on the server.", + ); + + for (const data of stdout.split("\n")) { + if (data.startsWith("commit ") && commitOid === "") { + commitOid = data.substring(7); + } else if (data.startsWith("parent ")) { + if (baseOid === "") { + baseOid = data.substring(7); + } else if (headOid === "") { + headOid = data.substring(7); + } + } + } + + // Let's confirm our assumptions: We had a merge commit and the parsed parent data looks correct + if ( + commitOid === mergeSha && + headOid.length === 40 && + baseOid.length === 40 + ) { + return baseOid; + } + return undefined; + } catch { + return undefined; + } +}; + +/** + * Deepen the git history of HEAD by one level. Errors are logged. + * + * This function uses the `checkout_path` to determine the repository path and + * works only when called from `analyze` or `upload-sarif`. + */ +export const deepenGitHistory = async function () { + try { + await runGitCommand( + getOptionalInput("checkout_path"), + [ + "fetch", + "origin", + "HEAD", + "--no-tags", + "--no-recurse-submodules", + "--deepen=1", + ], + "Cannot deepen the shallow repository.", + ); + } catch { + // Errors are already logged by runGitCommand() + } +}; + +/** + * Fetch the given remote branch. Errors are logged. + * + * This function uses the `checkout_path` to determine the repository path and + * works only when called from `analyze` or `upload-sarif`. + */ +export const gitFetch = async function (branch: string, extraFlags: string[]) { + try { + await runGitCommand( + getOptionalInput("checkout_path"), + ["fetch", "--no-tags", ...extraFlags, "origin", `${branch}:${branch}`], + `Cannot fetch ${branch}.`, + ); + } catch { + // Errors are already logged by runGitCommand() + } +}; + +/** + * Repack the git repository, using with the given flags. Errors are logged. + * + * This function uses the `checkout_path` to determine the repository path and + * works only when called from `analyze` or `upload-sarif`. + */ +export const gitRepack = async function (flags: string[]) { + try { + await runGitCommand( + getOptionalInput("checkout_path"), + ["repack", ...flags], + "Cannot repack the repository.", + ); + } catch { + // Errors are already logged by runGitCommand() + } +}; + +/** + * Compute the all merge bases between the given refs. Returns an empty array + * if no merge base is found, or if there is an error. + * + * This function uses the `checkout_path` to determine the repository path and + * works only when called from `analyze` or `upload-sarif`. + */ +export const getAllGitMergeBases = async function ( + refs: string[], +): Promise { + try { + const stdout = await runGitCommand( + getOptionalInput("checkout_path"), + ["merge-base", "--all", ...refs], + `Cannot get merge base of ${refs}.`, + ); + return stdout.trim().split("\n"); + } catch { + return []; + } +}; + +/** + * Compute the diff hunk headers between the two given refs. + * + * This function uses the `checkout_path` to determine the repository path and + * works only when called from `analyze` or `upload-sarif`. + * + * @returns an array of diff hunk headers (one element per line), or undefined + * if the action was not triggered by a pull request, or if the diff could not + * be determined. + */ +export const getGitDiffHunkHeaders = async function ( + fromRef: string, + toRef: string, +): Promise { + let stdout = ""; + try { + stdout = await runGitCommand( + getOptionalInput("checkout_path"), + [ + "-c", + "core.quotePath=false", + "diff", + "--no-renames", + "--irreversible-delete", + "-U0", + fromRef, + toRef, + ], + `Cannot get diff from ${fromRef} to ${toRef}.`, + ); + } catch { + return undefined; + } + + const headers: string[] = []; + for (const line of stdout.split("\n")) { + if ( + line.startsWith("--- ") || + line.startsWith("+++ ") || + line.startsWith("@@ ") + ) { + headers.push(line); + } + } + return headers; +}; + +/** + * Decode, if necessary, a file path produced by Git. See + * https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath + * for details on how Git encodes file paths with special characters. + * + * This function works only for Git output with `core.quotePath=false`. + */ +export const decodeGitFilePath = function (filePath: string): string { + if (filePath.startsWith('"') && filePath.endsWith('"')) { + filePath = filePath.substring(1, filePath.length - 1); + return filePath.replace( + /\\([abfnrtv\\"]|[0-7]{1,3})/g, + (_match, seq: string) => { + switch (seq[0]) { + case "a": + return "\x07"; + case "b": + return "\b"; + case "f": + return "\f"; + case "n": + return "\n"; + case "r": + return "\r"; + case "t": + return "\t"; + case "v": + return "\v"; + case "\\": + return "\\"; + case '"': + return '"'; + default: + // Both String.fromCharCode() and String.fromCodePoint() works only + // for constructing an entire character at once. If a Unicode + // character is encoded as a sequence of escaped bytes, calling these + // methods sequentially on the individual byte values would *not* + // produce the original multi-byte Unicode character. As a result, + // this implementation works only with the Git option core.quotePath + // set to false. + return String.fromCharCode(parseInt(seq, 8)); + } + }, + ); + } + return filePath; +}; + +function getRefFromEnv(): string { + // To workaround a limitation of Actions dynamic workflows not setting + // the GITHUB_REF in some cases, we accept also the ref within the + // CODE_SCANNING_REF variable. When possible, however, we prefer to use + // the GITHUB_REF as that is a protected variable and cannot be overwritten. + let refEnv: string; + try { + refEnv = getRequiredEnvParam("GITHUB_REF"); + } catch (e) { + // If the GITHUB_REF is not set, we try to rescue by getting the + // CODE_SCANNING_REF. + const maybeRef = process.env["CODE_SCANNING_REF"]; + if (maybeRef === undefined || maybeRef.length === 0) { + throw e; + } + refEnv = maybeRef; + } + return refEnv; +} + +/** + * Get the ref currently being analyzed. + */ +export async function getRef(): Promise { + // 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 refInput = getOptionalInput("ref"); + const shaInput = getOptionalInput("sha"); + const checkoutPath = + getOptionalInput("checkout_path") || + getOptionalInput("source-root") || + getRequiredEnvParam("GITHUB_WORKSPACE"); + + const hasRefInput = !!refInput; + const hasShaInput = !!shaInput; + // If one of 'ref' or 'sha' are provided, both are required + if ((hasRefInput || hasShaInput) && !(hasRefInput && hasShaInput)) { + throw new ConfigurationError( + "Both 'ref' and 'sha' are required if one of them is provided.", + ); + } + + const ref = refInput || getRefFromEnv(); + const sha = shaInput || getRequiredEnvParam("GITHUB_SHA"); + + // If the ref is a user-provided input, we have to skip logic + // and assume that it is really where they want to upload the results. + if (refInput) { + return refInput; + } + + // For pull request refs we want to detect whether the workflow + // has run `git checkout HEAD^2` to analyze the 'head' ref rather + // than the 'merge' ref. If so, we want to convert the ref that + // we report back. + const pull_ref_regex = /refs\/pull\/(\d+)\/merge/; + if (!pull_ref_regex.test(ref)) { + return ref; + } + + const head = await getCommitOid(checkoutPath, "HEAD"); + + // in actions/checkout@v2+ we can check if git rev-parse HEAD == GITHUB_SHA + // in actions/checkout@v1 this may not be true as it checks out the repository + // using GITHUB_REF. There is a subtle race condition where + // git rev-parse GITHUB_REF != GITHUB_SHA, so we must check + // git rev-parse GITHUB_REF == git rev-parse HEAD instead. + const hasChangedRef = + sha !== head && + (await getCommitOid( + checkoutPath, + ref.replace(/^refs\/pull\//, "refs/remotes/pull/"), + )) !== head; + + if (hasChangedRef) { + const newRef = ref.replace(pull_ref_regex, "refs/pull/$1/head"); + core.debug( + `No longer on merge commit, rewriting ref from ${ref} to ${newRef}.`, + ); + return newRef; + } else { + return ref; + } +} + +function removeRefsHeadsPrefix(ref: string): string { + return ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref; +} + +/** + * Returns whether we are analyzing the default branch for the repository. + * + * This first checks the environment variable `CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH`. This + * environment variable can be set in cases where repository information might not be available, for + * example dynamic workflows. + */ +export async function isAnalyzingDefaultBranch(): Promise { + if (process.env.CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH === "true") { + return true; + } + + // Get the current ref and trim and refs/heads/ prefix + let currentRef = await getRef(); + currentRef = removeRefsHeadsPrefix(currentRef); + + const event = getWorkflowEvent(); + let defaultBranch = event?.repository?.default_branch; + + if (getWorkflowEventName() === "schedule") { + defaultBranch = removeRefsHeadsPrefix(getRefFromEnv()); + } + + return currentRef === defaultBranch; +} diff --git a/src/status-report.ts b/src/status-report.ts index 14972c5da4..1cb37830aa 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -5,7 +5,6 @@ import * as core from "@actions/core"; import { getWorkflowEventName, getOptionalInput, - getRef, getWorkflowRunID, getWorkflowRunAttempt, getActionVersion, @@ -16,6 +15,7 @@ import { getAnalysisKey, getApiClient } from "./api-client"; import { type Config } from "./config-utils"; import { DocUrl } from "./doc-url"; import { EnvVar } from "./environment"; +import { getRef } from "./git-utils"; import { Logger } from "./logging"; import { ConfigurationError, diff --git a/src/trap-caching.test.ts b/src/trap-caching.test.ts index fa098ca586..17a215b045 100644 --- a/src/trap-caching.test.ts +++ b/src/trap-caching.test.ts @@ -14,6 +14,7 @@ import { } from "./codeql"; import * as configUtils from "./config-utils"; import { Feature } from "./feature-flags"; +import * as gitUtils from "./git-utils"; import { Language } from "./languages"; import { getRunnerLogger } from "./logging"; import { @@ -96,7 +97,7 @@ function getTestConfigWithTempDir(tempDir: string): configUtils.Config { test("check flags for JS, analyzing default branch", async (t) => { await util.withTmpDir(async (tmpDir) => { const config = getTestConfigWithTempDir(tmpDir); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const result = await getTrapCachingExtractorConfigArgsForLang( config, Language.javascript, @@ -112,7 +113,7 @@ test("check flags for JS, analyzing default branch", async (t) => { test("check flags for all, not analyzing default branch", async (t) => { await util.withTmpDir(async (tmpDir) => { const config = getTestConfigWithTempDir(tmpDir); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); const result = await getTrapCachingExtractorConfigArgs(config); t.deepEqual(result, [ `-O=javascript.trap.cache.dir=${path.resolve(tmpDir, "jsCache")}`, @@ -139,7 +140,7 @@ test("get languages that support TRAP caching", async (t) => { test("upload cache key contains right fields", async (t) => { const loggedMessages = []; const logger = getRecordingLogger(loggedMessages); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); sinon.stub(util, "tryGetFolderBytes").resolves(999_999_999); const stubSave = sinon.stub(cache, "saveCache"); process.env.GITHUB_SHA = "somesha"; @@ -160,7 +161,7 @@ test("download cache looks for the right key and creates dir", async (t) => { const loggedMessages = []; const logger = getRecordingLogger(loggedMessages); sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tmpDir); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); const stubRestore = sinon.stub(cache, "restoreCache").resolves("found"); const eventFile = path.resolve(tmpDir, "event.json"); process.env.GITHUB_EVENT_NAME = "pull_request"; @@ -200,8 +201,8 @@ test("cleanup removes only old CodeQL TRAP caches", async (t) => { // This config specifies that we are analyzing JavaScript and Ruby, but not Swift. const config = getTestConfigWithTempDir(tmpDir); - sinon.stub(actionsUtil, "getRef").resolves("refs/heads/main"); - sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true); + sinon.stub(gitUtils, "getRef").resolves("refs/heads/main"); + sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); const listStub = sinon.stub(apiClient, "listActionsCaches").resolves([ // Should be kept, since it's not relevant to CodeQL. In reality, the API shouldn't return // this in the first place, but this is a defensive check. diff --git a/src/trap-caching.ts b/src/trap-caching.ts index df790e3e41..73491a13ee 100644 --- a/src/trap-caching.ts +++ b/src/trap-caching.ts @@ -9,6 +9,7 @@ import { CodeQL } from "./codeql"; import type { Config } from "./config-utils"; import { DocUrl } from "./doc-url"; import { Feature, FeatureEnablement } from "./feature-flags"; +import * as gitUtils from "./git-utils"; import { Language } from "./languages"; import { Logger } from "./logging"; import { @@ -71,7 +72,7 @@ export async function downloadTrapCaches( result[language] = cacheDir; } - if (await actionsUtil.isAnalyzingDefaultBranch()) { + if (await gitUtils.isAnalyzingDefaultBranch()) { logger.info( "Analyzing default branch. Skipping downloading of TRAP caches.", ); @@ -131,7 +132,7 @@ export async function uploadTrapCaches( config: Config, logger: Logger, ): Promise { - if (!(await actionsUtil.isAnalyzingDefaultBranch())) return false; // Only upload caches from the default branch + if (!(await gitUtils.isAnalyzingDefaultBranch())) return false; // Only upload caches from the default branch for (const language of config.languages) { const cacheDir = config.trapCaches[language]; @@ -184,7 +185,7 @@ export async function cleanupTrapCaches( trap_cache_cleanup_skipped_because: "feature disabled", }; } - if (!(await actionsUtil.isAnalyzingDefaultBranch())) { + if (!(await gitUtils.isAnalyzingDefaultBranch())) { return { trap_cache_cleanup_skipped_because: "not analyzing default branch", }; @@ -195,7 +196,7 @@ export async function cleanupTrapCaches( const allCaches = await apiClient.listActionsCaches( CODEQL_TRAP_CACHE_PREFIX, - await actionsUtil.getRef(), + await gitUtils.getRef(), ); for (const language of config.languages) { diff --git a/src/upload-lib.ts b/src/upload-lib.ts index e13ed7ebaa..3d122e4305 100644 --- a/src/upload-lib.ts +++ b/src/upload-lib.ts @@ -17,6 +17,7 @@ import { getConfig } from "./config-utils"; import { EnvVar } from "./environment"; import { FeatureEnablement } from "./feature-flags"; import * as fingerprints from "./fingerprints"; +import * as gitUtils from "./git-utils"; import { initCodeQL } from "./init"; import { Logger } from "./logging"; import { parseRepositoryNwo, RepositoryNwo } from "./repository"; @@ -599,8 +600,8 @@ export async function uploadFiles( const checkoutURI = fileUrl(checkoutPath); const payload = buildPayload( - await actionsUtil.getCommitOid(checkoutPath), - await actionsUtil.getRef(), + await gitUtils.getCommitOid(checkoutPath), + await gitUtils.getRef(), analysisKey, util.getRequiredEnvParam("GITHUB_WORKFLOW"), zippedSarif, @@ -609,7 +610,7 @@ export async function uploadFiles( checkoutURI, environment, toolNames, - await actionsUtil.determineBaseBranchHeadCommitOid(), + await gitUtils.determineBaseBranchHeadCommitOid(), ); // Log some useful debug info about the info