Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c05a5b7
Add `ActionsEnvVars` enum
mbg Jun 16, 2026
652296e
Allow abstracting over `process.env`
mbg Jun 16, 2026
9c4ee01
Add `RemoteFileAddress` type
mbg Jun 23, 2026
07b6b1e
Move `getRemoteConfig` to `config/file.ts`
mbg Jun 23, 2026
9ef8be7
Refactor `parseRemoteFileAddress` out of `getRemoteConfig`
mbg Jun 23, 2026
82e5ca6
Return `RemoteFileAddress` from `parseRemoteFileAddress`
mbg Jun 23, 2026
c7a94c9
Add `getRemoteConfig` JSDoc
mbg Jun 23, 2026
85c8a8c
Add tests for `parseRemoteFileAddress`
mbg Jun 23, 2026
598d008
Make `ref` optional in `parseRemoteFileAddress`
mbg Jun 23, 2026
e537ff2
Make `path` optional in `parseRemoteFileAddress`
mbg Jun 23, 2026
12821cf
Make `owner` optional in `parseRemoteFileAddress`
mbg Jun 23, 2026
81ad479
Fix test names
mbg Jun 23, 2026
8102fa6
Anchor regex and trim input
mbg Jun 23, 2026
00e5a58
Update format in error message
mbg Jun 23, 2026
8d69da9
Improve whitespace handling
mbg Jun 23, 2026
3fe7ef9
Fix `getEnv` not using `getOptionalEnvVarFrom`
mbg Jun 23, 2026
812b882
Merge remote-tracking branch 'origin/main' into mbg/repo-props/config…
mbg Jun 23, 2026
f77cf55
Support old and new formats
mbg Jun 26, 2026
d8d1d6d
Add FF for shorthands
mbg Jun 24, 2026
e446d55
Add initial `ActionState` type
mbg Jun 30, 2026
f86aa52
Use `ActionState` for `getRemoteConfig`
mbg Jun 30, 2026
7b2af89
Add `TestEnv` class to simplify testing of `ActionState`-dependent fu…
mbg Jun 30, 2026
b6be81d
Gate new remote file format behind FF
mbg Jun 30, 2026
708e106
Index `ActionState` over the types of state used
mbg Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 166 additions & 83 deletions lib/entry-points.js

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions src/action-common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Env } from "./environment";
import { FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";

/** Describes different state features that an Action may have. */
export interface FeatureState {
Logger: {
/** The logger that is in use. */
logger: Logger;
};
Env: {
/** Information about environment variables. */
env: Env;
};
FeatureFlags: {
/** Information about enabled feature flags. */
features: FeatureEnablement;
};
}

/** Identifies a type of state an Action may have. */
export type StateFeature = keyof FeatureState;

/** Constructs the union of all state types identifies by `Fs`. */
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends [
infer Head extends StateFeature,
]
? FeatureState[Head]
: Fs extends [
infer Head extends StateFeature,
...infer Tail extends StateFeature[],
]
? FeatureState[Head] & FieldsOf<Tail>
: never;

/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
44 changes: 34 additions & 10 deletions src/actions-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ import {
*/
declare const __CODEQL_ACTION_VERSION__: string;

/**
* Enumerates known GitHub Actions environment variables that we expect
* to be set in a GitHub Actions environment.
*/
export enum ActionsEnvVars {
GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY",
GITHUB_API_URL = "GITHUB_API_URL",
GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME",
GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH",
GITHUB_JOB = "GITHUB_JOB",
GITHUB_REF = "GITHUB_REF",
GITHUB_REPOSITORY = "GITHUB_REPOSITORY",
GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT",
GITHUB_RUN_ID = "GITHUB_RUN_ID",
GITHUB_SERVER_URL = "GITHUB_SERVER_URL",
GITHUB_SHA = "GITHUB_SHA",
GITHUB_WORKFLOW = "GITHUB_WORKFLOW",
RUNNER_NAME = "RUNNER_NAME",
RUNNER_OS = "RUNNER_OS",
RUNNER_TEMP = "RUNNER_TEMP",
}

/**
* Abstracts over GitHub Actions functions so that we do not have to stub
* global functions in tests.
Expand Down Expand Up @@ -65,7 +87,7 @@ export function getTemporaryDirectory(): string {
const value = process.env["CODEQL_ACTION_TEMP"];
return value !== undefined && value !== ""
? value
: getRequiredEnvParam("RUNNER_TEMP");
: getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP);
}

const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json";
Expand All @@ -84,7 +106,7 @@ export function getActionVersion(): string {
* This will be "dynamic" for default setup workflow runs.
*/
export function getWorkflowEventName() {
return getRequiredEnvParam("GITHUB_EVENT_NAME");
return getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_NAME);
}

/**
Expand All @@ -104,14 +126,14 @@ export function isRunningLocalAction(): boolean {
* This can be used to get the Action's name or tell if we're running a local Action.
*/
function getRelativeScriptPath(): string {
const runnerTemp = getRequiredEnvParam("RUNNER_TEMP");
const runnerTemp = getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP);
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
return path.relative(actionsDirectory, __filename);
}

