Move Git functions to git-utils.ts
This commit is contained in:
parent
dfed55caa4
commit
b0cd76b9fb
13 changed files with 507 additions and 488 deletions
|
|
@ -8,6 +8,7 @@ import * as sinon from "sinon";
|
||||||
import * as actionsUtil from "./actions-util";
|
import * as actionsUtil from "./actions-util";
|
||||||
import { computeAutomationID } from "./api-client";
|
import { computeAutomationID } from "./api-client";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { setupActionsVars, setupTests } from "./testing-utils";
|
import { setupActionsVars, setupTests } from "./testing-utils";
|
||||||
import { initializeEnvironment, withTmpDir } from "./util";
|
import { initializeEnvironment, withTmpDir } from "./util";
|
||||||
|
|
||||||
|
|
@ -15,7 +16,7 @@ setupTests(test);
|
||||||
|
|
||||||
test("getRef() throws on the empty string", async (t) => {
|
test("getRef() throws on the empty string", async (t) => {
|
||||||
process.env["GITHUB_REF"] = "";
|
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) => {
|
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_REF"] = expectedRef;
|
||||||
process.env["GITHUB_SHA"] = currentSha;
|
process.env["GITHUB_SHA"] = currentSha;
|
||||||
|
|
||||||
const callback = sinon.stub(actionsUtil, "getCommitOid");
|
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||||
callback.withArgs("HEAD").resolves(currentSha);
|
callback.withArgs("HEAD").resolves(currentSha);
|
||||||
|
|
||||||
const actualRef = await actionsUtil.getRef();
|
const actualRef = await gitUtils.getRef();
|
||||||
t.deepEqual(actualRef, expectedRef);
|
t.deepEqual(actualRef, expectedRef);
|
||||||
callback.restore();
|
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);
|
process.env["GITHUB_SHA"] = "b".repeat(40);
|
||||||
const sha = "a".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("refs/remotes/pull/1/merge").resolves(sha);
|
||||||
callback.withArgs("HEAD").resolves(sha);
|
callback.withArgs("HEAD").resolves(sha);
|
||||||
|
|
||||||
const actualRef = await actionsUtil.getRef();
|
const actualRef = await gitUtils.getRef();
|
||||||
t.deepEqual(actualRef, expectedRef);
|
t.deepEqual(actualRef, expectedRef);
|
||||||
callback.restore();
|
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_REF"] = "refs/pull/1/merge";
|
||||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
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, "refs/pull/1/merge").resolves("a".repeat(40));
|
||||||
callback.withArgs(tmpDir, "HEAD").resolves("b".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");
|
t.deepEqual(actualRef, "refs/pull/1/head");
|
||||||
callback.restore();
|
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_REF"] = "refs/pull/1/merge";
|
||||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
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("refs/pull/1/merge").resolves("b".repeat(40));
|
||||||
callback.withArgs("HEAD").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");
|
t.deepEqual(actualRef, "refs/pull/2/merge");
|
||||||
callback.restore();
|
callback.restore();
|
||||||
getAdditionalInputStub.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_REF"] = "";
|
||||||
process.env["GITHUB_SHA"] = currentSha;
|
process.env["GITHUB_SHA"] = currentSha;
|
||||||
|
|
||||||
const actualRef = await actionsUtil.getRef();
|
const actualRef = await gitUtils.getRef();
|
||||||
t.deepEqual(actualRef, expectedRef);
|
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_REF"] = expectedRef;
|
||||||
process.env["GITHUB_SHA"] = currentSha;
|
process.env["GITHUB_SHA"] = currentSha;
|
||||||
|
|
||||||
const actualRef = await actionsUtil.getRef();
|
const actualRef = await gitUtils.getRef();
|
||||||
t.deepEqual(actualRef, expectedRef);
|
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(
|
await t.throwsAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await actionsUtil.getRef();
|
await gitUtils.getRef();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
instanceOf: Error,
|
instanceOf: Error,
|
||||||
|
|
@ -148,7 +149,7 @@ test("getRef() throws an error if only `sha` is provided as an input", async (t)
|
||||||
|
|
||||||
await t.throwsAsync(
|
await t.throwsAsync(
|
||||||
async () => {
|
async () => {
|
||||||
await actionsUtil.getRef();
|
await gitUtils.getRef();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
instanceOf: Error,
|
instanceOf: Error,
|
||||||
|
|
@ -219,7 +220,7 @@ test("initializeEnvironment", (t) => {
|
||||||
test("isAnalyzingDefaultBranch()", async (t) => {
|
test("isAnalyzingDefaultBranch()", async (t) => {
|
||||||
process.env["GITHUB_EVENT_NAME"] = "push";
|
process.env["GITHUB_EVENT_NAME"] = "push";
|
||||||
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "true";
|
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";
|
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "false";
|
||||||
|
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
|
@ -237,13 +238,13 @@ test("isAnalyzingDefaultBranch()", async (t) => {
|
||||||
|
|
||||||
process.env["GITHUB_REF"] = "main";
|
process.env["GITHUB_REF"] = "main";
|
||||||
process.env["GITHUB_SHA"] = "1234";
|
process.env["GITHUB_SHA"] = "1234";
|
||||||
t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true);
|
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||||
|
|
||||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
process.env["GITHUB_REF"] = "refs/heads/main";
|
||||||
t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), true);
|
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
|
||||||
|
|
||||||
process.env["GITHUB_REF"] = "feature";
|
process.env["GITHUB_REF"] = "feature";
|
||||||
t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), false);
|
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
envFile,
|
envFile,
|
||||||
|
|
@ -253,7 +254,7 @@ test("isAnalyzingDefaultBranch()", async (t) => {
|
||||||
);
|
);
|
||||||
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
||||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
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");
|
const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
|
||||||
getAdditionalInputStub
|
getAdditionalInputStub
|
||||||
|
|
@ -264,7 +265,7 @@ test("isAnalyzingDefaultBranch()", async (t) => {
|
||||||
.resolves("0000000000000000000000000000000000000000");
|
.resolves("0000000000000000000000000000000000000000");
|
||||||
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
process.env["GITHUB_EVENT_NAME"] = "schedule";
|
||||||
process.env["GITHUB_REF"] = "refs/heads/main";
|
process.env["GITHUB_REF"] = "refs/heads/main";
|
||||||
t.deepEqual(await actionsUtil.isAnalyzingDefaultBranch(), false);
|
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
|
||||||
getAdditionalInputStub.restore();
|
getAdditionalInputStub.restore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -274,7 +275,7 @@ test("determineBaseBranchHeadCommitOid non-pullrequest", async (t) => {
|
||||||
|
|
||||||
process.env["GITHUB_EVENT_NAME"] = "hucairz";
|
process.env["GITHUB_EVENT_NAME"] = "hucairz";
|
||||||
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
||||||
const result = await actionsUtil.determineBaseBranchHeadCommitOid(__dirname);
|
const result = await gitUtils.determineBaseBranchHeadCommitOid(__dirname);
|
||||||
t.deepEqual(result, undefined);
|
t.deepEqual(result, undefined);
|
||||||
t.deepEqual(0, infoStub.callCount);
|
t.deepEqual(0, infoStub.callCount);
|
||||||
|
|
||||||
|
|
@ -288,7 +289,7 @@ test("determineBaseBranchHeadCommitOid not git repository", async (t) => {
|
||||||
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
||||||
|
|
||||||
await withTmpDir(async (tmpDir) => {
|
await withTmpDir(async (tmpDir) => {
|
||||||
await actionsUtil.determineBaseBranchHeadCommitOid(tmpDir);
|
await gitUtils.determineBaseBranchHeadCommitOid(tmpDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
t.deepEqual(1, infoStub.callCount);
|
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_EVENT_NAME"] = "pull_request";
|
||||||
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
process.env["GITHUB_SHA"] = "100912429fab4cb230e66ffb11e738ac5194e73a";
|
||||||
const result = await actionsUtil.determineBaseBranchHeadCommitOid(
|
const result = await gitUtils.determineBaseBranchHeadCommitOid(
|
||||||
path.join(__dirname, "../../i-dont-exist"),
|
path.join(__dirname, "../../i-dont-exist"),
|
||||||
);
|
);
|
||||||
t.deepEqual(result, undefined);
|
t.deepEqual(result, undefined);
|
||||||
|
|
@ -326,39 +327,39 @@ test("determineBaseBranchHeadCommitOid other error", async (t) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("decodeGitFilePath unquoted strings", async (t) => {
|
test("decodeGitFilePath unquoted strings", async (t) => {
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo"), "foo");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo"), "foo");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo bar"), "foo bar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo bar"), "foo bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\\\bar"), "foo\\\\bar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\\\bar"), "foo\\\\bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('foo\\"bar'), 'foo\\"bar');
|
t.deepEqual(gitUtils.decodeGitFilePath('foo\\"bar'), 'foo\\"bar');
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\001bar"), "foo\\001bar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\001bar"), "foo\\001bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\abar"), "foo\\abar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\abar"), "foo\\abar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\bbar"), "foo\\bbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\bbar"), "foo\\bbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\fbar"), "foo\\fbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\fbar"), "foo\\fbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\nbar"), "foo\\nbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\nbar"), "foo\\nbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\rbar"), "foo\\rbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\rbar"), "foo\\rbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\tbar"), "foo\\tbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\tbar"), "foo\\tbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath("foo\\vbar"), "foo\\vbar");
|
t.deepEqual(gitUtils.decodeGitFilePath("foo\\vbar"), "foo\\vbar");
|
||||||
t.deepEqual(
|
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",
|
"\\a\\b\\f\\n\\r\\t\\v",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("decodeGitFilePath quoted strings", async (t) => {
|
test("decodeGitFilePath quoted strings", async (t) => {
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo"'), "foo");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo"'), "foo");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo bar"'), "foo bar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo bar"'), "foo bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\\\bar"'), "foo\\bar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\\\bar"'), "foo\\bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\"bar"'), 'foo"bar');
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\"bar"'), 'foo"bar');
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\001bar"'), "foo\x01bar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\001bar"'), "foo\x01bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\abar"'), "foo\x07bar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\abar"'), "foo\x07bar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\bbar"'), "foo\bbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\bbar"'), "foo\bbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\fbar"'), "foo\fbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\fbar"'), "foo\fbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\nbar"'), "foo\nbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\nbar"'), "foo\nbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\rbar"'), "foo\rbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\rbar"'), "foo\rbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\tbar"'), "foo\tbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\tbar"'), "foo\tbar");
|
||||||
t.deepEqual(actionsUtil.decodeGitFilePath('"foo\\vbar"'), "foo\vbar");
|
t.deepEqual(gitUtils.decodeGitFilePath('"foo\\vbar"'), "foo\vbar");
|
||||||
t.deepEqual(
|
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",
|
"\x07\b\f\n\r\t\v",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -49,382 +49,6 @@ export function getTemporaryDirectory(): string {
|
||||||
: getRequiredEnvParam("RUNNER_TEMP");
|
: getRequiredEnvParam("RUNNER_TEMP");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runGitCommand(
|
|
||||||
checkoutPath: string | undefined,
|
|
||||||
args: string[],
|
|
||||||
customErrorMessage: string,
|
|
||||||
): Promise<string> {
|
|
||||||
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<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 {
|
|
||||||
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<string | undefined> {
|
|
||||||
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<string[]> {
|
|
||||||
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<string[] | undefined> {
|
|
||||||
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<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 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 {
|
export function getActionVersion(): string {
|
||||||
return pkg.version!;
|
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<boolean> {
|
|
||||||
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) {
|
export async function printDebugLogs(config: Config) {
|
||||||
for (const language of config.languages) {
|
for (const language of config.languages) {
|
||||||
const databaseDirectory = getCodeQLDatabasePath(config, language);
|
const databaseDirectory = getCodeQLDatabasePath(config, language);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as actionsUtil from "./actions-util";
|
||||||
import * as analyze from "./analyze";
|
import * as analyze from "./analyze";
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
import * as configUtils from "./config-utils";
|
import * as configUtils from "./config-utils";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import * as statusReport from "./status-report";
|
import * as statusReport from "./status-report";
|
||||||
import {
|
import {
|
||||||
setupTests,
|
setupTests,
|
||||||
|
|
@ -31,7 +32,7 @@ test("analyze action with RAM & threads from environment variables", async (t) =
|
||||||
.stub(statusReport, "createStatusReportBase")
|
.stub(statusReport, "createStatusReportBase")
|
||||||
.resolves({} as statusReport.StatusReportBase);
|
.resolves({} as statusReport.StatusReportBase);
|
||||||
sinon.stub(statusReport, "sendStatusReport").resolves();
|
sinon.stub(statusReport, "sendStatusReport").resolves();
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
const gitHubVersion: util.GitHubVersion = {
|
const gitHubVersion: util.GitHubVersion = {
|
||||||
type: util.GitHubVariant.DOTCOM,
|
type: util.GitHubVariant.DOTCOM,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import * as actionsUtil from "./actions-util";
|
||||||
import * as analyze from "./analyze";
|
import * as analyze from "./analyze";
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
import * as configUtils from "./config-utils";
|
import * as configUtils from "./config-utils";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import * as statusReport from "./status-report";
|
import * as statusReport from "./status-report";
|
||||||
import {
|
import {
|
||||||
setupTests,
|
setupTests,
|
||||||
|
|
@ -47,7 +48,7 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
|
||||||
optionalInputStub.withArgs("cleanup-level").returns("none");
|
optionalInputStub.withArgs("cleanup-level").returns("none");
|
||||||
optionalInputStub.withArgs("expect-error").returns("false");
|
optionalInputStub.withArgs("expect-error").returns("false");
|
||||||
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);
|
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
setupActionsVars(tmpDir, tmpDir);
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
mockFeatureFlagApiEndpoint(200, {});
|
mockFeatureFlagApiEndpoint(200, {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import * as configUtils from "./config-utils";
|
||||||
import { addDiagnostic, makeDiagnostic } from "./diagnostics";
|
import { addDiagnostic, makeDiagnostic } from "./diagnostics";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
import { FeatureEnablement, Feature } from "./feature-flags";
|
import { FeatureEnablement, Feature } from "./feature-flags";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { isScannedLanguage, Language } from "./languages";
|
import { isScannedLanguage, Language } from "./languages";
|
||||||
import { Logger, withGroupAsync } from "./logging";
|
import { Logger, withGroupAsync } from "./logging";
|
||||||
import { DatabaseCreationTimings, EventReport } from "./status-report";
|
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
|
// 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
|
// topic branch head, so that the PR merge commit is no longer considered a
|
||||||
// grafted commit.
|
// grafted commit.
|
||||||
await actionsUtil.deepenGitHistory();
|
await gitUtils.deepenGitHistory();
|
||||||
// Step 2: Fetch the base branch shallow history. This step ensures that the
|
// 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
|
// 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
|
// 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
|
// an ancestor of the PR topic branch head, Step 4 would fail without doing
|
||||||
// anything, so we need to fetch the base branch explicitly.
|
// 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
|
// Step 3: Fetch the PR topic branch history, stopping when we reach commits
|
||||||
// that are reachable from the base branch head.
|
// 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
|
// Step 4: Fetch the base branch history, stopping when we reach commits that
|
||||||
// are reachable from the PR topic branch head.
|
// 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
|
// 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
|
// the previous fetches. This step works around a bug that causes subsequent
|
||||||
// deepening fetches to fail with "fatal: error in object: unshallow <SHA>".
|
// deepening fetches to fail with "fatal: error in object: unshallow <SHA>".
|
||||||
// See https://stackoverflow.com/q/63878612
|
// 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
|
// Step 6: Deepen the history so that we have the merge bases between the base
|
||||||
// branch and the PR topic branch.
|
// 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 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
|
// 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
|
// one merge base, which is by far the most common case. If there are multiple
|
||||||
// merge bases, we stop without producing a diff range.
|
// 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(", ")}`);
|
logger.info(`Merge bases: ${mergeBases.join(", ")}`);
|
||||||
if (mergeBases.length !== 1) {
|
if (mergeBases.length !== 1) {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -340,7 +341,7 @@ async function getPullRequestEditedDiffRanges(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const diffHunkHeaders = await actionsUtil.getGitDiffHunkHeaders(
|
const diffHunkHeaders = await gitUtils.getGitDiffHunkHeaders(
|
||||||
mergeBases[0],
|
mergeBases[0],
|
||||||
headRef,
|
headRef,
|
||||||
);
|
);
|
||||||
|
|
@ -353,7 +354,7 @@ async function getPullRequestEditedDiffRanges(
|
||||||
let changedFile = "";
|
let changedFile = "";
|
||||||
for (const line of diffHunkHeaders) {
|
for (const line of diffHunkHeaders) {
|
||||||
if (line.startsWith("+++ ")) {
|
if (line.startsWith("+++ ")) {
|
||||||
const filePath = actionsUtil.decodeGitFilePath(line.substring(4));
|
const filePath = gitUtils.decodeGitFilePath(line.substring(4));
|
||||||
if (filePath.startsWith("b/")) {
|
if (filePath.startsWith("b/")) {
|
||||||
// The file was edited: track all hunks in the file
|
// The file was edited: track all hunks in the file
|
||||||
changedFile = filePath.substring(2);
|
changedFile = filePath.substring(2);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
CommandInvocationError,
|
CommandInvocationError,
|
||||||
getActionVersion,
|
getActionVersion,
|
||||||
getOptionalInput,
|
getOptionalInput,
|
||||||
isAnalyzingDefaultBranch,
|
|
||||||
runTool,
|
runTool,
|
||||||
} from "./actions-util";
|
} from "./actions-util";
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
|
|
@ -24,6 +23,7 @@ import {
|
||||||
Feature,
|
Feature,
|
||||||
FeatureEnablement,
|
FeatureEnablement,
|
||||||
} from "./feature-flags";
|
} from "./feature-flags";
|
||||||
|
import { isAnalyzingDefaultBranch } from "./git-utils";
|
||||||
import { Language } from "./languages";
|
import { Language } from "./languages";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import * as setupCodeql from "./setup-codeql";
|
import * as setupCodeql from "./setup-codeql";
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import * as apiClient from "./api-client";
|
||||||
import { setCodeQL } from "./codeql";
|
import { setCodeQL } from "./codeql";
|
||||||
import { Config } from "./config-utils";
|
import { Config } from "./config-utils";
|
||||||
import { uploadDatabases } from "./database-upload";
|
import { uploadDatabases } from "./database-upload";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { Language } from "./languages";
|
import { Language } from "./languages";
|
||||||
import { RepositoryNwo } from "./repository";
|
import { RepositoryNwo } from "./repository";
|
||||||
import {
|
import {
|
||||||
|
|
@ -75,7 +76,7 @@ test("Abort database upload if 'upload-database' input set to false", async (t)
|
||||||
.stub(actionsUtil, "getRequiredInput")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("false");
|
.returns("false");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
await uploadDatabases(
|
await uploadDatabases(
|
||||||
|
|
@ -102,7 +103,7 @@ test("Abort database upload if running against GHES", async (t) => {
|
||||||
.stub(actionsUtil, "getRequiredInput")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("true");
|
.returns("true");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
const config = getTestConfig(tmpDir);
|
const config = getTestConfig(tmpDir);
|
||||||
config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" };
|
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")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("true");
|
.returns("true");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
|
||||||
|
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
await uploadDatabases(
|
await uploadDatabases(
|
||||||
|
|
@ -158,7 +159,7 @@ test("Don't crash if uploading a database fails", async (t) => {
|
||||||
.stub(actionsUtil, "getRequiredInput")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("true");
|
.returns("true");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
await mockHttpRequests(500);
|
await mockHttpRequests(500);
|
||||||
|
|
||||||
|
|
@ -194,7 +195,7 @@ test("Successfully uploading a database to github.com", async (t) => {
|
||||||
.stub(actionsUtil, "getRequiredInput")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("true");
|
.returns("true");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
await mockHttpRequests(201);
|
await mockHttpRequests(201);
|
||||||
|
|
||||||
|
|
@ -228,7 +229,7 @@ test("Successfully uploading a database to GHEC-DR", async (t) => {
|
||||||
.stub(actionsUtil, "getRequiredInput")
|
.stub(actionsUtil, "getRequiredInput")
|
||||||
.withArgs("upload-database")
|
.withArgs("upload-database")
|
||||||
.returns("true");
|
.returns("true");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
|
|
||||||
const databaseUploadSpy = await mockHttpRequests(201);
|
const databaseUploadSpy = await mockHttpRequests(201);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as actionsUtil from "./actions-util";
|
||||||
import { getApiClient, GitHubApiDetails } from "./api-client";
|
import { getApiClient, GitHubApiDetails } from "./api-client";
|
||||||
import { getCodeQL } from "./codeql";
|
import { getCodeQL } from "./codeql";
|
||||||
import { Config } from "./config-utils";
|
import { Config } from "./config-utils";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import { RepositoryNwo } from "./repository";
|
import { RepositoryNwo } from "./repository";
|
||||||
import * as util from "./util";
|
import * as util from "./util";
|
||||||
|
|
@ -34,7 +35,7 @@ export async function uploadDatabases(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await actionsUtil.isAnalyzingDefaultBranch())) {
|
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
|
||||||
// We only want to upload a database if we are analyzing the default branch.
|
// We only want to upload a database if we are analyzing the default branch.
|
||||||
logger.debug("Not analyzing default branch. Skipping upload.");
|
logger.debug("Not analyzing default branch. Skipping upload.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -62,7 +63,7 @@ export async function uploadDatabases(
|
||||||
const bundledDb = await bundleDb(config, language, codeql, language);
|
const bundledDb = await bundleDb(config, language, codeql, language);
|
||||||
const bundledDbSize = fs.statSync(bundledDb).size;
|
const bundledDbSize = fs.statSync(bundledDb).size;
|
||||||
const bundledDbReadStream = fs.createReadStream(bundledDb);
|
const bundledDbReadStream = fs.createReadStream(bundledDb);
|
||||||
const commitOid = await actionsUtil.getCommitOid(
|
const commitOid = await gitUtils.getCommitOid(
|
||||||
actionsUtil.getRequiredInput("checkout_path"),
|
actionsUtil.getRequiredInput("checkout_path"),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
416
src/git-utils.ts
Normal file
416
src/git-utils.ts
Normal file
|
|
@ -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<string> {
|
||||||
|
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<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 {
|
||||||
|
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<string | undefined> {
|
||||||
|
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<string[]> {
|
||||||
|
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<string[] | undefined> {
|
||||||
|
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<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 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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import * as core from "@actions/core";
|
||||||
import {
|
import {
|
||||||
getWorkflowEventName,
|
getWorkflowEventName,
|
||||||
getOptionalInput,
|
getOptionalInput,
|
||||||
getRef,
|
|
||||||
getWorkflowRunID,
|
getWorkflowRunID,
|
||||||
getWorkflowRunAttempt,
|
getWorkflowRunAttempt,
|
||||||
getActionVersion,
|
getActionVersion,
|
||||||
|
|
@ -16,6 +15,7 @@ import { getAnalysisKey, getApiClient } from "./api-client";
|
||||||
import { type Config } from "./config-utils";
|
import { type Config } from "./config-utils";
|
||||||
import { DocUrl } from "./doc-url";
|
import { DocUrl } from "./doc-url";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
|
import { getRef } from "./git-utils";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import {
|
import {
|
||||||
ConfigurationError,
|
ConfigurationError,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from "./codeql";
|
} from "./codeql";
|
||||||
import * as configUtils from "./config-utils";
|
import * as configUtils from "./config-utils";
|
||||||
import { Feature } from "./feature-flags";
|
import { Feature } from "./feature-flags";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { Language } from "./languages";
|
import { Language } from "./languages";
|
||||||
import { getRunnerLogger } from "./logging";
|
import { getRunnerLogger } from "./logging";
|
||||||
import {
|
import {
|
||||||
|
|
@ -96,7 +97,7 @@ function getTestConfigWithTempDir(tempDir: string): configUtils.Config {
|
||||||
test("check flags for JS, analyzing default branch", async (t) => {
|
test("check flags for JS, analyzing default branch", async (t) => {
|
||||||
await util.withTmpDir(async (tmpDir) => {
|
await util.withTmpDir(async (tmpDir) => {
|
||||||
const config = getTestConfigWithTempDir(tmpDir);
|
const config = getTestConfigWithTempDir(tmpDir);
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
const result = await getTrapCachingExtractorConfigArgsForLang(
|
const result = await getTrapCachingExtractorConfigArgsForLang(
|
||||||
config,
|
config,
|
||||||
Language.javascript,
|
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) => {
|
test("check flags for all, not analyzing default branch", async (t) => {
|
||||||
await util.withTmpDir(async (tmpDir) => {
|
await util.withTmpDir(async (tmpDir) => {
|
||||||
const config = getTestConfigWithTempDir(tmpDir);
|
const config = getTestConfigWithTempDir(tmpDir);
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(false);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
|
||||||
const result = await getTrapCachingExtractorConfigArgs(config);
|
const result = await getTrapCachingExtractorConfigArgs(config);
|
||||||
t.deepEqual(result, [
|
t.deepEqual(result, [
|
||||||
`-O=javascript.trap.cache.dir=${path.resolve(tmpDir, "jsCache")}`,
|
`-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) => {
|
test("upload cache key contains right fields", async (t) => {
|
||||||
const loggedMessages = [];
|
const loggedMessages = [];
|
||||||
const logger = getRecordingLogger(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);
|
sinon.stub(util, "tryGetFolderBytes").resolves(999_999_999);
|
||||||
const stubSave = sinon.stub(cache, "saveCache");
|
const stubSave = sinon.stub(cache, "saveCache");
|
||||||
process.env.GITHUB_SHA = "somesha";
|
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 loggedMessages = [];
|
||||||
const logger = getRecordingLogger(loggedMessages);
|
const logger = getRecordingLogger(loggedMessages);
|
||||||
sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tmpDir);
|
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 stubRestore = sinon.stub(cache, "restoreCache").resolves("found");
|
||||||
const eventFile = path.resolve(tmpDir, "event.json");
|
const eventFile = path.resolve(tmpDir, "event.json");
|
||||||
process.env.GITHUB_EVENT_NAME = "pull_request";
|
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.
|
// This config specifies that we are analyzing JavaScript and Ruby, but not Swift.
|
||||||
const config = getTestConfigWithTempDir(tmpDir);
|
const config = getTestConfigWithTempDir(tmpDir);
|
||||||
|
|
||||||
sinon.stub(actionsUtil, "getRef").resolves("refs/heads/main");
|
sinon.stub(gitUtils, "getRef").resolves("refs/heads/main");
|
||||||
sinon.stub(actionsUtil, "isAnalyzingDefaultBranch").resolves(true);
|
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||||
const listStub = sinon.stub(apiClient, "listActionsCaches").resolves([
|
const listStub = sinon.stub(apiClient, "listActionsCaches").resolves([
|
||||||
// Should be kept, since it's not relevant to CodeQL. In reality, the API shouldn't return
|
// 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.
|
// this in the first place, but this is a defensive check.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { CodeQL } from "./codeql";
|
||||||
import type { Config } from "./config-utils";
|
import type { Config } from "./config-utils";
|
||||||
import { DocUrl } from "./doc-url";
|
import { DocUrl } from "./doc-url";
|
||||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { Language } from "./languages";
|
import { Language } from "./languages";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import {
|
import {
|
||||||
|
|
@ -71,7 +72,7 @@ export async function downloadTrapCaches(
|
||||||
result[language] = cacheDir;
|
result[language] = cacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await actionsUtil.isAnalyzingDefaultBranch()) {
|
if (await gitUtils.isAnalyzingDefaultBranch()) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"Analyzing default branch. Skipping downloading of TRAP caches.",
|
"Analyzing default branch. Skipping downloading of TRAP caches.",
|
||||||
);
|
);
|
||||||
|
|
@ -131,7 +132,7 @@ export async function uploadTrapCaches(
|
||||||
config: Config,
|
config: Config,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
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) {
|
for (const language of config.languages) {
|
||||||
const cacheDir = config.trapCaches[language];
|
const cacheDir = config.trapCaches[language];
|
||||||
|
|
@ -184,7 +185,7 @@ export async function cleanupTrapCaches(
|
||||||
trap_cache_cleanup_skipped_because: "feature disabled",
|
trap_cache_cleanup_skipped_because: "feature disabled",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!(await actionsUtil.isAnalyzingDefaultBranch())) {
|
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
|
||||||
return {
|
return {
|
||||||
trap_cache_cleanup_skipped_because: "not analyzing default branch",
|
trap_cache_cleanup_skipped_because: "not analyzing default branch",
|
||||||
};
|
};
|
||||||
|
|
@ -195,7 +196,7 @@ export async function cleanupTrapCaches(
|
||||||
|
|
||||||
const allCaches = await apiClient.listActionsCaches(
|
const allCaches = await apiClient.listActionsCaches(
|
||||||
CODEQL_TRAP_CACHE_PREFIX,
|
CODEQL_TRAP_CACHE_PREFIX,
|
||||||
await actionsUtil.getRef(),
|
await gitUtils.getRef(),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const language of config.languages) {
|
for (const language of config.languages) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { getConfig } from "./config-utils";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
import { FeatureEnablement } from "./feature-flags";
|
import { FeatureEnablement } from "./feature-flags";
|
||||||
import * as fingerprints from "./fingerprints";
|
import * as fingerprints from "./fingerprints";
|
||||||
|
import * as gitUtils from "./git-utils";
|
||||||
import { initCodeQL } from "./init";
|
import { initCodeQL } from "./init";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import { parseRepositoryNwo, RepositoryNwo } from "./repository";
|
import { parseRepositoryNwo, RepositoryNwo } from "./repository";
|
||||||
|
|
@ -599,8 +600,8 @@ export async function uploadFiles(
|
||||||
const checkoutURI = fileUrl(checkoutPath);
|
const checkoutURI = fileUrl(checkoutPath);
|
||||||
|
|
||||||
const payload = buildPayload(
|
const payload = buildPayload(
|
||||||
await actionsUtil.getCommitOid(checkoutPath),
|
await gitUtils.getCommitOid(checkoutPath),
|
||||||
await actionsUtil.getRef(),
|
await gitUtils.getRef(),
|
||||||
analysisKey,
|
analysisKey,
|
||||||
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
|
||||||
zippedSarif,
|
zippedSarif,
|
||||||
|
|
@ -609,7 +610,7 @@ export async function uploadFiles(
|
||||||
checkoutURI,
|
checkoutURI,
|
||||||
environment,
|
environment,
|
||||||
toolNames,
|
toolNames,
|
||||||
await actionsUtil.determineBaseBranchHeadCommitOid(),
|
await gitUtils.determineBaseBranchHeadCommitOid(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log some useful debug info about the info
|
// Log some useful debug info about the info
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue