Compare commits

...

10 Commits

Author SHA1 Message Date
Michael B. Gale
c2215db9ae Update git-utils tests 2026-09-28 10:51:26 +01:00
Michael B. Gale
5769ffc476 Merge remote-tracking branch 'origin/main' into mbg/improve-checkout-path 2026-09-28 10:49:13 +01:00
Michael B. Gale
72509a81ba Consistently use resolved path 2026-09-23 12:57:16 +01:00
Michael B. Gale
a876e23075 Add tests for determineCheckoutPath 2026-09-22 16:25:47 +01:00
Michael B. Gale
6fb4a88ed6 Normalise the results of getGitRoot 2026-09-22 16:25:47 +01:00
Michael B. Gale
f5716fadf4 Allow getCheckoutPathInputOrThrow to get root from config 2026-09-22 16:25:46 +01:00
Michael B. Gale
54a43494ca Propagate checkoutPath to getRef 2026-09-22 16:25:46 +01:00
Michael B. Gale
ba615b5d9d Verify persisted repository root path in analyze action 2026-09-22 16:25:46 +01:00
Michael B. Gale
5dab0ccf51 Persist repository root from init action in CodeQL Action state 2026-09-22 16:25:46 +01:00
Michael B. Gale
99fe83948e Refactor: Add determineCheckoutPath function for analyze action 2026-09-22 16:25:46 +01:00
24 changed files with 1139 additions and 687 deletions