/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
export function getWorkflowEvent(): any {
const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH");
const eventJsonFile = getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH);
try {
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));
} catch (e) {
Expand Down Expand Up @@ -181,16 +203,16 @@ export function getUploadValue(input: string | undefined): UploadKind {
* Get the workflow run ID.
*/
export function getWorkflowRunID(): number {
const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID");
const workflowRunIdString = getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID);
const workflowRunID = parseInt(workflowRunIdString, 10);
if (Number.isNaN(workflowRunID)) {
throw new Error(
`GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
`${ActionsEnvVars.GITHUB_RUN_ID} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
);
}
if (workflowRunID < 0) {
throw new Error(
`GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}`,
`${ActionsEnvVars.GITHUB_RUN_ID} must be a non-negative integer. Current value is ${workflowRunIdString}`,
);
}
return workflowRunID;
Expand All @@ -200,16 +222,18 @@ export function getWorkflowRunID(): number {
* Get the workflow run attempt number.
*/
export function getWorkflowRunAttempt(): number {
const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT");
const workflowRunAttemptString = getRequiredEnvParam(
ActionsEnvVars.GITHUB_RUN_ATTEMPT,
);
const workflowRunAttempt = parseInt(workflowRunAttemptString, 10);
if (Number.isNaN(workflowRunAttempt)) {
throw new Error(
`GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
);
}
if (workflowRunAttempt <= 0) {
throw new Error(
`GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}`,
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must be a positive integer. Current value is ${workflowRunAttemptString}`,
);
}
return workflowRunAttempt;
Expand Down
10 changes: 7 additions & 3 deletions src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import * as core from "@actions/core";
import * as githubUtils from "@actions/github/lib/utils";
import * as retry from "@octokit/plugin-retry";

import { getActionVersion, getRequiredInput } from "./actions-util";
import {
ActionsEnvVars,
getActionVersion,
getRequiredInput,
} from "./actions-util";
import { EnvVar } from "./environment";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
Expand Down Expand Up @@ -70,8 +74,8 @@ function createApiClientWithDetails(
export function getApiDetails(): GitHubApiDetails {
return {
auth: getRequiredInput("token"),
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
url: getRequiredEnvParam(ActionsEnvVars.GITHUB_SERVER_URL),
apiURL: getRequiredEnvParam(ActionsEnvVars.GITHUB_API_URL),
};
}

Expand Down
28 changes: 0 additions & 28 deletions src/config-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,34 +424,6 @@ test.serial("load input outside of workspace", async (t) => {
});
});

test.serial("load non-local input with invalid repo syntax", async (t) => {
return await withTmpDir(async (tempDir) => {
// no filename given, just a repo
const configFile = "octo-org/codeql-config@main";

try {
await configUtils.initConfig(
createFeatures([]),
createTestInitConfigInputs({
configFile,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(
"octo-org/codeql-config@main",
),
),
);
}
});
});

test.serial("load non-existent input", async (t) => {
return await withTmpDir(async (tempDir) => {
const languagesInput = "javascript";
Expand Down
71 changes: 11 additions & 60 deletions src/config-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as yaml from "js-yaml";

import { ActionState } from "./action-common";
import {
getActionVersion,
getOptionalInput,
Expand All @@ -27,6 +28,7 @@ import {
parseUserConfig,
UserConfig,
} from "./config/db-config";
import { getRemoteConfig } from "./config/file";
import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
Expand Down Expand Up @@ -77,6 +79,7 @@ import {
Success,
Failure,
isHostedRunner,
getEnv,
} from "./util";

/**
Expand Down Expand Up @@ -599,12 +602,11 @@ async function downloadCacheWithTime(
}

async function loadUserConfig(
logger: Logger,
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
configFile: string,
workspacePath: string,
apiDetails: api.GitHubApiCombinedDetails,
tempDir: string,
validateConfig: boolean,
): Promise<UserConfig> {
if (isLocal(configFile)) {
if (configFile !== userConfigFromActionPath(tempDir)) {
Expand All @@ -617,14 +619,12 @@ async function loadUserConfig(
);
}
}
return getLocalConfig(logger, configFile, validateConfig);
} else {
return await getRemoteConfig(
logger,
configFile,
apiDetails,
validateConfig,
const validateConfig = await actionState.features.getValue(
Feature.ValidateDbConfig,
);
return getLocalConfig(actionState.logger, configFile, validateConfig);
} else {
return await getRemoteConfig(actionState, configFile, apiDetails);
}
}

Expand Down Expand Up @@ -1160,14 +1160,13 @@ export async function initConfig(
logger.debug("No configuration file was provided");
} else {
logger.debug(`Using configuration file: ${inputs.configFile}`);
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
const actionState = { logger, features, env: getEnv() };
userConfig = await loadUserConfig(
logger,
actionState,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
validateConfig,
);
}

Expand Down Expand Up @@ -1369,54 +1368,6 @@ function getLocalConfig(
);
}

async function getRemoteConfig(
logger: Logger,
configFile: string,
apiDetails: api.GitHubApiCombinedDetails,
validateConfig: boolean,
): Promise<UserConfig> {
// retrieve the various parts of the config location, and ensure they're present
const format = new RegExp(
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
);
const pieces = format.exec(configFile);
// 5 = 4 groups + the whole expression
if (pieces?.groups === undefined || pieces.length < 5) {
throw new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
);
}

const response = await api
.getApiClientWithExternalAuth(apiDetails)
.rest.repos.getContent({
owner: pieces.groups.owner,
repo: pieces.groups.repo,
path: pieces.groups.path,
ref: pieces.groups.ref,
});

let fileContents: string;
if ("content" in response.data && response.data.content !== undefined) {
fileContents = response.data.content;
} else if (Array.isArray(response.data)) {
throw new ConfigurationError(
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
);
} else {
throw new ConfigurationError(
errorMessages.getConfigFileFormatInvalidMessage(configFile),
);
}

return parseUserConfig(
logger,
configFile,
Buffer.from(fileContents, "base64").toString("binary"),
validateConfig,
);
}

/**
* Get the file path where the parsed config will be stored.
*/
Expand Down
Loading
Loading