diff --git a/CHANGELOG.md b/CHANGELOG.md index 56458aecc..0b9c0015b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/entry-points.js b/lib/entry-points.js index c41ed7c5e..713d16001 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -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.` diff --git a/src/config/resolve-tools-input.test.ts b/src/config/resolve-tools-input.test.ts index 00f60c0db..f4a96b3f4 100644 --- a/src/config/resolve-tools-input.test.ts +++ b/src/config/resolve-tools-input.test.ts @@ -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.", + ); +}); diff --git a/src/config/resolve-tools-input.ts b/src/config/resolve-tools-input.ts index c7d890f72..eb739ec7d 100644 --- a/src/config/resolve-tools-input.ts +++ b/src/config/resolve-tools-input.ts @@ -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( diff --git a/src/feature-flags/properties.test.ts b/src/feature-flags/properties.test.ts index 2bb9e10e4..f8b088aa5 100644 --- a/src/feature-flags/properties.test.ts +++ b/src/feature-flags/properties.test.ts @@ -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: {}, diff --git a/src/feature-flags/properties.ts b/src/feature-flags/properties.ts index a80ef436e..b27ee414c 100644 --- a/src/feature-flags/properties.ts +++ b/src/feature-flags/properties.ts @@ -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( Object.values(RepositoryPropertyName), diff --git a/src/init-action.ts b/src/init-action.ts index a1af8b8c8..50422443b 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -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(), diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 79215e477..576903bf6 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -157,7 +157,9 @@ async function run(startedAt: Date): Promise { // 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(),