Compare commits

...

34 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
7b6a151fb5 Merge pull request #4177 from github/mbg/version-check/recommend-dependabot-grouping
Point at Dependabot `groups` docs in version mismatch error
2026-09-25 15:14:03 +00:00
Michael B. Gale
29b01b6799 Point at Dependabot groups docs in version mismatch error 2026-09-25 15:59:36 +01:00
Michael B. Gale
fa2bea7c7a Merge pull request #4176 from github/mbg/start-proxy/improve-post-githubversion-logic
Remove unnecessary check in `start-proxy-action-post`
2026-09-25 14:39:40 +00:00
Michael B. Gale
9fb2fa5f46 Avoid duplicate getApiDetails in getGitHubVersion 2026-09-25 15:02:13 +01:00
Michael B. Gale
054b25e1c2 Remove unnecessary config check 2026-09-25 14:58:43 +01:00
Michael B. Gale
fa8392b7e5 Merge pull request #4161 from github/mbg/improve-json-failures
Add context to JSON parse errors
2026-09-24 11:18:23 +00:00
Michael B. Gale
38c1d74ffa Merge remote-tracking branch 'origin/main' into mbg/improve-json-failures 2026-09-24 12:06:30 +01:00
Michael B. Gale
19aa55de51 Log error in getTracerConfigForCluster instead of re-throwing it 2026-09-24 12:06:01 +01:00
Michael B. Gale
a7334dd080 Merge pull request #4170 from github/mergeback/v4.38.2-to-main-2892aa5e
Mergeback v4.38.2 refs/heads/releases/v4 into main
2026-09-24 10:45:39 +00:00
Michael B. Gale
aa2cb993a1 Merge remote-tracking branch 'origin/main' into mbg/improve-json-failures 2026-09-24 11:42:41 +01:00
Henry Mercer
9c970806c4 Merge pull request #4167 from github/henrymercer/move-bundle-helpers
Move bundle types and helpers out of setup-codeql.ts
2026-09-24 10:40:36 +00:00
Michael B. Gale
b5f938c947 Log error in restoreInputs instead of re-throwing it 2026-09-24 11:34:56 +01:00
github-actions[bot]
73dc34459c Rebuild 2026-09-24 10:28:01 +00:00
github-actions[bot]
266e866100 Update changelog and version after v4.38.2 2026-09-24 10:27:53 +00:00
Henry Mercer
6aa6d5553a Remove the getCodeQLBundleName wrapper from setup-codeql.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-24 10:09:24 +01:00
Henry Mercer
f42df5b5c1 Move bundle types and helpers out of setup-codeql.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-24 09:52:32 +01:00
Michael B. Gale
72509a81ba Consistently use resolved path 2026-09-23 12:57:16 +01:00
Michael B. Gale
fa4712895b Restore previous restoreInputs behaviour 2026-09-22 19:09:07 +01:00
Michael B. Gale
660f7c5f24 Fix formatting 2026-09-22 18:58:17 +01:00
Michael B. Gale
fe0a932a6a Wrap JSON parse errors in readSarifFile with context 2026-09-22 18:53:03 +01:00
Michael B. Gale
8e36092a16 Wrap JSON parse errors in cloneObject with context 2026-09-22 18:52:03 +01:00
Michael B. Gale
2294a7a8b1 Wrap JSON parse errors in parseMatrixInput with context 2026-09-22 18:46:55 +01:00
Michael B. Gale
c0369726ee Wrap JSON parse errors in getTracerConfigForCluster with context 2026-09-22 18:43:26 +01:00
Michael B. Gale
a5c2338ec0 Wrap JSON parse errors in restoreInputs with context 2026-09-22 18:41:31 +01:00
Michael B. Gale
f042742baf Wrap JSON parse errors in resolveExtractor with context 2026-09-22 18:40:37 +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
41 changed files with 1440 additions and 891 deletions

View File

@@ -2,6 +2,10 @@
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
## [UNRELEASED]
No user facing changes.
## 4.38.2 - 24 Sept 2026
- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)

1349
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.38.2",
"version": "4.38.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.38.2",
"version": "4.38.3",
"license": "MIT",
"workspaces": [
"pr-checks"

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.38.2",
"version": "4.38.3",
"private": true,
"description": "CodeQL action",
"scripts": {

View File

@@ -14,6 +14,7 @@ import {
getCodeQLDatabasePath,
ConfigurationError,
getEnv,
getErrorMessage,
} from "./util";
/**
@@ -94,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);
}
@@ -121,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"));
@@ -412,14 +413,23 @@ export const persistInputs = function (env: Env = getEnv()) {
/**
* Restores all inputs to the action from the persisted state.
*/
export const restoreInputs = function () {
const persistedInputs = core.getState(persistedInputsKey);
if (persistedInputs) {
for (const [name, value] of JSON.parse(persistedInputs)) {
process.env[name] = value;
export function restoreInputs(logger: Logger) {
try {
const persistedInputsValue = core.getState(persistedInputsKey);
if (persistedInputsValue) {
const persistedInputs = JSON.parse(persistedInputsValue);
for (const [name, value] of persistedInputs) {
process.env[name] = value;
}
}
} catch (err) {
logger.error(`Unable to restore inputs: ${getErrorMessage(err)}`);
throw new Error(
"Failed to restore inputs from the state set by this action's main execution.",
);
}
};
}
export interface PullRequestBranches {
base: string;

View File

@@ -25,8 +25,8 @@ export async function runWrapper() {
// possible, and only use safe functions outside.
try {
actionsUtil.restoreInputs();
const logger = getActionsLogger();
actionsUtil.restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);

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

@@ -249,9 +249,10 @@ export async function getGitHubVersionFromApi(
*/
export async function getGitHubVersion(): Promise<GitHubVersion> {
if (cachedGitHubVersion === undefined) {
const apiDetails = getApiDetails();
cachedGitHubVersion = await getGitHubVersionFromApi(
getApiClient(),
getApiDetails(),
createApiClientWithDetails(apiDetails),
apiDetails,
);
}
return cachedGitHubVersion;

View File

@@ -1,7 +1,26 @@
import test from "ava";
import { getCodeQLBundleFromUrl } from "./codeql-bundle";
import { getCodeQLBundleFromUrl, getCodeQLBundleName } from "./codeql-bundle";
import { BuiltInLanguage } from "./languages";
import { BundlePlatform } from "./platform";
test("getCodeQLBundleName returns a per-language bundle name only when a language is specified", (t) => {
t.is(
getCodeQLBundleName("zstd", BundlePlatform.Linux64, BuiltInLanguage.java),
"codeql-bundle-java-linux64.tar.zst",
);
t.is(
getCodeQLBundleName("zstd", BundlePlatform.Linux64),
"codeql-bundle-linux64.tar.zst",
);
});
test("getCodeQLBundleName names the Swift bundle for macOS", (t) => {
t.is(
getCodeQLBundleName("zstd", BundlePlatform.Osx64, BuiltInLanguage.swift),
"codeql-bundle-swift-osx64.tar.zst",
);
});
for (const [assetName, language] of [
["codeql-bundle-java-linux64.tar.zst", BuiltInLanguage.java],

View File

@@ -1,4 +1,6 @@
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { BundlePlatform } from "./platform";
import type { CompressionMethod } from "./tar";
/** Describes the contents and location of a downloadable CodeQL bundle. */
export type CodeQLBundle =
@@ -11,6 +13,42 @@ export type CodeQLBundle =
combinedBundleURL?: string;
};
/** A resolved download, including its bundle identity and version. */
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */
compressionMethod: CompressionMethod;
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** Requested CLI version, if known. */
cliVersion?: string;
/** Resolved version for telemetry, independent of whether the bundle can be cached. */
toolsVersion: string;
}
/** Returns the exact bundle asset name for a platform and optional language. */
export function getCodeQLBundleName(
compressionMethod: CompressionMethod,
platform: BundlePlatform | undefined,
language?: BuiltInLanguage,
): string {
const extensions: Record<CompressionMethod, string> = {
gzip: ".tar.gz",
zstd: ".tar.zst",
};
const extension = extensions[compressionMethod];
if (platform === undefined) {
return `codeql-bundle${extension}`;
}
if (language !== undefined) {
return `codeql-bundle-${language}-${platform}${extension}`;
}
return `codeql-bundle-${platform}${extension}`;
}
const PER_LANGUAGE_BUNDLE_NAME =
/^codeql-bundle-(.+)-(?:linux64|osx64|win64)\.tar\.(?:gz|zst)$/;

View File

@@ -952,7 +952,13 @@ async function getCodeQLForCmd(
},
},
).exec();
return JSON.parse(extractorPath) as string;
try {
return JSON.parse(extractorPath) as string;
} catch (err) {
throw new Error(
`Failed to parse extractor path for '${language}' from CLI: ${getErrorMessage(err)}\nOutput was: ${extractorPath}`,
);
}
},
async resolveQueriesStartingPacks(queries: string[]): Promise<string[]> {
const codeqlArgs = [
@@ -1213,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;
@@ -1425,7 +1440,12 @@ export async function getConfig(
}
if (config.version !== getActionVersion()) {
throw new ConfigurationError(
`Loaded a configuration file for version '${config.version}', but running version '${getActionVersion()}'`,
[
`Loaded a configuration file for version '${config.version}', but running version '${getActionVersion()}'.`,
"All steps in a workflow that use `github/codeql-action` must use the same version to work correctly.",
"If you are using Dependabot to manage dependency updates, you can configure a dependency group to update all `github/codeql-action` steps at the same time.",
"For more information, see https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#groups--",
].join(" "),
);
}

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
@@ -62,7 +63,7 @@ async function run(startedAt: Date) {
const jobStatus = getOptionalInput("job-status");
// Restore inputs from `init` Action.
restoreInputs();
restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);
@@ -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

@@ -716,7 +716,7 @@ async function run(
);
}
const tracerConfig = await getCombinedTracerConfig(codeql, config);
const tracerConfig = await getCombinedTracerConfig(logger, codeql, config);
if (tracerConfig !== undefined) {
for (const [key, value] of Object.entries(tracerConfig.env)) {
core.exportVariable(key, value);

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

@@ -131,3 +131,16 @@ export async function getPerLanguageBundleLanguage(
return language;
}
/** Explains why an eligible per-language bundle is being replaced by a combined bundle. */
export function logPerLanguageBundleFallback(
{ logger }: ActionState<["Logger"]>,
language: BuiltInLanguage,
location: string,
): void {
logger.warning(
`No per-language CodeQL bundle for '${language}' was found at ${location}, so ` +
"falling back to the bundle that contains all languages. This analysis will still " +
"produce correct results, but will take longer to set up.",
);
}

View File

@@ -1,6 +1,7 @@
import * as fs from "fs";
import { Logger } from "../logging";
import { getErrorMessage } from "../util";
import * as sarif from "sarif";
@@ -48,7 +49,13 @@ export function getToolNames(sarifFile: Partial<sarif.Log>): string[] {
* @returns The resulting JSON value, cast to a SARIF `Log`.
*/
export function readSarifFile(sarifFilePath: string): Partial<sarif.Log> {
return JSON.parse(fs.readFileSync(sarifFilePath, "utf8")) as sarif.Log;
try {
return JSON.parse(fs.readFileSync(sarifFilePath, "utf8")) as sarif.Log;
} catch (err) {
throw new Error(
`Parsing SARIF file at '${sarifFilePath}' failed: ${getErrorMessage(err)}`,
);
}
}
// Takes a list of paths to sarif files and combines them together,

View File

@@ -435,7 +435,7 @@ test.serial(
// Check that the `CodeQLToolsSource` object matches our expectations.
const expectedVersion = `0.0.0-${expectedDate}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/codeql-bundle-linux64.tar.zst`;
t.deepEqual(source, {
bundle: { kind: "combined", url: expectedURL },
bundleVersion: expectedDate,
@@ -505,7 +505,7 @@ test.serial(
// Check that the `CodeQLToolsSource` object matches our expectations.
const expectedVersion = `0.0.0-${expectedDate}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/codeql-bundle-linux64.tar.zst`;
t.deepEqual(source, {
bundle: { kind: "combined", url: expectedURL },
bundleVersion: expectedDate,
@@ -1143,30 +1143,6 @@ const PER_LANGUAGE_CLI_VERSION = {
],
};
test.serial(
"getCodeQLBundleName returns a per-language bundle name only when a language is specified",
(t) => {
sinon.stub(process, "platform").value("linux");
sinon.stub(process, "arch").value("x64");
t.is(
setupCodeql.getCodeQLBundleName("zstd", BuiltInLanguage.java),
"codeql-bundle-java-linux64.tar.zst",
);
t.is(
setupCodeql.getCodeQLBundleName("zstd"),
"codeql-bundle-linux64.tar.zst",
);
},
);
test.serial("getCodeQLBundleName names the Swift bundle for macOS", (t) => {
sinon.stub(process, "platform").value("darwin");
t.is(
setupCodeql.getCodeQLBundleName("zstd", BuiltInLanguage.swift),
"codeql-bundle-swift-osx64.tar.zst",
);
});
test.serial(
"getCodeQLSource downloads the per-language bundle for a single explicit language",
async (t) => {

View File

@@ -17,7 +17,12 @@ import {
isRunningLocalAction,
} from "./actions-util";
import * as api from "./api-client";
import { CodeQLBundle, getCodeQLBundleFromUrl } from "./codeql-bundle";
import {
CodeQLBundle,
CodeQLDownloadSource,
getCodeQLBundleFromUrl,
getCodeQLBundleName,
} from "./codeql-bundle";
import * as defaults from "./defaults.json";
import {
addNoLanguageDiagnostic,
@@ -35,7 +40,10 @@ import {
import { BuiltInLanguage } from "./languages";
import { Logger } from "./logging";
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
import { getPerLanguageBundleLanguage } from "./per-language-bundles";
import {
getPerLanguageBundleLanguage,
logPerLanguageBundleFallback,
} from "./per-language-bundles";
import { getBundlePlatform } from "./platform";
import * as tar from "./tar";
import {
@@ -49,6 +57,8 @@ import {
import * as util from "./util";
import { isGoodVersion } from "./util";
export type { CodeQLDownloadSource } from "./codeql-bundle";
export enum ToolsSource {
Unknown = "UNKNOWN",
Local = "LOCAL",
@@ -64,41 +74,6 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"];
const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
const CODEQL_TOOLCACHE_INPUT = "toolcache";
function getCodeQLBundleExtension(
compressionMethod: tar.CompressionMethod,
): string {
switch (compressionMethod) {
case "gzip":
return ".tar.gz";
case "zstd":
return ".tar.zst";
default:
util.assertNever(compressionMethod);
}
}
/**
* Returns the name of the CodeQL bundle asset to download.
*
* @param compressionMethod The compression method of the bundle.
* @param language Optional language for a per-language bundle. If omitted, returns a combined bundle name.
*/
export function getCodeQLBundleName(
compressionMethod: tar.CompressionMethod,
language?: BuiltInLanguage,
): string {
const extension = getCodeQLBundleExtension(compressionMethod);
const platform = getBundlePlatform();
if (platform === undefined) {
return `codeql-bundle${extension}`;
}
if (language !== undefined) {
return `codeql-bundle-${language}-${platform}${extension}`;
}
return `codeql-bundle-${platform}${extension}`;
}
export function getCodeQLActionRepository(logger: Logger): string {
if (isRunningLocalAction()) {
// This handles the case where the Action does not come from an Action repository,
@@ -223,22 +198,6 @@ export function convertToSemVer(version: string, logger: Logger): string {
return s;
}
/** A resolved download, including its bundle identity and version. */
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */
compressionMethod: tar.CompressionMethod;
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** Requested CLI version, if known. */
cliVersion?: string;
/** Resolved version for telemetry, independent of whether the bundle can be cached. */
toolsVersion: string;
}
export type CodeQLToolsSource =
| {
codeqlTarPath: string;
@@ -757,13 +716,14 @@ export async function getCodeQLSource(
? "zstd"
: "gzip";
const platform = getBundlePlatform();
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
{ env: getEnv(), features, logger },
{
rawLanguages,
cliVersion,
compressionMethod,
platform: getBundlePlatform(),
platform,
variant,
},
);
@@ -773,7 +733,7 @@ export async function getCodeQLSource(
getCodeQLBundleDownloadURL(
bundleTagName,
apiDetails,
getCodeQLBundleName(compressionMethod, language),
getCodeQLBundleName(compressionMethod, platform, language),
logger,
);
@@ -1153,11 +1113,7 @@ export async function downloadCodeQLBundle(
) {
throw e;
}
logger.warning(
`No per-language CodeQL bundle for '${bundle.language}' was found at ${bundle.url}, so ` +
"falling back to the bundle that contains all languages. This analysis will still " +
"produce correct results, but will take longer to set up.",
);
logPerLanguageBundleFallback(action, bundle.language, bundle.url);
const result = await downloadCodeQL(
{
@@ -1215,11 +1171,12 @@ async function getLatestNightlyBundle(
? "zstd"
: "gzip";
const platform = getBundlePlatform();
const language = await getPerLanguageBundleLanguage(action, {
rawLanguages,
cliVersion: undefined,
compressionMethod,
platform: getBundlePlatform(),
platform,
variant,
isLatestNightly: true,
});
@@ -1241,14 +1198,18 @@ async function getLatestNightlyBundle(
}
const assetUrl = (name: string) =>
`https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${name}`;
const url = assetUrl(getCodeQLBundleName(compressionMethod, language));
const url = assetUrl(
getCodeQLBundleName(compressionMethod, platform, language),
);
return language === undefined
? { kind: "combined", url }
: {
kind: "per-language",
url,
language,
combinedBundleURL: assetUrl(getCodeQLBundleName(compressionMethod)),
combinedBundleURL: assetUrl(
getCodeQLBundleName(compressionMethod, platform),
),
};
} catch (e) {
throw new Error(

View File

@@ -20,7 +20,7 @@ export async function runWrapper() {
try {
// Restore inputs from `start-proxy` Action.
actionsUtil.restoreInputs();
actionsUtil.restoreInputs(logger);
// Kill the running proxy
const pid = core.getState("proxy-process-pid");
@@ -38,12 +38,6 @@ export async function runWrapper() {
logger.info(
"Debug mode is on. Uploading proxy log as Actions debugging artifact...",
);
if (config?.gitHubVersion.type === undefined) {
logger.warning(
`Did not upload debug artifacts because cannot determine the GitHub variant running.`,
);
return;
}
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);

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

@@ -7,7 +7,12 @@ import * as sinon from "sinon";
import { CodeQL, getCodeQLForTesting } from "./codeql";
import * as configUtils from "./config-utils";
import { BuiltInLanguage } from "./languages";
import { createTestConfig, makeVersionInfo, setupTests } from "./testing-utils";
import {
createTestConfig,
makeVersionInfo,
RecordingLogger,
setupTests,
} from "./testing-utils";
import { ToolsFeature } from "./tools-features";
import { getCombinedTracerConfig } from "./tracer-config";
import * as util from "./util";
@@ -42,18 +47,20 @@ async function stubCodeql(
}
test("getCombinedTracerConfig - return undefined when no languages are traced languages", async (t) => {
const logger = new RecordingLogger();
await util.withTmpDir(async (tmpDir) => {
const config = getTestConfig(tmpDir);
// No traced languages
config.languages = [BuiltInLanguage.javascript, BuiltInLanguage.python];
t.deepEqual(
await getCombinedTracerConfig(await stubCodeql(), config),
await getCombinedTracerConfig(logger, await stubCodeql(), config),
undefined,
);
});
});
test("getCombinedTracerConfig", async (t) => {
const logger = new RecordingLogger();
await util.withTmpDir(async (tmpDir) => {
const config = getTestConfig(tmpDir);
@@ -82,7 +89,11 @@ test("getCombinedTracerConfig", async (t) => {
);
fs.writeFileSync(startTracingJson, JSON.stringify(startTracingEnv));
const result = await getCombinedTracerConfig(await stubCodeql(), config);
const result = await getCombinedTracerConfig(
logger,
await stubCodeql(),
config,
);
t.notDeepEqual(result, undefined);
t.false(Object.prototype.hasOwnProperty.call(result?.env, "CODEQL_RUNNER"));

View File

@@ -4,7 +4,7 @@ import * as path from "path";
import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { Logger } from "./logging";
import { asyncSome, BuildMode } from "./util";
import { asyncSome, BuildMode, getErrorMessage } from "./util";
export type TracerConfig = {
env: { [key: string]: string };
@@ -77,23 +77,28 @@ export async function endTracingForCluster(
}
async function getTracerConfigForCluster(
logger: Logger,
config: Config,
): Promise<TracerConfig> {
const tracingEnvVariables = JSON.parse(
fs.readFileSync(
path.resolve(
config.dbLocation,
"temp/tracingEnvironment/start-tracing.json",
),
"utf8",
),
const filePath = path.resolve(
config.dbLocation,
"temp/tracingEnvironment/start-tracing.json",
);
return {
env: tracingEnvVariables,
};
try {
const tracingEnvVariables = JSON.parse(fs.readFileSync(filePath, "utf8"));
return {
env: tracingEnvVariables,
};
} catch (err) {
logger.error(
`Failed to parse tracing environment from '${filePath}': ${getErrorMessage(err)}`,
);
throw new Error(`Failed to parse tracing environment from '${filePath}'.`);
}
}
export async function getCombinedTracerConfig(
logger: Logger,
codeql: CodeQL,
config: Config,
): Promise<TracerConfig | undefined> {
@@ -101,5 +106,5 @@ export async function getCombinedTracerConfig(
return undefined;
}
return await getTracerConfigForCluster(config);
return await getTracerConfigForCluster(logger, config);
}

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

@@ -17,9 +17,10 @@ export async function runWrapper() {
// possible, and only use safe functions outside.
try {
// Restore inputs from `upload-sarif` Action.
actionsUtil.restoreInputs();
const logger = getActionsLogger();
// Restore inputs from `upload-sarif` Action.
actionsUtil.restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);

View File

@@ -894,7 +894,13 @@ export function parseMatrixInput(
if (matrixInput === undefined || matrixInput === "null") {
return undefined;
}
return JSON.parse(matrixInput) as { [key: string]: string };
try {
return JSON.parse(matrixInput) as { [key: string]: string };
} catch (err) {
throw new Error(
`Failed to parse matrix input '${matrixInput}': ${getErrorMessage(err)}`,
);
}
}
export function wrapError(error: unknown): Error {
@@ -1037,7 +1043,11 @@ export enum BuildMode {
}
export function cloneObject<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
try {
return JSON.parse(JSON.stringify(obj)) as T;
} catch (err) {
throw new Error(`Cloning object failed: ${getErrorMessage(err)}`);
}
}
export async function cleanUpPath(file: string, name: string, logger: Logger) {

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)
);
}