mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +00:00
Merge branch 'main' into mbg/config/merge
This commit is contained in:
@@ -6,6 +6,11 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.0 - 08 Jul 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
|
||||
- In addition to the existing input format, the `config-file` input for the `codeql-action/init` step will soon support a new `[owner/]repo[@ref][:path]` format. All components except the repository name are optional. If omitted, `owner` defaults to the same owner as the repository the analysis is running for, `ref` to `main`, and `path` to `.github/codeql-action.yaml`. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. [#3973](https://github.com/github/codeql-action/pull/3973)
|
||||
|
||||
## 4.36.3 - 01 Jul 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
}
|
||||
|
||||
1840
lib/entry-points.js
generated
1840
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.36.4",
|
||||
"version": "4.37.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codeql",
|
||||
"version": "4.36.4",
|
||||
"version": "4.37.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks"
|
||||
@@ -8896,9 +8896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/supertap/node_modules/js-yaml": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
|
||||
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9360,9 +9360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.36.4",
|
||||
"version": "4.37.1",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
DEFAULT_ACTIONS_VARS,
|
||||
initAllState,
|
||||
callee,
|
||||
} from "./testing-utils";
|
||||
import {
|
||||
GitHubVariant,
|
||||
@@ -162,8 +164,9 @@ test.serial("load empty config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
@@ -204,8 +207,9 @@ test.serial("load code quality config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -282,9 +286,10 @@ test.serial(
|
||||
repositoryProperties,
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
await t.notThrowsAsync(async () => {
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -323,8 +328,9 @@ test.serial("loading a saved config produces the same config", async (t) => {
|
||||
// Sanity check that getConfig returns undefined before we have called initConfig
|
||||
t.deepEqual(await configUtils.getConfig(tempDir, logger), undefined);
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config1 = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -375,8 +381,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
.stub(actionsUtil, "getActionVersion")
|
||||
.returns("does-not-exist");
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -404,8 +411,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
test.serial("load input outside of workspace", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: "../input",
|
||||
tempDir,
|
||||
@@ -426,34 +434,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";
|
||||
@@ -461,8 +441,9 @@ test.serial("load non-existent input", async (t) => {
|
||||
t.false(fs.existsSync(path.join(tempDir, configFile)));
|
||||
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -544,8 +525,9 @@ test.serial("load non-empty input", async (t) => {
|
||||
const languagesInput = "javascript";
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);
|
||||
|
||||
const state = initAllState();
|
||||
const actualConfig = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
buildModeInput: "none",
|
||||
@@ -601,8 +583,9 @@ test.serial(
|
||||
// Only JS, python packs will be ignored
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState({ env: util.getEnv() });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile: configFilePath,
|
||||
@@ -653,8 +636,9 @@ test.serial("API client used when reading remote config", async (t) => {
|
||||
const configFile = "octo-org/codeql-config/config.yaml@main";
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -675,9 +659,10 @@ test.serial(
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -705,9 +690,10 @@ test.serial("Invalid format of remote config handled correctly", async (t) => {
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -735,9 +721,10 @@ test.serial("No detected languages", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
tempDir,
|
||||
codeql,
|
||||
@@ -758,9 +745,10 @@ test.serial("Unknown languages", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const languagesInput = "rubbish,english";
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
tempDir,
|
||||
@@ -2281,23 +2269,22 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
|
||||
|
||||
test("determineUserConfig - empty config when neither input is specified", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = util.getEnv(DEFAULT_ACTIONS_VARS);
|
||||
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([]),
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withEnv(util.getEnv(DEFAULT_ACTIONS_VARS))
|
||||
.withFeatures([])
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
// The returned configuration should be empty.
|
||||
t.deepEqual(result, {});
|
||||
await target.passes(async (fn) => t.deepEqual(await fn(), {}));
|
||||
|
||||
const logger = target.getLogger();
|
||||
// And the fact that no configuration was provided should have been logged,
|
||||
// but not the messages for the two input sources.
|
||||
t.true(logger.hasMessage("No configuration file was provided"));
|
||||
@@ -2324,9 +2311,7 @@ test("determineUserConfig - loads config file", async (t) => {
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([]),
|
||||
initAllState({ logger, env }),
|
||||
tmpDir,
|
||||
inputs,
|
||||
);
|
||||
@@ -2364,9 +2349,7 @@ test("determineUserConfig - loads config input", async (t) => {
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([]),
|
||||
initAllState({ logger, env }),
|
||||
tmpDir,
|
||||
inputs,
|
||||
);
|
||||
@@ -2407,9 +2390,7 @@ test("determineUserConfig - ignores config file input when both specified", asyn
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([]),
|
||||
initAllState({ logger, env }),
|
||||
tmpDir,
|
||||
inputs,
|
||||
);
|
||||
@@ -2458,9 +2439,11 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([Feature.AllowMergeConfigFiles]),
|
||||
initAllState({
|
||||
logger,
|
||||
env,
|
||||
features: createFeatures([Feature.AllowMergeConfigFiles]),
|
||||
}),
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: defaultSetupConfigInput,
|
||||
@@ -2515,9 +2498,7 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([]),
|
||||
initAllState({ logger, env }),
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
@@ -2558,9 +2539,11 @@ test("determineUserConfig - ignores config file input outside Default Setup if F
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const result = await configUtils.determineUserConfig(
|
||||
logger,
|
||||
env,
|
||||
createFeatures([Feature.AllowMergeConfigFiles]),
|
||||
initAllState({
|
||||
logger,
|
||||
env,
|
||||
features: createFeatures([Feature.AllowMergeConfigFiles]),
|
||||
}),
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
|
||||
@@ -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,
|
||||
@@ -19,8 +20,9 @@ import {
|
||||
getAnalysisConfig,
|
||||
} from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind, getCachingKind } from "./caching-utils";
|
||||
import { getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config/action-config";
|
||||
import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
@@ -29,6 +31,12 @@ import {
|
||||
parseUserConfig,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import { getRemoteConfig } from "./config/file";
|
||||
import {
|
||||
parseRegistries,
|
||||
type RegistryConfigNoCredentials,
|
||||
type RegistryConfigWithCredentials,
|
||||
} from "./config/pack-registries";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
@@ -82,6 +90,8 @@ import {
|
||||
getEnv,
|
||||
} from "./util";
|
||||
|
||||
export { type Config } from "./config/action-config";
|
||||
|
||||
/**
|
||||
* The minimum available disk space (in MB) required to perform overlay analysis.
|
||||
* If the available disk space on the runner is below the threshold when deciding
|
||||
@@ -120,148 +130,6 @@ const OVERLAY_MINIMUM_MEMORY_MB = 5 * 1024;
|
||||
*/
|
||||
const CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE = "2.24.3";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format of the parsed config file.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
|
||||
async function getSupportedLanguageMap(
|
||||
codeql: CodeQL,
|
||||
logger: Logger,
|
||||
@@ -613,12 +481,11 @@ async function downloadCacheWithTime(
|
||||
* @returns The loaded configuration file, if successful.
|
||||
*/
|
||||
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)) {
|
||||
@@ -631,14 +498,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,13 +1024,13 @@ export async function applyIncrementalAnalysisSettings(
|
||||
* was specified.
|
||||
*/
|
||||
export async function determineUserConfig(
|
||||
logger: Logger,
|
||||
env: Env,
|
||||
features: FeatureEnablement,
|
||||
action: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
tempDir: string,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<UserConfig> {
|
||||
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
|
||||
const validateConfig = await action.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
|
||||
// We have the following cases:
|
||||
// 1. A `config` or `config-file` input is provided, but not both: use the provided one.
|
||||
@@ -1179,40 +1044,39 @@ export async function determineUserConfig(
|
||||
// merge supported configuration file properties is enabled. We only execute
|
||||
// this lazily if the other checks pass.
|
||||
const allowMergeConfigs = () =>
|
||||
features.getValue(Feature.AllowMergeConfigFiles);
|
||||
action.features.getValue(Feature.AllowMergeConfigFiles);
|
||||
|
||||
// Check whether we also have a `config-file` input and decide what to do.
|
||||
if (
|
||||
inputs.configFile &&
|
||||
isDefaultSetup(env) &&
|
||||
isDefaultSetup(action.env) &&
|
||||
(await allowMergeConfigs())
|
||||
) {
|
||||
// If the FF is enabled and we are in Default Setup, combine the supported
|
||||
// configuration file properties and write the result to disk.
|
||||
const fromConfigInput = parseUserConfig(
|
||||
logger,
|
||||
action.logger,
|
||||
"`config` input",
|
||||
inputs.configInput,
|
||||
validateConfig,
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
logger,
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
validateConfig,
|
||||
);
|
||||
|
||||
// Write the merged configuration to disk so that it can be loaded subsequently by
|
||||
// the CLI or other CodeQL Action steps.
|
||||
const mergedConfig = mergeUserConfigs(
|
||||
logger,
|
||||
action.logger,
|
||||
fromConfigInput,
|
||||
fromConfigFile,
|
||||
);
|
||||
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
|
||||
logger.debug(
|
||||
action.logger.debug(
|
||||
`Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`,
|
||||
);
|
||||
|
||||
@@ -1223,7 +1087,7 @@ export async function determineUserConfig(
|
||||
// we didn't meet the conditions for merging the configurations. Warn the user
|
||||
// that the configuration file will be ignored.
|
||||
if (inputs.configFile) {
|
||||
logger.warning(
|
||||
action.logger.warning(
|
||||
`Both a config file and config input were provided. Ignoring config file.`,
|
||||
);
|
||||
}
|
||||
@@ -1231,23 +1095,24 @@ export async function determineUserConfig(
|
||||
// Write the `config` input straight to disk.
|
||||
fs.writeFileSync(computedConfigPath, inputs.configInput);
|
||||
inputs.configFile = computedConfigPath;
|
||||
logger.debug(`Using config from action input: ${inputs.configFile}`);
|
||||
action.logger.debug(
|
||||
`Using config from action input: ${inputs.configFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load whatever configuration file we have, if any.
|
||||
if (!inputs.configFile) {
|
||||
logger.debug("No configuration file was provided");
|
||||
action.logger.debug("No configuration file was provided");
|
||||
return {};
|
||||
} else {
|
||||
logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
return await loadUserConfig(
|
||||
logger,
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1259,18 +1124,13 @@ export async function determineUserConfig(
|
||||
* a default config. The parsed config is then stored to a known location.
|
||||
*/
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<Config> {
|
||||
const { logger, tempDir } = inputs;
|
||||
const { logger, features } = actionState;
|
||||
const { tempDir } = inputs;
|
||||
|
||||
const userConfig = await determineUserConfig(
|
||||
logger,
|
||||
getEnv(),
|
||||
features,
|
||||
tempDir,
|
||||
inputs,
|
||||
);
|
||||
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
|
||||
|
||||
const config = await initActionState(inputs, userConfig);
|
||||
|
||||
@@ -1418,29 +1278,6 @@ export async function initConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
}
|
||||
|
||||
function isLocal(configPath: string): boolean {
|
||||
// If the path starts with ./, look locally
|
||||
if (configPath.indexOf("./") === 0) {
|
||||
@@ -1470,54 +1307,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.
|
||||
*/
|
||||
|
||||
128
src/config/action-config.ts
Normal file
128
src/config/action-config.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { AnalysisKind } from "../analyses";
|
||||
import type { CachingKind } from "../caching-utils";
|
||||
import type { RepositoryProperties } from "../feature-flags/properties";
|
||||
import type { Language } from "../languages";
|
||||
import type { OverlayDatabaseMode } from "../overlay/overlay-database-mode";
|
||||
import type { BuildMode, GitHubVersion } from "../util";
|
||||
|
||||
import type { ExcludeQueryFilter, UserConfig } from "./db-config";
|
||||
|
||||
/**
|
||||
* Format of the CodeQL Action configuration state that is persisted
|
||||
* between steps of the CodeQL Action in a CodeQL workflow.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import * as api from "../api-client";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
@@ -46,3 +52,52 @@ export async function getConfigFileInput(
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch a `UserConfig` from a remote `address`.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The remote address of the configuration file.
|
||||
* @param apiDetails Information about how to connect to the API.
|
||||
*
|
||||
* @returns The `UserConfig`, if it could be fetched and parsed successfully.
|
||||
*/
|
||||
export async function getRemoteConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
): Promise<UserConfig> {
|
||||
const address = await parseRemoteFileAddress(actionState, configFile);
|
||||
|
||||
const response = await api
|
||||
.getApiClientWithExternalAuth(apiDetails)
|
||||
.rest.repos.getContent({
|
||||
owner: address.owner,
|
||||
repo: address.repo,
|
||||
path: address.path,
|
||||
ref: address.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),
|
||||
);
|
||||
}
|
||||
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return parseUserConfig(
|
||||
actionState.logger,
|
||||
configFile,
|
||||
Buffer.from(fileContents, "base64").toString("binary"),
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
|
||||
49
src/config/pack-registries.ts
Normal file
49
src/config/pack-registries.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
export function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
}
|
||||
282
src/config/remote-file.test.ts
Normal file
282
src/config/remote-file.test.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionsEnvVars } from "../actions-util";
|
||||
import * as errors from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { callee, getTestEnv } from "../testing-utils";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import {
|
||||
DEFAULT_CONFIG_FILE_NAME,
|
||||
DEFAULT_CONFIG_FILE_REF,
|
||||
parseRemoteFileAddress,
|
||||
RemoteFileAddress,
|
||||
} from "./remote-file";
|
||||
|
||||
type ParseRemoteFileAddressTest = {
|
||||
input: string;
|
||||
expected: RemoteFileAddress;
|
||||
};
|
||||
|
||||
test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const expected: RemoteFileAddress = {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: "ref",
|
||||
};
|
||||
|
||||
const oldFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo/path@ref", expected },
|
||||
{ input: "owner /repo/path@ref", expected },
|
||||
{ input: "owner/ repo/path@ref", expected },
|
||||
{ input: "owner/repo /path@ref", expected },
|
||||
{ input: "owner/repo/ path@ref", expected },
|
||||
{ input: "owner/repo/path @ref", expected },
|
||||
{ input: "owner/repo/path@ ref", expected },
|
||||
{
|
||||
input: "owner/repo/path/to/codeql.yml@ref/feature",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo/path/to/codeql.yml@ref/feature ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const oldFormatInput of oldFormatInputs) {
|
||||
await target
|
||||
.withArgs(oldFormatInput.input)
|
||||
.passes(async (fn) => t.deepEqual(await fn(), oldFormatInput.expected));
|
||||
}
|
||||
|
||||
// New format.
|
||||
const newFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo@ref:path", expected },
|
||||
{ input: "owner /repo@ref:path", expected },
|
||||
{ input: "owner/ repo@ref:path", expected },
|
||||
{ input: "owner/repo @ref:path", expected },
|
||||
{ input: "owner/repo@ ref:path", expected },
|
||||
{ input: "owner/repo@ref :path", expected },
|
||||
{ input: "owner/repo@ref: path", expected },
|
||||
{
|
||||
input: "owner/repo@ref/feature:path/to/codeql.yml",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo@ref/feature:path/to/codeql.yml ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const newFormatInput of newFormatInputs) {
|
||||
const targetWithArgs = target.withArgs(newFormatInput.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), newFormatInput.expected));
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const env = target.getState().env;
|
||||
const owner = "test-owner";
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
|
||||
const targetWithEnv = target.withEnv(env);
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "repo@ref:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo@ref",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = targetWithEnv.withArgs(testCase.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress).withArgs("repo@ref");
|
||||
|
||||
const env = target.getState().env;
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`);
|
||||
|
||||
await target
|
||||
.withEnv(env)
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.throwsAsync(fn, { instanceOf: Error }));
|
||||
|
||||
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a path", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "owner/repo@ref",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "owner/repo",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
|
||||
|
||||
// Should only accept the input if the FF is enabled.
|
||||
await target.withFeatures([]).passes(t.throwsAsync);
|
||||
await target
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) =>
|
||||
t.deepEqual(await fn(), {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
} satisfies RemoteFileAddress),
|
||||
);
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress rejects invalid values", async (t) => {
|
||||
const env = getTestEnv();
|
||||
const owner = "owner";
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
|
||||
const target = callee(parseRemoteFileAddress).withEnv(env);
|
||||
|
||||
const testInputs = [
|
||||
" ",
|
||||
"repo//absolute",
|
||||
"repo:/absolute",
|
||||
"/repo@ref",
|
||||
" /repo@ref",
|
||||
"repo@",
|
||||
"repo:",
|
||||
"repo/",
|
||||
"/repo",
|
||||
":path",
|
||||
"@ref",
|
||||
"@ref:path",
|
||||
"owner/@ref:path",
|
||||
"owner/@ref",
|
||||
"owner/:path",
|
||||
];
|
||||
|
||||
for (const testInput of testInputs) {
|
||||
const targetWithArgs = target.withArgs(testInput);
|
||||
|
||||
// Should throw both when the new format is and isn't accepted.
|
||||
await targetWithArgs.withFeatures([]).passes(async (fn) =>
|
||||
t.throwsAsync(fn, {
|
||||
instanceOf: ConfigurationError,
|
||||
message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
|
||||
}),
|
||||
);
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, {
|
||||
// When the new format is accepted, there are some more specific
|
||||
// errors in some cases. It is sufficient for us to check that
|
||||
// an exception is thrown.
|
||||
instanceOf: ConfigurationError,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
139
src/config/remote-file.ts
Normal file
139
src/config/remote-file.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { ActionsEnvVars } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { ConfigurationError, Failure, Result, Success } from "../util";
|
||||
|
||||
/** Represents remote file addresses. */
|
||||
export interface RemoteFileAddress {
|
||||
/** The owner of the repository. */
|
||||
owner: string;
|
||||
/** The repository name. */
|
||||
repo: string;
|
||||
/** The path of the file. */
|
||||
path: string;
|
||||
/** The ref of the repository. */
|
||||
ref: string;
|
||||
}
|
||||
|
||||
/** The default file path to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
|
||||
/** The default ref to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */
|
||||
function getDefaultOwner(env: Env): string {
|
||||
const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY);
|
||||
const nwoParts = currentRepoNwo.split("/");
|
||||
|
||||
if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) {
|
||||
// This shouldn't happen, so we should throw if `GITHUB_REPOSITORY` doesn't match
|
||||
// our expectations.
|
||||
throw new Error(
|
||||
`Expected ${ActionsEnvVars.GITHUB_REPOSITORY} to contain a name with owner, but got '${currentRepoNwo}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return nwoParts[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* The old remote address format that's always been supported for the `config-file` input.
|
||||
* All the components are required. Unchanged from the previous implementation.
|
||||
*/
|
||||
const OLD_REMOTE_ADDRESS_FORMAT = new RegExp(
|
||||
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
|
||||
);
|
||||
|
||||
/**
|
||||
* Attempts to parse `input` as a `RemoteFileAddress` using the old format.
|
||||
*
|
||||
* @param input The input to try and parse.
|
||||
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
|
||||
*/
|
||||
function parseOldRemoteFileAddress(
|
||||
input: string,
|
||||
): Result<RemoteFileAddress, undefined> {
|
||||
const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input);
|
||||
|
||||
// 5 = 4 groups + the whole expression
|
||||
if (pieces?.groups === undefined || pieces.length < 5) {
|
||||
return new Failure(undefined);
|
||||
}
|
||||
|
||||
return new Success({
|
||||
owner: pieces.groups.owner.trim(),
|
||||
repo: pieces.groups.repo.trim(),
|
||||
path: pieces.groups.path.trim(),
|
||||
ref: pieces.groups.ref.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The string to try and parse.
|
||||
* @returns The successful result of executing the regex.
|
||||
* @throws `ConfigurationError` if the format of `configFile` is not valid.
|
||||
*/
|
||||
export async function parseRemoteFileAddress(
|
||||
actionState: ActionState<["FeatureFlags", "Env"]>,
|
||||
configFile: string,
|
||||
): Promise<RemoteFileAddress> {
|
||||
// Try to parse the input using the old format. If successful, return the
|
||||
// resulting `RemoteFileAddress`. Otherwise, continue using the new format.
|
||||
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
|
||||
|
||||
if (oldFormatAddressResult.isSuccess()) {
|
||||
return oldFormatAddressResult.value;
|
||||
}
|
||||
|
||||
// If the FF for the new format is not enabled, throw the old format error.
|
||||
const allowNewFormat = await actionState.features.getValue(
|
||||
Feature.NewRemoteFileAddresses,
|
||||
);
|
||||
if (!allowNewFormat) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoOldFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
|
||||
);
|
||||
const pieces = format.exec(configFile.trim());
|
||||
|
||||
const repo: string | undefined = pieces?.groups?.repo?.trim();
|
||||
|
||||
// Check that the regular expression matched and that we have at least the repo name.
|
||||
if (!pieces?.groups || !repo || repo.length === 0) {
|
||||
// Neither the old format nor the new format worked. Throw an error that
|
||||
// explains the format we accept. We only mention the new format, since that's
|
||||
// what we want to be used going forward.
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const owner: string | undefined = pieces.groups.owner?.trim();
|
||||
const path: string | undefined = pieces.groups.path?.trim();
|
||||
const ref: string | undefined = pieces.groups.ref?.trim();
|
||||
|
||||
// Ensure that the path is a relative path.
|
||||
if (path?.startsWith("/")) {
|
||||
throw new ConfigurationError(
|
||||
`The path component of '${configFile}' cannot be an absolute path.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
owner: owner || getDefaultOwner(actionState.env),
|
||||
repo,
|
||||
path: path || DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: ref || DEFAULT_CONFIG_FILE_REF,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function getInvalidConfigFileMessage(
|
||||
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
export function getConfigFileRepoOldFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
@@ -39,6 +39,15 @@ export function getConfigFileRepoFormatInvalidMessage(
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
error += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileFormatInvalidMessage(configFile: string): string {
|
||||
return `The configuration file "${configFile}" could not be read`;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,8 @@ export enum Feature {
|
||||
ForceNightly = "force_nightly",
|
||||
IgnoreGeneratedFiles = "ignore_generated_files",
|
||||
JavaNetworkDebugging = "java_network_debugging",
|
||||
/** Allow the new remote file address format. */
|
||||
NewRemoteFileAddresses = "new_remote_file_addresses",
|
||||
OverlayAnalysis = "overlay_analysis",
|
||||
OverlayAnalysisCodeScanningCpp = "overlay_analysis_code_scanning_cpp",
|
||||
OverlayAnalysisCodeScanningCsharp = "overlay_analysis_code_scanning_csharp",
|
||||
@@ -262,6 +264,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_JAVA_NETWORK_DEBUGGING",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.NewRemoteFileAddresses]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_NEW_REMOTE_FILE_ADDRESSES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysis]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",
|
||||
|
||||
@@ -203,7 +203,7 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(actionState: ActionState<["Logger", "Actions"]>) {
|
||||
async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
@@ -262,8 +262,9 @@ async function run(actionState: ActionState<["Logger", "Actions"]>) {
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFile = await getConfigFileInput(
|
||||
{ ...actionState, features },
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
@@ -363,7 +364,7 @@ async function run(actionState: ActionState<["Logger", "Actions"]>) {
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
config = await initConfig(features, {
|
||||
config = await initConfig(actionStateWithFeatures, {
|
||||
analysisKinds,
|
||||
languagesInput: getOptionalInput("languages"),
|
||||
queriesInput: getOptionalInput("queries"),
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getOptionalInput,
|
||||
isAnalyzingPullRequest,
|
||||
@@ -81,11 +82,11 @@ export async function initCodeQL(
|
||||
}
|
||||
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: configUtils.InitConfigInputs,
|
||||
): Promise<configUtils.Config> {
|
||||
return await withGroupAsync("Load language configuration", async () => {
|
||||
return await configUtils.initConfig(features, inputs);
|
||||
return await configUtils.initConfig(actionState, inputs);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ const testSchema = {
|
||||
requiredKey: json.string,
|
||||
};
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
const optionalOrNullSchema = {
|
||||
optionalKey: json.optionalOrNull(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - required properties are required", async (t) => {
|
||||
@@ -28,13 +28,36 @@ test("validateSchema - required properties are required", async (t) => {
|
||||
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
|
||||
});
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
test("validateSchema - optionalOrNullSchema properties are optional or null", async (t) => {
|
||||
// Optional fields may be absent
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, {}));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: null }));
|
||||
|
||||
// But, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: false }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: true }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: [] }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: {} }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
// Optional fields may be absent or explicitly undefined
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
|
||||
// But should reject null
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
|
||||
// And, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
|
||||
|
||||
@@ -30,6 +30,11 @@ export function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is a number. */
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === "number";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is either a string or undefined. */
|
||||
export function isStringOrUndefined(
|
||||
value: unknown,
|
||||
@@ -55,8 +60,17 @@ export const string = {
|
||||
required: true,
|
||||
} as const satisfies Validator<string>;
|
||||
|
||||
/** Transforms a validator to be optional. */
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
/** A validator for number fields in schemas. */
|
||||
export const number = {
|
||||
validate: isNumber,
|
||||
required: true,
|
||||
} as const satisfies Validator<number>;
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` or `null` for an
|
||||
* absent value.
|
||||
*/
|
||||
export function optionalOrNull<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return val === undefined || val === null || validator.validate(val);
|
||||
@@ -65,6 +79,19 @@ export function optional<T>(validator: Validator<T>) {
|
||||
} as const satisfies Validator<T | undefined | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` for an absent
|
||||
* value but, unlike `optionalOrNull`, rejecting `null`.
|
||||
*/
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown): val is T | undefined => {
|
||||
return val === undefined || validator.validate(val);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined>;
|
||||
}
|
||||
|
||||
/** Represents an arbitrary object schema. */
|
||||
export type Schema = Record<string, Validator<any>>;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export type RawCredential = UnvalidatedObject<Credential>;
|
||||
/** A schema for credential objects with a username. */
|
||||
export const usernameSchema = {
|
||||
/** The username needed to authenticate to the package registry, if any. */
|
||||
username: json.optional(json.string),
|
||||
username: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Usernames may be present for both authentication with tokens or passwords. */
|
||||
@@ -29,7 +29,7 @@ export function hasUsername(config: AuthConfig): config is Username {
|
||||
/** A schema for credential objects with a username and password. */
|
||||
export const usernamePasswordSchema = {
|
||||
/** The password needed to authenticate to the package registry, if any. */
|
||||
password: json.optional(json.string),
|
||||
password: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -52,7 +52,7 @@ export function hasUsernameAndPassword(
|
||||
/** A schema for credential objects for token-based authentication. */
|
||||
export const tokenSchema = {
|
||||
/** The token needed to authenticate to the package registry, if any. */
|
||||
token: json.optional(json.string),
|
||||
token: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -100,7 +100,7 @@ export const awsConfigSchema = {
|
||||
"role-name": json.string,
|
||||
domain: json.string,
|
||||
"domain-owner": json.string,
|
||||
audience: json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for AWS OIDC. */
|
||||
@@ -116,8 +116,8 @@ export function isAWSConfig(
|
||||
/** A schema for JFrog OIDC configurations. */
|
||||
export const jfrogConfigSchema = {
|
||||
"jfrog-oidc-provider-name": json.string,
|
||||
audience: json.optional(json.string),
|
||||
"identity-mapping-name": json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
"identity-mapping-name": json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for JFrog OIDC. */
|
||||
@@ -150,8 +150,8 @@ export function isCloudsmithConfig(
|
||||
/** A schema for GCP OIDC configurations. */
|
||||
export const gcpConfigSchema = {
|
||||
"workload-identity-provider": json.string,
|
||||
"service-account": json.optional(json.string),
|
||||
audience: json.optional(json.string),
|
||||
"service-account": json.optionalOrNull(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for GCP OIDC. */
|
||||
|
||||
@@ -12,15 +12,16 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import { parseRegistriesWithoutCredentials, type Config } from "./config-utils";
|
||||
import { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import type { Config } from "./config/action-config";
|
||||
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
||||
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { getRef } from "./git-utils";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import type { Logger } from "./logging";
|
||||
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import type { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
ConfigurationError,
|
||||
getRequiredEnvParam,
|
||||
|
||||
@@ -193,6 +193,21 @@ export function getTestActionsEnv(): ActionsEnv {
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
type AllState = ["Logger", "Env", "Actions", "FeatureFlags"];
|
||||
|
||||
/** Initialise a fresh `ActionState<AllState>` value. */
|
||||
export function initAllState(
|
||||
overrides?: Partial<ActionState<AllState>>,
|
||||
): ActionState<AllState> {
|
||||
return {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
logger: new RecordingLogger(),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
features: createFeatures([]),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a function that accepts an `ActionState` for testing in different environments.
|
||||
*/
|
||||
@@ -216,14 +231,7 @@ export class TestEnv<
|
||||
this.state =
|
||||
cloneFrom !== undefined
|
||||
? { ...cloneFrom.state, logger: this.logger }
|
||||
: {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
logger: this.logger,
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
features: createFeatures([]),
|
||||
};
|
||||
: initAllState({ logger: this.logger });
|
||||
}
|
||||
|
||||
private clone(): TestEnv<Args, R, Fs> {
|
||||
|
||||
Reference in New Issue
Block a user