Implement an enforce/dynamic approach

This commit is contained in:
Óscar San José
2026-06-01 18:27:44 +02:00
parent f22e7a29ce
commit d426d33115
8 changed files with 268 additions and 91 deletions

View File

@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
- Organizations can now create a custom repository property with the name `github-codeql-tools` to set the default CodeQL CLI tools value for dynamic workflows. If a workflow provides an explicit `tools:` input, that input takes precedence. For more information, see [Managing custom properties for repositories in your organization](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization), [Repository properties for Code Scanning](https://docs.github.com/en/code-security/concepts/code-scanning/repository-properties) and [Customizing your advanced setup for code scanning](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning).
- Organizations can create a custom repository property named `github-codeql-tools` to set a default CodeQL CLI tools value. You can optionally set `github-codeql-tools-mode` to control scope: use `enforce` (default) to apply to all workflows, or `dynamic` to apply only to dynamic workflows. If a workflow provides an explicit `tools:` input, that input takes precedence. For more information, see [Managing custom properties for repositories in your organization](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization), [Repository properties for Code Scanning](https://docs.github.com/en/code-security/concepts/code-scanning/repository-properties) and [Customizing your advanced setup for code scanning](https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning).
## 4.36.0 - 22 May 2026

25
lib/entry-points.js generated
View File

@@ -150417,6 +150417,7 @@ var RepositoryPropertyName = /* @__PURE__ */ ((RepositoryPropertyName2) => {
RepositoryPropertyName2["EXTRA_QUERIES"] = "github-codeql-extra-queries";
RepositoryPropertyName2["FILE_COVERAGE_ON_PRS"] = "github-codeql-file-coverage-on-prs";
RepositoryPropertyName2["TOOLS"] = "github-codeql-tools";
RepositoryPropertyName2["TOOLS_MODE"] = "github-codeql-tools-mode";
return RepositoryPropertyName2;
})(RepositoryPropertyName || {});
function isString2(value) {
@@ -150435,7 +150436,11 @@ var repositoryPropertyParsers = {
["github-codeql-disable-overlay" /* DISABLE_OVERLAY */]: booleanProperty,
["github-codeql-extra-queries" /* EXTRA_QUERIES */]: stringProperty,
["github-codeql-file-coverage-on-prs" /* FILE_COVERAGE_ON_PRS */]: booleanProperty,
["github-codeql-tools" /* TOOLS */]: stringProperty
["github-codeql-tools" /* TOOLS */]: stringProperty,
["github-codeql-tools-mode" /* TOOLS_MODE */]: {
validate: isString2,
parse: parseToolsModeRepositoryProperty
}
};
async function loadPropertiesFromApi(logger, repositoryNwo) {
try {
@@ -150529,6 +150534,15 @@ function parseBooleanRepositoryProperty(name, value, logger) {
function parseStringRepositoryProperty(_name, value) {
return value;
}
function parseToolsModeRepositoryProperty(name, value, logger) {
if (value !== "dynamic" /* Dynamic */ && value !== "enforce" /* Enforce */) {
logger.warning(
`Repository property '${name}' has unexpected value '${value}'. Expected 'dynamic' or 'enforce'. Defaulting to 'enforce'.`
);
return "enforce" /* Enforce */;
}
return value;
}
var KNOWN_REPOSITORY_PROPERTY_NAMES = new Set(
Object.values(RepositoryPropertyName)
);
@@ -158732,7 +158746,7 @@ var io7 = __toESM(require_io());
var semver10 = __toESM(require_semver2());
// src/config/resolve-tools-input.ts
function resolveToolsInput(toolsWorkflowInput, _allowRepositoryPropertyFallback, repositoryProperties, logger) {
function resolveToolsInput(toolsWorkflowInput, isDynamicWorkflow2, repositoryProperties, logger) {
if (toolsWorkflowInput) {
logger.info(
`Setting tools: ${toolsWorkflowInput} based on workflow input.`
@@ -158740,6 +158754,13 @@ function resolveToolsInput(toolsWorkflowInput, _allowRepositoryPropertyFallback,
return toolsWorkflowInput;
}
const toolsPropertyValue = repositoryProperties["github-codeql-tools" /* TOOLS */];
const toolsMode = repositoryProperties["github-codeql-tools-mode" /* TOOLS_MODE */] ?? "enforce" /* Enforce */;
if (toolsPropertyValue && toolsMode === "dynamic" /* Dynamic */ && !isDynamicWorkflow2) {
logger.info(
`Ignoring '${"github-codeql-tools" /* TOOLS */}' repository property because '${"github-codeql-tools-mode" /* TOOLS_MODE */}' is set to '${toolsMode}' and this is not a dynamic workflow.`
);
return void 0;
}
if (toolsPropertyValue) {
logger.info(
`Setting tools: ${toolsPropertyValue} based on the '${"github-codeql-tools" /* TOOLS */}' repository property.`

View File

@@ -1,7 +1,10 @@
import test from "ava";
import { resolveToolsInput } from "../config/resolve-tools-input";
import { RepositoryPropertyName } from "../feature-flags/properties";
import {
RepositoryPropertyName,
ToolsModeRepositoryPropertyValue,
} from "../feature-flags/properties";
import type { RepositoryProperties } from "../feature-flags/properties";
import {
getRecordingLogger,
@@ -15,7 +18,7 @@ test("resolveToolsInput returns undefined when no tools input or repository prop
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const result = resolveToolsInput(undefined, true, {}, logger);
const result = resolveToolsInput(undefined, false, {}, logger);
t.is(result, undefined);
t.is(loggedMessages.length, 0);
@@ -25,7 +28,7 @@ test("resolveToolsInput returns workflow input when only workflow input is provi
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const result = resolveToolsInput("latest", true, {}, logger);
const result = resolveToolsInput("latest", false, {}, logger);
t.is(result, "latest");
t.is(loggedMessages.length, 1);
@@ -39,85 +42,6 @@ test("resolveToolsInput returns repository property when only repository propert
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput(
undefined,
true,
repositoryProperties,
logger,
);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput prioritizes workflow input over repository property", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput(
"nightly",
true,
repositoryProperties,
logger,
);
t.is(result, "nightly");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: nightly based on workflow input.",
);
});
test("resolveToolsInput treats empty string workflow input as not set", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput("", true, repositoryProperties, logger);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput returns undefined when repository property is undefined", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: undefined,
};
const result = resolveToolsInput(
undefined,
true,
repositoryProperties,
logger,
);
t.is(result, undefined);
t.is(loggedMessages.length, 0);
});
test("resolveToolsInput returns repository property when fallback is disabled", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
@@ -136,7 +60,86 @@ test("resolveToolsInput returns repository property when fallback is disabled",
);
});
test("resolveToolsInput does not log when fallback is disabled and repository property is not set", (t) => {
test("resolveToolsInput prioritizes workflow input over repository property", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput(
"nightly",
false,
repositoryProperties,
logger,
);
t.is(result, "nightly");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: nightly based on workflow input.",
);
});
test("resolveToolsInput treats empty string workflow input as not set", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput("", false, repositoryProperties, logger);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput returns undefined when repository property is undefined", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: undefined,
};
const result = resolveToolsInput(
undefined,
false,
repositoryProperties,
logger,
);
t.is(result, undefined);
t.is(loggedMessages.length, 0);
});
test("resolveToolsInput returns repository property when workflow input is not set", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
};
const result = resolveToolsInput(
undefined,
false,
repositoryProperties,
logger,
);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput does not log when workflow input and repository property are not set", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
@@ -145,3 +148,70 @@ test("resolveToolsInput does not log when fallback is disabled and repository pr
t.is(result, undefined);
t.is(loggedMessages.length, 0);
});
test("resolveToolsInput applies tools property in enforce mode for static workflows", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
[RepositoryPropertyName.TOOLS_MODE]:
ToolsModeRepositoryPropertyValue.Enforce,
};
const result = resolveToolsInput(
undefined,
false,
repositoryProperties,
logger,
);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput applies tools property in dynamic mode for dynamic workflows", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
[RepositoryPropertyName.TOOLS_MODE]:
ToolsModeRepositoryPropertyValue.Dynamic,
};
const result = resolveToolsInput(undefined, true, repositoryProperties, logger);
t.is(result, "toolcache");
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
);
});
test("resolveToolsInput ignores tools property in dynamic mode for static workflows", (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const repositoryProperties: RepositoryProperties = {
[RepositoryPropertyName.TOOLS]: "toolcache",
[RepositoryPropertyName.TOOLS_MODE]:
ToolsModeRepositoryPropertyValue.Dynamic,
};
const result = resolveToolsInput(
undefined,
false,
repositoryProperties,
logger,
);
t.is(result, undefined);
t.is(loggedMessages.length, 1);
t.is(
loggedMessages[0].message,
"Ignoring 'github-codeql-tools' repository property because 'github-codeql-tools-mode' is set to 'dynamic' and this is not a dynamic workflow.",
);
});

View File

@@ -1,23 +1,26 @@
import {
RepositoryProperties,
RepositoryPropertyName,
ToolsModeRepositoryPropertyValue,
} from "../feature-flags/properties";
import { Logger } from "../logging";
/**
* Resolves the effective tools input by combining the workflow input and repository properties.
* The explicit `tools` workflow input takes precedence. If none is provided,
* falls back to the repository property (if set and enabled for this workflow).
* falls back to the repository property (if set). The optional
* `github-codeql-tools-mode` repository property controls whether this fallback
* applies to all workflows (`enforce`) or only dynamic workflows (`dynamic`).
*
* @param toolsWorkflowInput - The value of the `tools` workflow input, if provided.
* @param _allowRepositoryPropertyFallback - Reserved for backwards compatibility.
* @param isDynamicWorkflow - Whether the current workflow is dynamic.
* @param repositoryProperties - The parsed repository properties.
* @param logger - Logger for outputting resolution messages.
* @returns The effective tools input value.
*/
export function resolveToolsInput(
toolsWorkflowInput: string | undefined,
_allowRepositoryPropertyFallback: boolean,
isDynamicWorkflow: boolean,
repositoryProperties: RepositoryProperties,
logger: Logger,
): string | undefined {
@@ -29,6 +32,20 @@ export function resolveToolsInput(
}
const toolsPropertyValue = repositoryProperties[RepositoryPropertyName.TOOLS];
const toolsMode =
repositoryProperties[RepositoryPropertyName.TOOLS_MODE] ??
ToolsModeRepositoryPropertyValue.Enforce;
if (
toolsPropertyValue &&
toolsMode === ToolsModeRepositoryPropertyValue.Dynamic &&
!isDynamicWorkflow
) {
logger.info(
`Ignoring '${RepositoryPropertyName.TOOLS}' repository property because '${RepositoryPropertyName.TOOLS_MODE}' is set to '${toolsMode}' and this is not a dynamic workflow.`,
);
return undefined;
}
if (toolsPropertyValue) {
logger.info(

View File

@@ -79,6 +79,7 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => {
data: [
{ property_name: "github-codeql-extra-queries", value: "+queries" },
{ property_name: "github-codeql-tools", value: "toolcache" },
{ property_name: "github-codeql-tools-mode", value: "dynamic" },
{ property_name: "unknown-property", value: "something" },
] satisfies properties.GitHubPropertiesResponse,
});
@@ -91,9 +92,42 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => {
t.deepEqual(response, {
"github-codeql-extra-queries": "+queries",
"github-codeql-tools": "toolcache",
"github-codeql-tools-mode": "dynamic",
});
});
test.serial(
"loadPropertiesFromApi warns if tools mode property has unexpected value",
async (t) => {
sinon.stub(api, "getRepositoryProperties").resolves({
headers: {},
status: 200,
url: "",
data: [
{
property_name: "github-codeql-tools-mode",
value: "all",
},
] satisfies properties.GitHubPropertiesResponse,
});
const logger = getRunnerLogger(true);
const warningSpy = sinon.spy(logger, "warning");
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
const response = await properties.loadPropertiesFromApi(
logger,
mockRepositoryNwo,
);
t.deepEqual(response, {
"github-codeql-tools-mode": "enforce",
});
t.true(warningSpy.calledOnce);
t.is(
warningSpy.firstCall.args[0],
"Repository property 'github-codeql-tools-mode' has unexpected value 'all'. Expected 'dynamic' or 'enforce'. Defaulting to 'enforce'.",
);
},
);
test.serial("loadPropertiesFromApi parses true boolean property", async (t) => {
sinon.stub(api, "getRepositoryProperties").resolves({
headers: {},

View File

@@ -17,6 +17,12 @@ export enum RepositoryPropertyName {
EXTRA_QUERIES = "github-codeql-extra-queries",
FILE_COVERAGE_ON_PRS = "github-codeql-file-coverage-on-prs",
TOOLS = "github-codeql-tools",
TOOLS_MODE = "github-codeql-tools-mode",
}
export enum ToolsModeRepositoryPropertyValue {
Dynamic = "dynamic",
Enforce = "enforce",
}
/** Parsed types of the known repository properties. */
@@ -25,6 +31,7 @@ export type AllRepositoryProperties = {
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
[RepositoryPropertyName.TOOLS]: string;
[RepositoryPropertyName.TOOLS_MODE]: ToolsModeRepositoryPropertyValue;
};
/** Parsed repository properties. */
@@ -36,6 +43,7 @@ export type RepositoryPropertyApiType = {
[RepositoryPropertyName.EXTRA_QUERIES]: string;
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: string;
[RepositoryPropertyName.TOOLS]: string;
[RepositoryPropertyName.TOOLS_MODE]: string;
};
/** The type of functions which take the `value` from the API and try to convert it to the type we want. */
@@ -84,6 +92,10 @@ const repositoryPropertyParsers: {
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
[RepositoryPropertyName.TOOLS]: stringProperty,
[RepositoryPropertyName.TOOLS_MODE]: {
validate: isString,
parse: parseToolsModeRepositoryProperty,
},
};
/**
@@ -256,6 +268,25 @@ function parseStringRepositoryProperty(_name: string, value: string): string {
return value;
}
/** Parse the tools mode repository property. */
function parseToolsModeRepositoryProperty(
name: string,
value: string,
logger: Logger,
): ToolsModeRepositoryPropertyValue {
if (
value !== ToolsModeRepositoryPropertyValue.Dynamic &&
value !== ToolsModeRepositoryPropertyValue.Enforce
) {
logger.warning(
`Repository property '${name}' has unexpected value '${value}'. Expected 'dynamic' or 'enforce'. Defaulting to 'enforce'.`,
);
return ToolsModeRepositoryPropertyValue.Enforce;
}
return value;
}
/** Set of known repository property names, for fast lookups. */
const KNOWN_REPOSITORY_PROPERTY_NAMES = new Set<string>(
Object.values(RepositoryPropertyName),

View File

@@ -298,7 +298,9 @@ async function run(startedAt: Date) {
// Determine the effective tools input.
// The explicit `tools` workflow input takes precedence. If none is provided,
// fall back to the 'github-codeql-tools' repository property (if set) only for dynamic workflows.
// fall back to the 'github-codeql-tools' repository property (if set).
// If 'github-codeql-tools-mode' is set to 'dynamic', this fallback applies
// only to dynamic workflows. Otherwise, it applies to all workflows.
effectiveToolsInput = resolveToolsInput(
getOptionalInput("tools"),
isDynamicWorkflow(),

View File

@@ -157,7 +157,9 @@ async function run(startedAt: Date): Promise<void> {
// Determine the effective tools input.
// The explicit `tools` workflow input takes precedence. If none is provided,
// fall back to the 'github-codeql-tools' repository property (if set) only for dynamic workflows.
// fall back to the 'github-codeql-tools' repository property (if set).
// If 'github-codeql-tools-mode' is set to 'dynamic', this fallback applies
// only to dynamic workflows. Otherwise, it applies to all workflows.
effectiveToolsInput = resolveToolsInput(
getOptionalInput("tools"),
isDynamicWorkflow(),