Skip to content

Commit d426d33

Browse files
committed
Implement an enforce/dynamic approach
1 parent f22e7a2 commit d426d33

8 files changed

Lines changed: 194 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
44

55
## [UNRELEASED]
66

7-
- 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).
7+
- 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).
88

99
## 4.36.0 - 22 May 2026
1010

lib/entry-points.js

Lines changed: 23 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config/resolve-tools-input.test.ts

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import test from "ava";
22

33
import { resolveToolsInput } from "../config/resolve-tools-input";
4-
import { RepositoryPropertyName } from "../feature-flags/properties";
4+
import {
5+
RepositoryPropertyName,
6+
ToolsModeRepositoryPropertyValue,
7+
} from "../feature-flags/properties";
58
import type { RepositoryProperties } from "../feature-flags/properties";
69
import {
710
getRecordingLogger,
@@ -15,7 +18,7 @@ test("resolveToolsInput returns undefined when no tools input or repository prop
1518
const loggedMessages: LoggedMessage[] = [];
1619
const logger = getRecordingLogger(loggedMessages);
1720

18-
const result = resolveToolsInput(undefined, true, {}, logger);
21+
const result = resolveToolsInput(undefined, false, {}, logger);
1922

2023
t.is(result, undefined);
2124
t.is(loggedMessages.length, 0);
@@ -25,7 +28,7 @@ test("resolveToolsInput returns workflow input when only workflow input is provi
2528
const loggedMessages: LoggedMessage[] = [];
2629
const logger = getRecordingLogger(loggedMessages);
2730

28-
const result = resolveToolsInput("latest", true, {}, logger);
31+
const result = resolveToolsInput("latest", false, {}, logger);
2932

3033
t.is(result, "latest");
3134
t.is(loggedMessages.length, 1);
@@ -44,7 +47,7 @@ test("resolveToolsInput returns repository property when only repository propert
4447
};
4548
const result = resolveToolsInput(
4649
undefined,
47-
true,
50+
false,
4851
repositoryProperties,
4952
logger,
5053
);
@@ -66,7 +69,7 @@ test("resolveToolsInput prioritizes workflow input over repository property", (t
6669
};
6770
const result = resolveToolsInput(
6871
"nightly",
69-
true,
72+
false,
7073
repositoryProperties,
7174
logger,
7275
);
@@ -86,7 +89,7 @@ test("resolveToolsInput treats empty string workflow input as not set", (t) => {
8689
const repositoryProperties: RepositoryProperties = {
8790
[RepositoryPropertyName.TOOLS]: "toolcache",
8891
};
89-
const result = resolveToolsInput("", true, repositoryProperties, logger);
92+
const result = resolveToolsInput("", false, repositoryProperties, logger);
9093

9194
t.is(result, "toolcache");
9295
t.is(loggedMessages.length, 1);
@@ -105,7 +108,7 @@ test("resolveToolsInput returns undefined when repository property is undefined"
105108
};
106109
const result = resolveToolsInput(
107110
undefined,
108-
true,
111+
false,
109112
repositoryProperties,
110113
logger,
111114
);
@@ -114,7 +117,7 @@ test("resolveToolsInput returns undefined when repository property is undefined"
114117
t.is(loggedMessages.length, 0);
115118
});
116119

117-
test("resolveToolsInput returns repository property when fallback is disabled", (t) => {
120+
test("resolveToolsInput returns repository property when workflow input is not set", (t) => {
118121
const loggedMessages: LoggedMessage[] = [];
119122
const logger = getRecordingLogger(loggedMessages);
120123

@@ -136,7 +139,7 @@ test("resolveToolsInput returns repository property when fallback is disabled",
136139
);
137140
});
138141

139-
test("resolveToolsInput does not log when fallback is disabled and repository property is not set", (t) => {
142+
test("resolveToolsInput does not log when workflow input and repository property are not set", (t) => {
140143
const loggedMessages: LoggedMessage[] = [];
141144
const logger = getRecordingLogger(loggedMessages);
142145

@@ -145,3 +148,70 @@ test("resolveToolsInput does not log when fallback is disabled and repository pr
145148
t.is(result, undefined);
146149
t.is(loggedMessages.length, 0);
147150
});
151+
152+
test("resolveToolsInput applies tools property in enforce mode for static workflows", (t) => {
153+
const loggedMessages: LoggedMessage[] = [];
154+
const logger = getRecordingLogger(loggedMessages);
155+
156+
const repositoryProperties: RepositoryProperties = {
157+
[RepositoryPropertyName.TOOLS]: "toolcache",
158+
[RepositoryPropertyName.TOOLS_MODE]:
159+
ToolsModeRepositoryPropertyValue.Enforce,
160+
};
161+
const result = resolveToolsInput(
162+
undefined,
163+
false,
164+
repositoryProperties,
165+
logger,
166+
);
167+
168+
t.is(result, "toolcache");
169+
t.is(loggedMessages.length, 1);
170+
t.is(
171+
loggedMessages[0].message,
172+
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
173+
);
174+
});
175+
176+
test("resolveToolsInput applies tools property in dynamic mode for dynamic workflows", (t) => {
177+
const loggedMessages: LoggedMessage[] = [];
178+
const logger = getRecordingLogger(loggedMessages);
179+
180+
const repositoryProperties: RepositoryProperties = {
181+
[RepositoryPropertyName.TOOLS]: "toolcache",
182+
[RepositoryPropertyName.TOOLS_MODE]:
183+
ToolsModeRepositoryPropertyValue.Dynamic,
184+
};
185+
const result = resolveToolsInput(undefined, true, repositoryProperties, logger);
186+
187+
t.is(result, "toolcache");
188+
t.is(loggedMessages.length, 1);
189+
t.is(
190+
loggedMessages[0].message,
191+
"Setting tools: toolcache based on the 'github-codeql-tools' repository property.",
192+
);
193+
});
194+
195+
test("resolveToolsInput ignores tools property in dynamic mode for static workflows", (t) => {
196+
const loggedMessages: LoggedMessage[] = [];
197+
const logger = getRecordingLogger(loggedMessages);
198+
199+
const repositoryProperties: RepositoryProperties = {
200+
[RepositoryPropertyName.TOOLS]: "toolcache",
201+
[RepositoryPropertyName.TOOLS_MODE]:
202+
ToolsModeRepositoryPropertyValue.Dynamic,
203+
};
204+
const result = resolveToolsInput(
205+
undefined,
206+
false,
207+
repositoryProperties,
208+
logger,
209+
);
210+
211+
t.is(result, undefined);
212+
t.is(loggedMessages.length, 1);
213+
t.is(
214+
loggedMessages[0].message,
215+
"Ignoring 'github-codeql-tools' repository property because 'github-codeql-tools-mode' is set to 'dynamic' and this is not a dynamic workflow.",
216+
);
217+
});

src/config/resolve-tools-input.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import {
22
RepositoryProperties,
33
RepositoryPropertyName,
4+
ToolsModeRepositoryPropertyValue,
45
} from "../feature-flags/properties";
56
import { Logger } from "../logging";
67

78
/**
89
* Resolves the effective tools input by combining the workflow input and repository properties.
910
* The explicit `tools` workflow input takes precedence. If none is provided,
10-
* falls back to the repository property (if set and enabled for this workflow).
11+
* falls back to the repository property (if set). The optional
12+
* `github-codeql-tools-mode` repository property controls whether this fallback
13+
* applies to all workflows (`enforce`) or only dynamic workflows (`dynamic`).
1114
*
1215
* @param toolsWorkflowInput - The value of the `tools` workflow input, if provided.
13-
* @param _allowRepositoryPropertyFallback - Reserved for backwards compatibility.
16+
* @param isDynamicWorkflow - Whether the current workflow is dynamic.
1417
* @param repositoryProperties - The parsed repository properties.
1518
* @param logger - Logger for outputting resolution messages.
1619
* @returns The effective tools input value.
1720
*/
1821
export function resolveToolsInput(
1922
toolsWorkflowInput: string | undefined,
20-
_allowRepositoryPropertyFallback: boolean,
23+
isDynamicWorkflow: boolean,
2124
repositoryProperties: RepositoryProperties,
2225
logger: Logger,
2326
): string | undefined {
@@ -29,6 +32,20 @@ export function resolveToolsInput(
2932
}
3033

3134
const toolsPropertyValue = repositoryProperties[RepositoryPropertyName.TOOLS];
35+
const toolsMode =
36+
repositoryProperties[RepositoryPropertyName.TOOLS_MODE] ??
37+
ToolsModeRepositoryPropertyValue.Enforce;
38+
39+
if (
40+
toolsPropertyValue &&
41+
toolsMode === ToolsModeRepositoryPropertyValue.Dynamic &&
42+
!isDynamicWorkflow
43+
) {
44+
logger.info(
45+
`Ignoring '${RepositoryPropertyName.TOOLS}' repository property because '${RepositoryPropertyName.TOOLS_MODE}' is set to '${toolsMode}' and this is not a dynamic workflow.`,
46+
);
47+
return undefined;
48+
}
3249

3350
if (toolsPropertyValue) {
3451
logger.info(

src/feature-flags/properties.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => {
7979
data: [
8080
{ property_name: "github-codeql-extra-queries", value: "+queries" },
8181
{ property_name: "github-codeql-tools", value: "toolcache" },
82+
{ property_name: "github-codeql-tools-mode", value: "dynamic" },
8283
{ property_name: "unknown-property", value: "something" },
8384
] satisfies properties.GitHubPropertiesResponse,
8485
});
@@ -91,9 +92,42 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => {
9192
t.deepEqual(response, {
9293
"github-codeql-extra-queries": "+queries",
9394
"github-codeql-tools": "toolcache",
95+
"github-codeql-tools-mode": "dynamic",
9496
});
9597
});
9698

99+
test.serial(
100+
"loadPropertiesFromApi warns if tools mode property has unexpected value",
101+
async (t) => {
102+
sinon.stub(api, "getRepositoryProperties").resolves({
103+
headers: {},
104+
status: 200,
105+
url: "",
106+
data: [
107+
{
108+
property_name: "github-codeql-tools-mode",
109+
value: "all",
110+
},
111+
] satisfies properties.GitHubPropertiesResponse,
112+
});
113+
const logger = getRunnerLogger(true);
114+
const warningSpy = sinon.spy(logger, "warning");
115+
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
116+
const response = await properties.loadPropertiesFromApi(
117+
logger,
118+
mockRepositoryNwo,
119+
);
120+
t.deepEqual(response, {
121+
"github-codeql-tools-mode": "enforce",
122+
});
123+
t.true(warningSpy.calledOnce);
124+
t.is(
125+
warningSpy.firstCall.args[0],
126+
"Repository property 'github-codeql-tools-mode' has unexpected value 'all'. Expected 'dynamic' or 'enforce'. Defaulting to 'enforce'.",
127+
);
128+
},
129+
);
130+
97131
test.serial("loadPropertiesFromApi parses true boolean property", async (t) => {
98132
sinon.stub(api, "getRepositoryProperties").resolves({
99133
headers: {},

src/feature-flags/properties.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export enum RepositoryPropertyName {
1717
EXTRA_QUERIES = "github-codeql-extra-queries",
1818
FILE_COVERAGE_ON_PRS = "github-codeql-file-coverage-on-prs",
1919
TOOLS = "github-codeql-tools",
20+
TOOLS_MODE = "github-codeql-tools-mode",
21+
}
22+
23+
export enum ToolsModeRepositoryPropertyValue {
24+
Dynamic = "dynamic",
25+
Enforce = "enforce",
2026
}
2127

2228
/** Parsed types of the known repository properties. */
@@ -25,6 +31,7 @@ export type AllRepositoryProperties = {
2531
[RepositoryPropertyName.EXTRA_QUERIES]: string;
2632
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
2733
[RepositoryPropertyName.TOOLS]: string;
34+
[RepositoryPropertyName.TOOLS_MODE]: ToolsModeRepositoryPropertyValue;
2835
};
2936

3037
/** Parsed repository properties. */
@@ -36,6 +43,7 @@ export type RepositoryPropertyApiType = {
3643
[RepositoryPropertyName.EXTRA_QUERIES]: string;
3744
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: string;
3845
[RepositoryPropertyName.TOOLS]: string;
46+
[RepositoryPropertyName.TOOLS_MODE]: string;
3947
};
4048

4149
/** 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: {
8492
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
8593
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
8694
[RepositoryPropertyName.TOOLS]: stringProperty,
95+
[RepositoryPropertyName.TOOLS_MODE]: {
96+
validate: isString,
97+
parse: parseToolsModeRepositoryProperty,
98+
},
8799
};
88100

89101
/**
@@ -256,6 +268,25 @@ function parseStringRepositoryProperty(_name: string, value: string): string {
256268
return value;
257269
}
258270

271+
/** Parse the tools mode repository property. */
272+
function parseToolsModeRepositoryProperty(
273+
name: string,
274+
value: string,
275+
logger: Logger,
276+
): ToolsModeRepositoryPropertyValue {
277+
if (
278+
value !== ToolsModeRepositoryPropertyValue.Dynamic &&
279+
value !== ToolsModeRepositoryPropertyValue.Enforce
280+
) {
281+
logger.warning(
282+
`Repository property '${name}' has unexpected value '${value}'. Expected 'dynamic' or 'enforce'. Defaulting to 'enforce'.`,
283+
);
284+
return ToolsModeRepositoryPropertyValue.Enforce;
285+
}
286+
287+
return value;
288+
}
289+
259290
/** Set of known repository property names, for fast lookups. */
260291
const KNOWN_REPOSITORY_PROPERTY_NAMES = new Set<string>(
261292
Object.values(RepositoryPropertyName),

0 commit comments

Comments
 (0)