1173
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ export function getActionVersion(): string {
*
* This will be "dynamic" for default setup workflow runs.
*/
export function getWorkflowEventName(env: Env = getEnv()) {
export function getWorkflowEventName(env: ReadOnlyEnv = getEnv()) {
return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME);
}
@@ -122,7 +122,7 @@ function getRelativeScriptPath(env: Env): string {
}
/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
export function getWorkflowEvent(env: Env = getEnv()): any {
export function getWorkflowEvent(env: ReadOnlyEnv = getEnv()): any {
const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH);
try {
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));

View File

@@ -45,6 +45,7 @@ test.serial(
requiredInputStub.withArgs("token").returns("fake-token");
requiredInputStub.withArgs("upload-database").returns("false");
requiredInputStub.withArgs("output").returns("out");
requiredInputStub.withArgs("checkout_path").returns("");
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
optionalInputStub.withArgs("expect-error").returns("false");
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);
@@ -104,6 +105,7 @@ test.serial(
requiredInputStub.withArgs("token").returns("fake-token");
requiredInputStub.withArgs("upload-database").returns("false");
requiredInputStub.withArgs("output").returns("out");
requiredInputStub.withArgs("checkout_path").returns("");
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
optionalInputStub.withArgs("expect-error").returns("false");
sinon.stub(api, "getGitHubVersion").resolves(gitHubVersion);

View File

@@ -10,6 +10,7 @@ import * as analyses from "./analyses";
import {
CodeQLAnalysisError,
dbIsFinalized,
determineCheckoutPath,
QueriesStatusReport,
runFinalize,
runQueries,
@@ -212,13 +213,11 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
await runAutobuild(config, BuiltInLanguage.go, logger);
}
async function run({
startedAt,
logger,
actions,
}: ActionState<["Base", "Logger", "Actions"]>) {
async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const startedAt = action.startedAt;
const logger = action.logger;
let uploadResults:
| Partial<Record<analyses.AnalysisKind, UploadResult>>
@@ -311,7 +310,7 @@ async function run({
logger,
);
const checkoutPath = actions.getRequiredInput("checkout_path");
const checkoutPath = await determineCheckoutPath(action, config);
// Setup diff informed analysis if needed (based on whether init created the file)
const diffRangePackDir = await setupDiffInformedQueryRun(
@@ -407,7 +406,7 @@ async function run({
// Note: Take care with the ordering of this call since databases may be cleaned up
// at the `overlay` or `clear` level.
databaseUploadResults = await cleanupAndUploadDatabases(
{ logger, features },
{ ...action, features },
repositoryNwo,
codeql,
config,

View File

@@ -11,9 +11,11 @@ import {
resolveQuerySuiteAlias,
addSarifExtension,
diffRangeExtensionPackContents,
determineCheckoutPath,
} from "./analyze";
import { createStubCodeQL } from "./codeql";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import {
@@ -21,12 +23,98 @@ import {
setupActionsVars,
createFeatures,
createTestConfig,
callee,
} from "./testing-utils";
import * as uploadLib from "./upload-lib";
import * as util from "./util";
setupTests(test);
test.serial(
"determineCheckoutPath - logs when checkout_path is not in a work tree",
async (t) => {
const expectedPath = path.resolve("/checkout/path");
const target = callee(determineCheckoutPath)
.withActions((actions) => {
sinon
.stub(actions, "getRequiredInput")
.withArgs("checkout_path")
.returns(expectedPath);
})
.withArgs(createTestConfig({}));
sinon.stub(gitUtils, "getGitRoot").resolves(undefined);
await target
.logs(t, "is not in the work tree of a git repository")
.passes(t.is, expectedPath);
},
);
test.serial(
"determineCheckoutPath - logs when checkout_path is not a repo root",
async (t) => {
const expectedPath = path.resolve("/checkout/path");
const target = callee(determineCheckoutPath)
.withActions((actions) => {
sinon
.stub(actions, "getRequiredInput")
.withArgs("checkout_path")
.returns(expectedPath);
})
.withArgs(createTestConfig({}));
sinon.stub(gitUtils, "getGitRoot").resolves("/checkout");
await target
.logs(t, "is not the root of the repository")
.passes(t.is, expectedPath);
},
);
test.serial(
"determineCheckoutPath - logs when checkout_path is not the same as repo root in config",
async (t) => {
const expectedPath = path.resolve("/checkout/path");
const target = callee(determineCheckoutPath)
.withActions((actions) => {
sinon
.stub(actions, "getRequiredInput")
.withArgs("checkout_path")
.returns(expectedPath);
})
.withArgs(createTestConfig({ repositoryRoot: "/some/other/path" }));
sinon.stub(gitUtils, "getGitRoot").resolves(expectedPath);
await target
.logs(t, "does not match that found by the 'codeql-action/init' step")
.passes(t.is, expectedPath);
},
);
test.serial(
"determineCheckoutPath - doesn't log any of the messages when all is as expected",
async (t) => {
const expectedPath = path.resolve("/checkout/path");
const target = callee(determineCheckoutPath)
.withActions((actions) => {
sinon
.stub(actions, "getRequiredInput")
.withArgs("checkout_path")
.returns(expectedPath);
})
.withArgs(createTestConfig({ repositoryRoot: expectedPath }));
sinon.stub(gitUtils, "getGitRoot").resolves(expectedPath);
await target
.notLogs(
t,
"is not in the work tree of a git repository",
"is not the root of the repository",
"does not match that found by the 'codeql-action/init' step",
)
.passes(t.is, expectedPath);
},
);
/**
* Checks the status report produced by the analyze Action.
*

View File

@@ -5,6 +5,7 @@ import { performance } from "perf_hooks";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import type { ActionState } from "./action-common";
import { getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { setupCppAutobuild } from "./autobuild";
@@ -21,6 +22,7 @@ import {
} from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { FeatureEnablement, Feature } from "./feature-flags";
import { getGitRoot } from "./git-utils";
import { BuiltInLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
@@ -85,6 +87,58 @@ export interface QueriesStatusReport
event_reports?: EventReport[];
}
/**
* Determines the path at which the repository being analysed is checked out at.
* Returns the value of the required `checkout_path` input and validates that it
* refers to the root of a repository.
*
* @param action The action state.
* @param config The CodeQL Action configuration state.
*/
export async function determineCheckoutPath(
action: ActionState<["Logger", "Actions"]>,
config: configUtils.Config,
) {
const checkoutPathInput = action.actions.getRequiredInput("checkout_path");
// Try to obtain the root path of the repository and validate that it matches the input.
const absCheckoutPathInput = path.resolve(checkoutPathInput);
const repositoryRoot = await getGitRoot(absCheckoutPathInput);
if (repositoryRoot === undefined) {
action.logger.warning(
[
`The directory at '${absCheckoutPathInput}' is not in the work tree of a git repository.`,
"If the repository being analyzed is checked out elsewhere,",
"you must explicitly set the 'checkout_path' input for the 'codeql-action/analyze' step to",
"the checkout path.",
].join(" "),
);
} else if (repositoryRoot !== absCheckoutPathInput) {
action.logger.warning(
[
`The directory at '${absCheckoutPathInput}' is not the root of the repository ('${repositoryRoot}').`,
"Set the 'checkout_path' input for the 'codeql-action/analyze' step to the root path of the checkout.",
].join(" "),
);
} else if (
config.repositoryRoot !== undefined &&
repositoryRoot !== config.repositoryRoot
) {
// The repository root that was persisted by the `init` step doesn't match the one we have found here.
action.logger.warning(
[
`The repository path at '${repositoryRoot}' does not match that found by the 'codeql-action/init' step: '${config.repositoryRoot}'.`,
"Ensure that the 'checkout_path' input for the 'codeql-action/analyze' step is set to the path of the same repository that",
"the 'codeql-action/init' step determined. This is either the GitHub Actions workspace or the repository root corresponding to",
"the 'source-root' input if that was provided.",
].join(" "),
);
}
return absCheckoutPathInput;
}
async function setupPythonExtractor(logger: Logger) {
const codeqlPython = process.env["CODEQL_PYTHON"];
if (codeqlPython === undefined || codeqlPython.length === 0) {

View File

@@ -1219,7 +1219,7 @@ export async function getTrapCachingExtractorConfigArgsForLang(
): Promise<string[]> {
const cacheDir = config.trapCaches[language];
if (cacheDir === undefined) return [];
const write = await isAnalyzingDefaultBranch();
const write = await isAnalyzingDefaultBranch(getEnv(), config.repositoryRoot);
return [
`-O=${language}.trap.cache.dir=${cacheDir}`,
`-O=${language}.trap.cache.bound=${TRAP_CACHE_SIZE_MB}`,

View File

@@ -172,6 +172,7 @@ test.serial("load empty config", async (t) => {
createTestInitConfigInputs({
languagesInput: languages,
repository: { owner: "github", repo: "example" },
sourceRoot: tempDir,
tempDir,
codeql,
logger,
@@ -186,6 +187,7 @@ test.serial("load empty config", async (t) => {
logger,
}),
{},
undefined,
);
t.deepEqual(config, expectedConfig);
@@ -216,6 +218,7 @@ test.serial("load code quality config", async (t) => {
analysisKinds: [AnalysisKind.CodeQuality],
languagesInput: languages,
repository: { owner: "github", repo: "example" },
sourceRoot: tempDir,
tempDir,
codeql,
logger,
@@ -296,6 +299,7 @@ test.serial(
analysisKinds: [AnalysisKind.CodeQuality],
languagesInput: languages,
repository: { owner: "github", repo: "example" },
sourceRoot: tempDir,
tempDir,
codeql,
repositoryProperties,
@@ -512,6 +516,7 @@ test.serial("load non-empty input", async (t) => {
// And the config we expect it to parse to
const expectedConfig = createTestConfig({
languages: [BuiltInLanguage.javascript],
repositoryRoot: undefined,
buildMode: BuildMode.None,
originalUserInput: userConfig,
computedConfig: userConfig,
@@ -532,6 +537,7 @@ test.serial("load non-empty input", async (t) => {
state,
createTestInitConfigInputs({
languagesInput,
sourceRoot: tempDir,
buildModeInput: "none",
configFile: configFilePath,
debugArtifactName: "my-artifact",
@@ -1092,11 +1098,6 @@ const checkOverlayEnablementMacro = makeMacro({
return lang === BuiltInLanguage.java;
});
// Mock git root detection
if (setup.gitRoot !== undefined) {
sinon.stub(gitUtils, "getGitRoot").resolves(setup.gitRoot);
}
// Mock submodule detection
sinon.stub(gitUtils, "hasSubmodules").returns(setup.hasSubmodules);
@@ -1109,6 +1110,7 @@ const checkOverlayEnablementMacro = makeMacro({
codeql,
features,
setup.languages,
setup.gitRoot, // repositoryRoot
tempDir, // sourceRoot
setup.buildMode,
undefined,

View File

@@ -46,7 +46,7 @@ import {
makeTelemetryDiagnostic,
} from "./diagnostics";
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { EnvVar, getEnv } from "./environment";
import * as errorMessages from "./error-messages";
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
import {
@@ -385,6 +385,7 @@ export async function initActionState(
enableFileCoverageInformation,
}: InitConfigInputs,
userConfig: UserConfig,
repositoryRoot: string | undefined,
): Promise<Config> {
const languages = await getLanguages(
codeql,
@@ -436,6 +437,7 @@ export async function initActionState(
return {
version: getActionVersion(),
repositoryRoot,
analysisKinds,
languages,
buildMode,
@@ -465,12 +467,18 @@ async function downloadCacheWithTime(
codeQL: CodeQL,
languages: Language[],
logger: Logger,
repositoryRoot: string | undefined,
): Promise<{
trapCaches: { [language: string]: string };
trapCacheDownloadTime: number;
}> {
const start = performance.now();
const trapCaches = await downloadTrapCaches(codeQL, languages, logger);
const trapCaches = await downloadTrapCaches(
codeQL,
languages,
logger,
repositoryRoot,
);
const trapCacheDownloadTime = performance.now() - start;
return { trapCaches, trapCacheDownloadTime };
}
@@ -718,6 +726,7 @@ export async function checkOverlayEnablement(
codeql: CodeQL,
features: FeatureEnablement,
languages: Language[],
repositoryRoot: string | undefined,
sourceRoot: string,
buildMode: BuildMode | undefined,
ramInput: string | undefined,
@@ -747,6 +756,7 @@ export async function checkOverlayEnablement(
true,
codeql,
languages,
repositoryRoot,
sourceRoot,
buildMode,
gitVersion,
@@ -820,7 +830,7 @@ export async function checkOverlayEnablement(
`Setting overlay database mode to ${overlayDatabaseMode} ` +
"with caching because we are analyzing a pull request.",
);
} else if (await isAnalyzingDefaultBranch()) {
} else if (await isAnalyzingDefaultBranch(getEnv(), repositoryRoot)) {
overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
logger.info(
`Setting overlay database mode to ${overlayDatabaseMode} ` +
@@ -836,6 +846,7 @@ export async function checkOverlayEnablement(
false,
codeql,
languages,
repositoryRoot,
sourceRoot,
buildMode,
gitVersion,
@@ -855,6 +866,7 @@ async function validateOverlayDatabaseMode(
overlayModeSetExplicitly: boolean,
codeql: CodeQL,
languages: Language[],
repositoryRoot: string | undefined,
sourceRoot: string,
buildMode: BuildMode | undefined,
gitVersion: GitVersionInfo | undefined,
@@ -890,8 +902,7 @@ async function validateOverlayDatabaseMode(
);
return new Failure(OverlayDisabledReason.IncompatibleCodeQl);
}
const gitRoot = await getGitRoot(sourceRoot);
if (gitRoot === undefined) {
if (repositoryRoot === undefined) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the source root "${sourceRoot}" is not inside a git repository. ` +
@@ -899,7 +910,7 @@ async function validateOverlayDatabaseMode(
);
return new Failure(OverlayDisabledReason.NoGitRoot);
}
if (hasSubmodules(gitRoot)) {
if (hasSubmodules(repositoryRoot)) {
if (gitVersion === undefined) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
@@ -1160,9 +1171,11 @@ export async function initConfig(
const { logger, features } = actionState;
const { tempDir } = inputs;
const repositoryRoot = await getGitRoot(inputs.sourceRoot);
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
const config = await initActionState(inputs, userConfig);
const config = await initActionState(inputs, userConfig, repositoryRoot);
// If Code Quality analysis is the only enabled analysis kind, then we will initialise
// the database for Code Quality. That entails disabling the default queries and only
@@ -1244,6 +1257,7 @@ export async function initConfig(
inputs.codeql,
inputs.features,
config.languages,
repositoryRoot,
inputs.sourceRoot,
config.buildMode,
inputs.ramInput,
@@ -1297,6 +1311,7 @@ export async function initConfig(
inputs.codeql,
config.languages,
logger,
repositoryRoot,
);
config.trapCaches = trapCaches;
config.trapCacheDownloadTime = trapCacheDownloadTime;

View File

@@ -16,6 +16,11 @@ export interface Config {
* The version of the CodeQL Action that the configuration is for.
*/
version: string;
/**
* The path at which the repository being analysed is checked out at, if available.
* Persisted in the CodeQL Action configuration state, so that we can consult it in later workflow steps.
*/
repositoryRoot: string | undefined;
/**
* Set of analysis kinds that are enabled.
*/

View File

@@ -46,7 +46,7 @@ export interface DatabaseUploadResult {
}
export async function cleanupAndUploadDatabases(
action: ActionState<["Logger", "FeatureFlags"]>,
action: ActionState<["ReadOnlyEnv", "Logger", "FeatureFlags"]>,
repositoryNwo: RepositoryNwo,
codeql: CodeQL,
config: Config,
@@ -81,7 +81,7 @@ export async function cleanupAndUploadDatabases(
return [];
}
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
if (!(await gitUtils.isAnalyzingDefaultBranch(action.env, checkoutPath))) {
// We only want to upload a database if we are analyzing the default branch.
logger.debug("Not analyzing default branch. Skipping upload.");
return [];
@@ -113,7 +113,7 @@ export async function cleanupAndUploadDatabases(
includeDiagnostics: false,
});
bundledDbSize = fs.statSync(bundledDb).size;
const commitOid = await gitUtils.getCommitOid(checkoutPath);
const commitOid = await gitUtils.getCommitOid(action.env, checkoutPath);
// Upload with manual retry logic. We disable Octokit's built-in retries
// because the request body is a ReadStream, which can only be consumed
// once.

View File

@@ -307,6 +307,13 @@ export class Env<
this.changed = true;
}
/** Sets all environment variables given by `vars`. */
public setAll(vars: Record<string, T>): void {
for (const [key, val] of Object.entries(vars)) {
this.set(key, val);
}
}
/** Gets a value indicating whether `set` was called at least once. */
public hasChanged(): boolean {
return this.changed;

View File

@@ -7,35 +7,42 @@ import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { ActionsEnvVars, EnvVar } from "./environment";
import * as gitUtils from "./git-utils";
import { setupActionsVars, setupTests } from "./testing-utils";
import { getTestEnv, setupActionsVars, setupTests } from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
test.serial("getRef() throws on the empty string", async (t) => {
process.env["GITHUB_REF"] = "";
await t.throwsAsync(gitUtils.getRef);
test("getRef() throws on the empty string", async (t) => {
const env = getTestEnv({ [ActionsEnvVars.GITHUB_REF]: "" });
await t.throwsAsync(() => gitUtils.getRef(env, ""));
});
test.serial(
"getRef() returns merge PR ref if GITHUB_SHA still checked out",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const expectedRef = "refs/pull/1/merge";
const currentSha = "a".repeat(40);
process.env["GITHUB_REF"] = expectedRef;
process.env["GITHUB_SHA"] = currentSha;
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[ActionsEnvVars.GITHUB_REF]: expectedRef,
[ActionsEnvVars.GITHUB_SHA]: currentSha,
});
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs(sinon.match.string, "HEAD").resolves(currentSha);
callback
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
.resolves(currentSha);
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, expectedRef);
t.is(callback.callCount, 1);
t.true(callback.calledOnceWith(tmpDir, "HEAD"));
t.true(callback.calledOnceWith(env, tmpDir, "HEAD"));
});
},
);
@@ -44,24 +51,33 @@ test.serial(
"getRef() returns merge PR ref if GITHUB_REF still checked out but sha has changed (actions checkout@v1)",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const expectedRef = "refs/pull/1/merge";
process.env["GITHUB_REF"] = expectedRef;
process.env["GITHUB_SHA"] = "b".repeat(40);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[ActionsEnvVars.GITHUB_REF]: expectedRef,
[ActionsEnvVars.GITHUB_SHA]: "b".repeat(40),
});
const sha = "a".repeat(40);
const callback = sinon.stub(gitUtils, "getCommitOid");
callback
.withArgs(sinon.match.string, "refs/remotes/pull/1/merge")
.withArgs(
sinon.match.any,
sinon.match.string,
"refs/remotes/pull/1/merge",
)
.resolves(sha);
callback
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
.resolves(sha);
callback.withArgs(sinon.match.any, "HEAD").resolves(sha);
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, expectedRef);
t.is(callback.callCount, 2);
t.true(callback.calledWith(tmpDir, "HEAD"));
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
t.true(callback.calledWith(env, tmpDir, "HEAD"));
t.true(callback.calledWith(env, tmpDir, "refs/remotes/pull/1/merge"));
});
},
);
@@ -70,23 +86,28 @@ test.serial(
"getRef() returns head PR ref if GITHUB_REF no longer checked out",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
process.env["GITHUB_REF"] = "refs/pull/1/merge";
process.env["GITHUB_SHA"] = "a".repeat(40);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[ActionsEnvVars.GITHUB_REF]: "refs/pull/1/merge",
[ActionsEnvVars.GITHUB_SHA]: "a".repeat(40),
});
const callback = sinon.stub(gitUtils, "getCommitOid");
callback
.withArgs(tmpDir, "refs/remotes/pull/1/merge")
.withArgs(sinon.match.any, tmpDir, "refs/remotes/pull/1/merge")
.resolves("a".repeat(40));
callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40));
callback
.withArgs(sinon.match.any, tmpDir, "HEAD")
.resolves("b".repeat(40));
callback.throws(new Error("Unexpected getCommitOid call in test."));
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, "refs/pull/1/head");
t.is(callback.callCount, 2);
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
t.true(callback.calledWith(tmpDir, "HEAD"));
t.true(callback.calledWith(env, tmpDir, "refs/remotes/pull/1/merge"));
t.true(callback.calledWith(env, tmpDir, "HEAD"));
});
},
);
@@ -95,7 +116,6 @@ test.serial(
"getRef() returns ref provided as an input and ignores current HEAD",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const getAdditionalInputStub = sinon.stub(
actionsUtil,
"getOptionalInput",
@@ -104,14 +124,22 @@ test.serial(
getAdditionalInputStub.withArgs("sha").resolves("b".repeat(40));
// These values are be ignored
process.env["GITHUB_REF"] = "refs/pull/1/merge";
process.env["GITHUB_SHA"] = "a".repeat(40);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[ActionsEnvVars.GITHUB_REF]: "refs/pull/1/merge",
[ActionsEnvVars.GITHUB_SHA]: "a".repeat(40),
});
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("b".repeat(40));
callback.withArgs(sinon.match.any, "HEAD").resolves("b".repeat(40));
callback
.withArgs(sinon.match.any, tmpDir, "refs/pull/1/merge")
.resolves("b".repeat(40));
callback
.withArgs(sinon.match.any, sinon.match.string, "HEAD")
.resolves("b".repeat(40));
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, "refs/pull/2/merge");
// getCommitOid shouldn't be called, because the ref should be taken from the input
@@ -124,14 +152,17 @@ test.serial(
"getRef() returns CODE_SCANNING_REF as a fallback for GITHUB_REF",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const expectedRef = "refs/pull/1/HEAD";
const currentSha = "a".repeat(40);
process.env["CODE_SCANNING_REF"] = expectedRef;
process.env["GITHUB_REF"] = "";
process.env["GITHUB_SHA"] = currentSha;
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[EnvVar.CODE_SCANNING_REF]: expectedRef,
[ActionsEnvVars.GITHUB_REF]: "",
[ActionsEnvVars.GITHUB_SHA]: currentSha,
});
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, expectedRef);
});
},
@@ -141,14 +172,17 @@ test.serial(
"getRef() returns GITHUB_REF over CODE_SCANNING_REF if both are provided",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const expectedRef = "refs/pull/1/merge";
const currentSha = "a".repeat(40);
process.env["CODE_SCANNING_REF"] = "refs/pull/1/HEAD";
process.env["GITHUB_REF"] = expectedRef;
process.env["GITHUB_SHA"] = currentSha;
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[EnvVar.CODE_SCANNING_REF]: "refs/pull/1/HEAD",
[ActionsEnvVars.GITHUB_REF]: expectedRef,
[ActionsEnvVars.GITHUB_SHA]: currentSha,
});
const actualRef = await gitUtils.getRef();
const actualRef = await gitUtils.getRef(env, tmpDir);
t.deepEqual(actualRef, expectedRef);
});
},
@@ -158,7 +192,9 @@ test.serial(
"getRef() throws an error if only `ref` is provided as an input",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
const getAdditionalInputStub = sinon.stub(
actionsUtil,
"getOptionalInput",
@@ -167,7 +203,7 @@ test.serial(
await t.throwsAsync(
async () => {
await gitUtils.getRef();
await gitUtils.getRef(env, tmpDir);
},
{
instanceOf: Error,
@@ -183,8 +219,12 @@ test.serial(
"getRef() throws an error if only `sha` is provided as an input",
async (t) => {
await withTmpDir(async (tmpDir: string) => {
setupActionsVars(tmpDir, tmpDir);
process.env["GITHUB_WORKSPACE"] = "/tmp";
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
env.setAll({
[ActionsEnvVars.GITHUB_WORKSPACE]: "/tmp",
});
const getAdditionalInputStub = sinon.stub(
actionsUtil,
"getOptionalInput",
@@ -193,7 +233,7 @@ test.serial(
await t.throwsAsync(
async () => {
await gitUtils.getRef();
await gitUtils.getRef(env, tmpDir);
},
{
instanceOf: Error,
@@ -206,13 +246,16 @@ test.serial(
);
test.serial("isAnalyzingDefaultBranch()", async (t) => {
process.env["GITHUB_EVENT_NAME"] = "push";
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "true";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
process.env["CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH"] = "false";
const env = getTestEnv({
[ActionsEnvVars.GITHUB_EVENT_NAME]: "push",
CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH: "true",
});
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, ""), true);
env.set("CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH", "false");
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
setupActionsVars(tmpDir, tmpDir, {}, env);
const envFile = path.join(tmpDir, "event.json");
fs.writeFileSync(
envFile,
@@ -222,17 +265,17 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
},
}),
);
process.env["GITHUB_EVENT_PATH"] = envFile;
env.set(ActionsEnvVars.GITHUB_EVENT_PATH, envFile);
process.env["GITHUB_REF"] = "main";
process.env["GITHUB_SHA"] = "1234";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
env.set(ActionsEnvVars.GITHUB_REF, "main");
env.set(ActionsEnvVars.GITHUB_SHA, "1234");
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
process.env["GITHUB_REF"] = "refs/heads/main";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
process.env["GITHUB_REF"] = "feature";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
env.set(ActionsEnvVars.GITHUB_REF, "feature");
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), false);
fs.writeFileSync(
envFile,
@@ -240,9 +283,9 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
schedule: "0 0 * * *",
}),
);
process.env["GITHUB_EVENT_NAME"] = "schedule";
process.env["GITHUB_REF"] = "refs/heads/main";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), true);
env.set(ActionsEnvVars.GITHUB_EVENT_NAME, "schedule");
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), true);
const getAdditionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
getAdditionalInputStub
@@ -251,9 +294,9 @@ test.serial("isAnalyzingDefaultBranch()", async (t) => {
getAdditionalInputStub
.withArgs("sha")
.resolves("0000000000000000000000000000000000000000");
process.env["GITHUB_EVENT_NAME"] = "schedule";
process.env["GITHUB_REF"] = "refs/heads/main";
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(), false);
env.set(ActionsEnvVars.GITHUB_EVENT_NAME, "schedule");
env.set(ActionsEnvVars.GITHUB_REF, "refs/heads/main");
t.deepEqual(await gitUtils.isAnalyzingDefaultBranch(env, tmpDir), false);
});
});

View File

@@ -13,6 +13,7 @@ import {
getWorkflowEvent,
getWorkflowEventName,
} from "./actions-util";
import { ActionsEnvVars, EnvVar, type ReadOnlyEnv } from "./environment";
import { ConfigurationError, getRequiredEnvParam } from "./util";
/**
@@ -101,6 +102,7 @@ export const runGitCommand = async function (
* Gets the SHA of the commit that is currently checked out.
*/
export const getCommitOid = async function (
env: ReadOnlyEnv,
checkoutPath: string,
ref = "HEAD",
): Promise<string> {
@@ -119,7 +121,9 @@ export const getCommitOid = async function (
);
return stdout.trim();
} catch {
return getOptionalInput("sha") || getRequiredEnvParam("GITHUB_SHA");
return (
getOptionalInput("sha") || env.getRequired(ActionsEnvVars.GITHUB_SHA)
);
}
};
@@ -241,7 +245,7 @@ export const getGitRoot = async function (
["rev-parse", "--show-toplevel"],
`Cannot find Git repository root from the source root ${sourceRoot}.`,
);
return stdout.trim();
return path.resolve(stdout.trim());
} catch {
// Errors are already logged by runGitCommand()
return undefined;
@@ -314,18 +318,18 @@ export const getFileOidsUnderPath = async function (
return fileOidMap;
};
function getRefFromEnv(): string {
function getRefFromEnv(env: ReadOnlyEnv): 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");
refEnv = env.getRequired(ActionsEnvVars.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"];
const maybeRef = env.getOptional(EnvVar.CODE_SCANNING_REF);
if (maybeRef === undefined || maybeRef.length === 0) {
throw e;
}
@@ -337,15 +341,16 @@ function getRefFromEnv(): string {
/**
* Get the ref currently being analyzed.
*/
export async function getRef(): Promise<string> {
export async function getRef(
env: ReadOnlyEnv,
checkoutPath: string | undefined,
): 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");
checkoutPath =
checkoutPath ?? env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE);
const hasRefInput = !!refInput;
const hasShaInput = !!shaInput;
@@ -356,8 +361,8 @@ export async function getRef(): Promise<string> {
);
}
const ref = refInput || getRefFromEnv();
const sha = shaInput || getRequiredEnvParam("GITHUB_SHA");
const ref = refInput || getRefFromEnv(env);
const sha = shaInput || env.getRequired(ActionsEnvVars.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.
@@ -374,7 +379,7 @@ export async function getRef(): Promise<string> {
return ref;
}
const head = await getCommitOid(checkoutPath, "HEAD");
const head = await getCommitOid(env, 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
@@ -384,6 +389,7 @@ export async function getRef(): Promise<string> {
const hasChangedRef =
sha !== head &&
(await getCommitOid(
env,
checkoutPath,
ref.replace(/^refs\/pull\//, "refs/remotes/pull/"),
)) !== head;
@@ -410,20 +416,23 @@ function removeRefsHeadsPrefix(ref: string): string {
* 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") {
export async function isAnalyzingDefaultBranch(
env: ReadOnlyEnv,
checkoutPath: string | undefined,
): Promise<boolean> {
if (env.getOptional("CODE_SCANNING_IS_ANALYZING_DEFAULT_BRANCH") === "true") {
return true;
}
// Get the current ref and trim and refs/heads/ prefix
let currentRef = await getRef();
let currentRef = await getRef(env, checkoutPath);
currentRef = removeRefsHeadsPrefix(currentRef);
const event = getWorkflowEvent();
const event = getWorkflowEvent(env);
let defaultBranch = event?.repository?.default_branch;
if (getWorkflowEventName() === "schedule") {
defaultBranch = removeRefsHeadsPrefix(getRefFromEnv());
if (getWorkflowEventName(env) === "schedule") {
defaultBranch = removeRefsHeadsPrefix(getRefFromEnv(env));
}
return currentRef === defaultBranch;

View File

@@ -44,7 +44,7 @@ import {
} from "./util";
import {
getCategoryInputOrThrow,
getCheckoutPathInputOrThrow,
getRepositoryRootOrThrow,
getUploadInputOrThrow,
getWorkflow,
} from "./workflow";
@@ -144,7 +144,15 @@ async function prepareFailedSarif(
});
}
const category = getCategoryInputOrThrow(workflow, jobName, matrix);
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
// Try to determine the path at which the repository that we failed to analyse is checked out at.
// We need this to relativise the paths in the SARIF.
const checkoutPath = getRepositoryRootOrThrow(
workflow,
jobName,
matrix,
config,
);
const result = await generateFailedSarif(
logger,

View File

@@ -50,6 +50,7 @@ async function run(startedAt: Date) {
// possible, and only use safe functions outside.
const logger = getActionsLogger();
const env = getEnv();
let config: Config | undefined;
let uploadFailedSarifResult:
| initActionPostHelper.UploadFailedSarifResult
@@ -91,7 +92,7 @@ async function run(startedAt: Date) {
repositoryNwo,
features,
jobStatus,
getEnv(),
env,
logger,
);
@@ -100,7 +101,7 @@ async function run(startedAt: Date) {
// do this under these circumstances to avoid slowing down analyses for PRs
// and where caching may not be enabled.
if (
(await gitUtils.isAnalyzingDefaultBranch()) &&
(await gitUtils.isAnalyzingDefaultBranch(env, config.repositoryRoot)) &&
config.dependencyCachingEnabled !== CachingKind.None
) {
dependencyCachingUsage = await getDependencyCacheUsage(logger);

View File

@@ -15,6 +15,7 @@ import {
CleanupLevel,
getBaseDatabaseOidsFilePath,
getCodeQLDatabasePath,
getEnv,
getErrorMessage,
isInTestMode,
tryGetFolderBytes,
@@ -377,7 +378,7 @@ export async function getCacheSaveKey(
`Failed to get workflow run ID or attempt ID. Reason: ${getErrorMessage(e)}`,
);
}
const sha = await getCommitOid(checkoutPath);
const sha = await getCommitOid(getEnv(), checkoutPath);
const restoreKeyPrefix = await getCacheRestoreKeyPrefix(
config,
codeQlVersion,

View File

@@ -366,7 +366,7 @@ export async function createStatusReportBase(
try {
const commitOid =
getOptionalInput("sha") || process.env["GITHUB_SHA"] || "";
const ref = await getRef();
const ref = await getRef(getEnv(), config?.repositoryRoot);
const jobRunUUID = process.env[EnvVar.JOB_RUN_UUID] || "";
const workflowRunID = getWorkflowRunID();
const workflowRunAttempt = getWorkflowRunAttempt();

View File

@@ -968,6 +968,7 @@ export function createTestConfig(overrides: Partial<Config>): Config {
{},
{
version: getActionVersion(),
repositoryRoot: undefined,
analysisKinds: [AnalysisKind.CodeScanning],
languages: [],
buildMode: undefined,

View File

@@ -182,6 +182,7 @@ test.serial(
stubCodeql,
[BuiltInLanguage.javascript, BuiltInLanguage.cpp],
logger,
undefined,
);
t.assert(
stubRestore.calledOnceWith(

View File

@@ -14,6 +14,7 @@ import { Language } from "./languages";
import { Logger } from "./logging";
import {
asHTTPError,
getEnv,
getErrorMessage,
tryGetFolderBytes,
waitForResultWithTimeLimit,
@@ -43,6 +44,7 @@ const MAX_CACHE_OPERATION_MS = 120_000; // Two minutes
* @param codeql The CodeQL instance to use.
* @param languages The languages being analyzed.
* @param logger A logger to record some informational messages to.
* @param repositoryRoot The path at which the repository is checked out at.
* @returns A partial map from languages to TRAP cache paths on disk, with
* languages for which we shouldn't use TRAP caching omitted.
*/
@@ -50,6 +52,7 @@ export async function downloadTrapCaches(
codeql: CodeQL,
languages: Language[],
logger: Logger,
repositoryRoot: string | undefined,
): Promise<{ [language: string]: string }> {
const result: { [language: string]: string } = {};
const languagesSupportingCaching = await getLanguagesSupportingCaching(
@@ -72,7 +75,7 @@ export async function downloadTrapCaches(
result[language] = cacheDir;
}
if (await gitUtils.isAnalyzingDefaultBranch()) {
if (await gitUtils.isAnalyzingDefaultBranch(getEnv(), repositoryRoot)) {
logger.info(
"Analyzing default branch. Skipping downloading of TRAP caches.",
);
@@ -132,7 +135,12 @@ export async function uploadTrapCaches(
config: Config,
logger: Logger,
): Promise<boolean> {
if (!(await gitUtils.isAnalyzingDefaultBranch())) return false; // Only upload caches from the default branch
// Only upload caches from the default branch
if (
!(await gitUtils.isAnalyzingDefaultBranch(getEnv(), config.repositoryRoot))
) {
return false;
}
for (const language of config.languages) {
const cacheDir = config.trapCaches[language];
@@ -180,6 +188,8 @@ export async function cleanupTrapCaches(
features: FeatureEnablement,
logger: Logger,
): Promise<TrapCacheCleanupStatusReport> {
const env = getEnv();
if (!(await features.getValue(Feature.CleanupTrapCaches))) {
return {
trap_cache_cleanup_skipped_because: "feature disabled",
@@ -189,7 +199,7 @@ export async function cleanupTrapCaches(
"TRAP cache cleanup is deprecated and will be removed in May 2026. " +
"We recommend instead disabling TRAP caching by passing the `trap-caching: false` input to the `init` Action.",
);
if (!(await gitUtils.isAnalyzingDefaultBranch())) {
if (!(await gitUtils.isAnalyzingDefaultBranch(env, config.repositoryRoot))) {
return {
trap_cache_cleanup_skipped_because: "not analyzing default branch",
};
@@ -200,7 +210,7 @@ export async function cleanupTrapCaches(
const allCaches = await apiClient.listActionsCaches(
CODEQL_TRAP_CACHE_PREFIX,
await gitUtils.getRef(),
await gitUtils.getRef(env, config.repositoryRoot),
);
for (const language of config.languages) {

View File

@@ -14,7 +14,7 @@ import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import { getConfig } from "./config-utils";
import { readDiffRangesJsonFile } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import { FeatureEnablement } from "./feature-flags";
import * as fingerprints from "./fingerprints";
import * as gitUtils from "./git-utils";
@@ -761,12 +761,13 @@ export async function uploadPostProcessedFiles(
const zippedSarif = zlib.gzipSync(sarifPayload).toString("base64");
const checkoutURI = url.pathToFileURL(checkoutPath).href;
const env = util.getEnv();
const payload = uploadTarget.transformPayload(
buildPayload(
await gitUtils.getCommitOid(checkoutPath),
await gitUtils.getRef(),
await gitUtils.getCommitOid(env, checkoutPath),
await gitUtils.getRef(env, checkoutPath),
postProcessingResults.analysisKey,
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
env.getRequired(ActionsEnvVars.GITHUB_WORKFLOW),
zippedSarif,
actionsUtil.getWorkflowRunID(),
actionsUtil.getWorkflowRunAttempt(),

View File

@@ -4,10 +4,12 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { createStubCodeQL, getCodeQLForTesting } from "./codeql";
import { EnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import {
checkExpectedLogMessages,
createTestConfig,
getRecordingLogger,
getTestEnv,
LoggedMessage,
setupTests,
} from "./testing-utils";
@@ -1002,3 +1004,124 @@ test.serial(
t.is(messages.length, 0);
},
);
test("getRepositoryRootOrThrow - gets root from config", async (t) => {
const expectedRoot = "/path/to/root";
const repositoryRoot = workflow.getRepositoryRootOrThrow(
{},
"testJob",
{},
createTestConfig({ repositoryRoot: expectedRoot }),
getTestEnv(),
);
t.is(repositoryRoot, expectedRoot);
});
test("getRepositoryRootOrThrow - gets root from workflow", async (t) => {
const expectedRoot = "/path/to/root";
const repositoryRoot = workflow.getRepositoryRootOrThrow(
{
jobs: {
testJob: {
steps: [
{
uses: "github/codeql-action/analyze",
with: { checkout_path: expectedRoot },
},
],
},
},
},
"testJob",
{},
createTestConfig({}),
getTestEnv(),
);
t.is(repositoryRoot, expectedRoot);
});
test("getRepositoryRootOrThrow - gets root from environment", async (t) => {
const expectedRoot = "/path/to/root";
const repositoryRoot = workflow.getRepositoryRootOrThrow(
{
jobs: { testJob: { steps: [{ uses: "github/codeql-action/analyze" }] } },
},
"testJob",
{},
createTestConfig({}),
getTestEnv({ [ActionsEnvVars.GITHUB_WORKSPACE]: expectedRoot }),
);
t.is(repositoryRoot, expectedRoot);
});
test("getRepositoryRootOrThrow - throws if there's no matching job", async (t) => {
t.throws(
() =>
workflow.getRepositoryRootOrThrow(
{
jobs: {
otherJob: {
steps: [
{
uses: "github/codeql-action/analyze",
with: { checkout_path: "/some/path" },
},
],
},
},
},
"testJob",
{},
createTestConfig({}),
getTestEnv(),
),
{ message: /since the workflow has no job named testJob./ },
);
});
test("getRepositoryRootOrThrow - throws if there's no analyze step", async (t) => {
t.throws(
() =>
workflow.getRepositoryRootOrThrow(
{
jobs: {
testJob: { steps: [] },
},
},
"testJob",
{},
createTestConfig({}),
getTestEnv(),
),
{ message: /since the testJob job does not call/ },
);
});
test("getRepositoryRootOrThrow - throws if the env var is not set", async (t) => {
t.throws(
() =>
workflow.getRepositoryRootOrThrow(
{
jobs: {
testJob: {
steps: [
{
uses: "github/codeql-action/analyze",
},
],
},
},
},
"testJob",
{},
createTestConfig({}),
getTestEnv(),
),
{
message: `${ActionsEnvVars.GITHUB_WORKSPACE} environment variable must be set`,
},
);
});

View File

@@ -8,7 +8,8 @@ import * as yaml from "js-yaml";
import { isDynamicWorkflow } from "./actions-util";
import * as api from "./api-client";
import { CodeQL } from "./codeql";
import { EnvVar } from "./environment";
import type { Config } from "./config-utils";
import { ActionsEnvVars, EnvVar, getEnv, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
import {
getRequiredEnvParam,
@@ -441,27 +442,45 @@ export function getUploadInputOrThrow(
}
/**
* Makes a best effort attempt to retrieve the checkout_path input for the
* particular job, given a set of matrix variables.
* Makes a best effort attempt to determine the root path of the repository that the analysis
* relates to. We need that to make paths in SARIF files relative.
*
* - If available, we take the `repositoryRoot` from the `config`.
* - If it isn't, we fall back to trying to extract a `checkout_path` input from the `workflow`.
* - Finally, we fall back to the value of `GITHUB_WORKSPACE`.
*
* Typically you'll want to wrap this function in a try/catch block and handle the error.
*
* @returns the checkout_path input
* @throws an error if the checkout_path input could not be determined
* @param workflow The workflow specification of the currently running workflow.
* @param jobName The name of the job that is currently running.
* @param matrixVars The matrix variables, if any.
* @param config The CodeQL Action configuration state.
* @param env The environment variables.
*
* @returns The repository root path, or its best approximation.
* @throws `Error` if the repository root could not be determined.
*/
export function getCheckoutPathInputOrThrow(
export function getRepositoryRootOrThrow(
workflow: Workflow,
jobName: string,
matrixVars: { [key: string]: string } | undefined,
config: Config,
env: ReadOnlyEnv = getEnv(),
): string {
return (
// If the CodeQL Action already has a persisted repository root, then we can just use that.
config.repositoryRoot ??
// Otherwise, try to retrieve it from a `checkout_path` input in the workflow specification.
getInputOrThrow(
workflow,
jobName,
getAnalyzeActionName(),
"checkout_path",
matrixVars,
) || getRequiredEnvParam("GITHUB_WORKSPACE") // if unspecified, checkout_path defaults to ${{ github.workspace }}
) ??
// Finally, if all of the above fail, just use the value of `GITHUB_WORKSPACE` since that
// is what is used by default.
env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE)
);
}