diff --git a/CHANGELOG.md b/CHANGELOG.md index 039117fb25..0b9c0015b0 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] -No user facing changes. +- 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 0e7010b990..b000954ff2 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -150410,11 +150410,14 @@ function getUnknownLanguagesError(languages) { } // src/feature-flags/properties.ts +var github2 = __toESM(require_github()); var GITHUB_CODEQL_PROPERTY_PREFIX = "github-codeql-"; var RepositoryPropertyName = /* @__PURE__ */ ((RepositoryPropertyName2) => { RepositoryPropertyName2["DISABLE_OVERLAY"] = "github-codeql-disable-overlay"; 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) { @@ -150432,7 +150435,12 @@ var booleanProperty = { 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-file-coverage-on-prs" /* FILE_COVERAGE_ON_PRS */]: booleanProperty, + ["github-codeql-tools" /* TOOLS */]: stringProperty, + ["github-codeql-tools-mode" /* TOOLS_MODE */]: { + validate: isString2, + parse: parseToolsModeRepositoryProperty + } }; async function loadPropertiesFromApi(logger, repositoryNwo) { try { @@ -150485,6 +150493,26 @@ async function loadPropertiesFromApi(logger, repositoryNwo) { ); } } +async function loadRepositoryProperties(repositoryNwo, logger) { + const repositoryOwnerType = github2.context.payload.repository?.owner.type; + logger.debug( + `Repository owner type is '${repositoryOwnerType ?? "unknown"}'.` + ); + if (repositoryOwnerType === "User") { + logger.debug( + "Skipping loading repository properties because the repository is owned by a user and therefore cannot have repository properties." + ); + return new Success({}); + } + try { + return new Success(await loadPropertiesFromApi(logger, repositoryNwo)); + } catch (error3) { + logger.info( + `Failed to load repository properties: ${getErrorMessage(error3)}` + ); + return new Failure(error3); + } +} function setProperty2(properties, name, value, logger) { const propertyOptions = repositoryPropertyParsers[name]; if (propertyOptions.validate(value)) { @@ -150506,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) ); @@ -153376,7 +153413,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO ); } else { if (allowToolcacheValueFF) { - logger.warning( + logger.info( `Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.` ); } else { @@ -156717,7 +156754,7 @@ var fs19 = __toESM(require("fs")); var path17 = __toESM(require("path")); var core14 = __toESM(require_core()); var toolrunner4 = __toESM(require_toolrunner()); -var github2 = __toESM(require_github()); +var github3 = __toESM(require_github()); var io6 = __toESM(require_io()); async function initCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, features, logger) { logger.startGroup("Setup CodeQL tools"); @@ -156921,7 +156958,7 @@ function logFileCoverageOnPrsDeprecationWarning(logger) { if (process.env["CODEQL_ACTION_DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION" /* DID_LOG_FILE_COVERAGE_ON_PRS_DEPRECATION */]) { return; } - const repositoryOwnerType = github2.context.payload.repository?.owner.type; + const repositoryOwnerType = github3.context.payload.repository?.owner.type; let message = "Starting April 2026, the CodeQL Action will skip computing file coverage information on pull requests to improve analysis performance. File coverage information will still be computed on non-PR analyses."; const envVarOptOut = "set the `CODEQL_ACTION_FILE_COVERAGE_ON_PRS` environment variable to `true`."; const repoPropertyOptOut = 'create a custom repository property with the name `github-codeql-file-coverage-on-prs` and the type "True/false", then set this property to `true` in the repository\'s settings.'; @@ -158705,10 +158742,50 @@ async function runWrapper3() { var fs27 = __toESM(require("fs")); var path23 = __toESM(require("path")); var core21 = __toESM(require_core()); -var github3 = __toESM(require_github()); var io7 = __toESM(require_io()); var semver10 = __toESM(require_semver2()); +// src/config/resolve-tools-input.ts +function resolveToolsInputWithMetadata(toolsWorkflowInput, isDynamicWorkflow2, repositoryProperties, logger) { + if (toolsWorkflowInput) { + logger.info( + `Setting tools: ${toolsWorkflowInput} based on workflow input.` + ); + return { + effectiveToolsInput: toolsWorkflowInput, + effectiveToolsInputSource: "workflow-input" /* WorkflowInput */, + toolsRepoPropertyMode: void 0 + }; + } + 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 { + effectiveToolsInput: void 0, + effectiveToolsInputSource: "none" /* None */, + toolsRepoPropertyMode: toolsMode + }; + } + if (toolsPropertyValue) { + logger.info( + `Setting tools: ${toolsPropertyValue} based on the '${"github-codeql-tools" /* TOOLS */}' repository property (mode: '${toolsMode}').` + ); + return { + effectiveToolsInput: toolsPropertyValue, + effectiveToolsInputSource: "repository-property" /* RepositoryProperty */, + toolsRepoPropertyMode: toolsMode + }; + } + return { + effectiveToolsInput: void 0, + effectiveToolsInputSource: "none" /* None */, + toolsRepoPropertyMode: void 0 + }; +} + // src/workflow.ts var fs26 = __toESM(require("fs")); var path22 = __toESM(require("path")); @@ -158999,7 +159076,7 @@ async function sendStartingStatusReport(startedAt, config, logger) { await sendStatusReport(statusReportBase); } } -async function sendCompletedStatusReport2(startedAt, config, configFile, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, overlayBaseDatabaseStats, dependencyCachingResults, logger, error3) { +async function sendCompletedStatusReport2(startedAt, config, configFile, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, effectiveToolsInput, effectiveToolsInputSource, toolsRepoPropertyMode, overlayBaseDatabaseStats, dependencyCachingResults, logger, error3) { const statusReportBase = await createStatusReportBase( "init" /* Init */, getActionsStatus(error3), @@ -159017,6 +159094,9 @@ async function sendCompletedStatusReport2(startedAt, config, configFile, toolsDo const initStatusReport = { ...statusReportBase, tools_input: getOptionalInput("tools") || "", + effective_tools_input: effectiveToolsInput || "", + effective_tools_input_source: effectiveToolsInputSource, + tools_repo_property_mode: toolsRepoPropertyMode || "", tools_resolved_version: toolsVersion, tools_source: toolsSource || "UNKNOWN" /* Unknown */, workflow_languages: workflowLanguages || "" @@ -159060,6 +159140,9 @@ async function run3(startedAt) { let toolsSource; let toolsVersion; let zstdAvailability; + let effectiveToolsInput; + let effectiveToolsInputSource = "none" /* None */; + let toolsRepoPropertyMode; try { initializeEnvironment(getActionVersion()); persistInputs(); @@ -159083,6 +159166,7 @@ async function run3(startedAt) { repositoryNwo, logger ); + const repositoryProperties = repositoryPropertiesResult.orElse({}); const jobRunUuid = v4_default(); logger.info(`Job run UUID is ${jobRunUuid}.`); core21.exportVariable("JOB_RUN_UUID" /* JOB_RUN_UUID */, jobRunUuid); @@ -159108,12 +159192,21 @@ async function run3(startedAt) { } const codeQLDefaultVersionInfo = await features.getEnabledDefaultCliVersions(gitHubVersion.type); toolsFeatureFlagsValid = codeQLDefaultVersionInfo.toolsFeatureFlagsValid; + const resolvedToolsInput = resolveToolsInputWithMetadata( + getOptionalInput("tools"), + isDynamicWorkflow(), + repositoryProperties, + logger + ); + effectiveToolsInput = resolvedToolsInput.effectiveToolsInput; + effectiveToolsInputSource = resolvedToolsInput.effectiveToolsInputSource; + toolsRepoPropertyMode = resolvedToolsInput.toolsRepoPropertyMode; const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages") ); const useOverlayAwareDefaultCliVersion = analysisKinds?.length === 1 && analysisKinds[0] === "code-scanning" /* CodeScanning */; const initCodeQLResult = await initCodeQL( - getOptionalInput("tools"), + effectiveToolsInput, apiDetails, getTemporaryDirectory(), gitHubVersion.type, @@ -159149,7 +159242,6 @@ async function run3(startedAt) { } analysisKinds = await getAnalysisKinds(logger, features); const debugMode = getOptionalInput("debug") === "true" || core21.isDebug(); - const repositoryProperties = repositoryPropertiesResult.orElse({}); const fileCoverageResult = await getFileCoverageInformationEnabled( debugMode, codeql, @@ -159447,6 +159539,9 @@ exec ${goBinaryPath} "$@"` toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, overlayBaseDatabaseStats, dependencyCachingStatus, logger, @@ -159464,31 +159559,14 @@ exec ${goBinaryPath} "$@"` toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, overlayBaseDatabaseStats, dependencyCachingStatus, logger ); } -async function loadRepositoryProperties(repositoryNwo, logger) { - const repositoryOwnerType = github3.context.payload.repository?.owner.type; - logger.debug( - `Repository owner type is '${repositoryOwnerType ?? "unknown"}'.` - ); - if (repositoryOwnerType === "User") { - logger.debug( - "Skipping loading repository properties because the repository is owned by a user and therefore cannot have repository properties." - ); - return new Success({}); - } - try { - return new Success(await loadPropertiesFromApi(logger, repositoryNwo)); - } catch (error3) { - logger.warning( - `Failed to load repository properties: ${getErrorMessage(error3)}` - ); - return new Failure(error3); - } -} async function recordZstdAvailability(config, zstdAvailability) { addNoLanguageDiagnostic( config, @@ -160057,7 +160135,7 @@ async function runWrapper6() { // src/setup-codeql-action.ts var core24 = __toESM(require_core()); -async function sendCompletedStatusReport3(startedAt, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, logger, error3) { +async function sendCompletedStatusReport3(startedAt, toolsDownloadStatusReport, toolsFeatureFlagsValid, toolsSource, toolsVersion, effectiveToolsInput, effectiveToolsInputSource, toolsRepoPropertyMode, logger, error3) { const statusReportBase = await createStatusReportBase( "setup-codeql" /* SetupCodeQL */, getActionsStatus(error3), @@ -160074,6 +160152,9 @@ async function sendCompletedStatusReport3(startedAt, toolsDownloadStatusReport, const initStatusReport = { ...statusReportBase, tools_input: getOptionalInput("tools") || "", + effective_tools_input: effectiveToolsInput || "", + effective_tools_input_source: effectiveToolsInputSource, + tools_repo_property_mode: toolsRepoPropertyMode || "", tools_resolved_version: toolsVersion, tools_source: toolsSource || "UNKNOWN" /* Unknown */, workflow_languages: "" @@ -160094,6 +160175,9 @@ async function run6(startedAt) { let toolsFeatureFlagsValid; let toolsSource; let toolsVersion; + let effectiveToolsInput; + let effectiveToolsInputSource = "none" /* None */; + let toolsRepoPropertyMode; try { initializeEnvironment(getActionVersion()); const apiDetails = { @@ -160128,12 +160212,26 @@ async function run6(startedAt) { } const codeQLDefaultVersionInfo = await features.getEnabledDefaultCliVersions(gitHubVersion.type); toolsFeatureFlagsValid = codeQLDefaultVersionInfo.toolsFeatureFlagsValid; + const repositoryPropertiesResult = await loadRepositoryProperties( + repositoryNwo, + logger + ); + const repositoryProperties = repositoryPropertiesResult.orElse({}); + const resolvedToolsInput = resolveToolsInputWithMetadata( + getOptionalInput("tools"), + isDynamicWorkflow(), + repositoryProperties, + logger + ); + effectiveToolsInput = resolvedToolsInput.effectiveToolsInput; + effectiveToolsInputSource = resolvedToolsInput.effectiveToolsInputSource; + toolsRepoPropertyMode = resolvedToolsInput.toolsRepoPropertyMode; const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages") ); const analysisKinds = await getAnalysisKinds(logger, features); const initCodeQLResult = await initCodeQL( - getOptionalInput("tools"), + effectiveToolsInput, apiDetails, getTemporaryDirectory(), gitHubVersion.type, @@ -160174,6 +160272,9 @@ async function run6(startedAt) { toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, logger ); } diff --git a/src/analyze-action.ts b/src/analyze-action.ts index cc2777bc5f..4b22f2dc55 100644 --- a/src/analyze-action.ts +++ b/src/analyze-action.ts @@ -30,7 +30,7 @@ import { } from "./dependency-caching"; import { EnvVar } from "./environment"; import { initFeatures } from "./feature-flags"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getActionsLogger, Logger } from "./logging"; import { cleanupAndUploadOverlayBaseDatabaseToCache } from "./overlay/caching"; import { getRepositoryNwo } from "./repository"; diff --git a/src/analyze.test.ts b/src/analyze.test.ts index 7523d239bf..e94ea80b57 100644 --- a/src/analyze.test.ts +++ b/src/analyze.test.ts @@ -14,7 +14,7 @@ import { } from "./analyze"; import { createStubCodeQL } from "./codeql"; import { Feature } from "./feature-flags"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getRunnerLogger } from "./logging"; import { setupTests, diff --git a/src/analyze.ts b/src/analyze.ts index 63830445d2..0d854cda9d 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -21,7 +21,7 @@ import { } from "./diff-informed-analysis-utils"; import { EnvVar } from "./environment"; import { FeatureEnablement, Feature } from "./feature-flags"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { Logger, withGroupAsync } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; import type * as sarif from "./sarif"; diff --git a/src/autobuild-action.ts b/src/autobuild-action.ts index dc20211379..21244340eb 100644 --- a/src/autobuild-action.ts +++ b/src/autobuild-action.ts @@ -10,7 +10,7 @@ import { determineAutobuildLanguages, runAutobuild } from "./autobuild"; import { getCodeQL } from "./codeql"; import { Config, getConfig } from "./config-utils"; import { EnvVar } from "./environment"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { Logger, getActionsLogger } from "./logging"; import { StatusReportBase, diff --git a/src/autobuild.ts b/src/autobuild.ts index fc4983f4ef..fdf4ef90a8 100644 --- a/src/autobuild.ts +++ b/src/autobuild.ts @@ -7,7 +7,7 @@ import * as configUtils from "./config-utils"; import { DocUrl } from "./doc-url"; import { EnvVar } from "./environment"; import { Feature, featureConfig, initFeatures } from "./feature-flags"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; import { asyncFilter, BuildMode } from "./util"; diff --git a/src/codeql.test.ts b/src/codeql.test.ts index dea4cf04af..dd9faae92a 100644 --- a/src/codeql.test.ts +++ b/src/codeql.test.ts @@ -21,7 +21,7 @@ import { import type { Config } from "./config-utils"; import * as defaults from "./defaults.json"; import { DocUrl } from "./doc-url"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getRunnerLogger } from "./logging"; import { ToolsSource } from "./setup-codeql"; import { diff --git a/src/codeql.ts b/src/codeql.ts index 19f933c39a..80e1f1df58 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -22,7 +22,7 @@ import { FeatureEnablement, } from "./feature-flags"; import { isAnalyzingDefaultBranch } from "./git-utils"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { Logger } from "./logging"; import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "./overlay"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 10dee55399..79f7b0633e 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -18,7 +18,7 @@ import { Feature } from "./feature-flags"; import { RepositoryProperties } from "./feature-flags/properties"; import * as gitUtils from "./git-utils"; import { GitVersionInfo } from "./git-utils"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { getRunnerLogger } from "./logging"; import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay"; import * as overlayDiagnostics from "./overlay/diagnostics"; diff --git a/src/config-utils.ts b/src/config-utils.ts index 87329fce2e..b6e5e8ef3c 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -48,7 +48,7 @@ import { hasSubmodules, isAnalyzingDefaultBranch, } from "./git-utils"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { Logger } from "./logging"; import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay"; import { diff --git a/src/config/resolve-tools-input.test.ts b/src/config/resolve-tools-input.test.ts new file mode 100644 index 0000000000..20c20e5a60 --- /dev/null +++ b/src/config/resolve-tools-input.test.ts @@ -0,0 +1,280 @@ +import test from "ava"; + +import { + EffectiveToolsInputSource, + resolveToolsInput, + resolveToolsInputWithMetadata, +} from "../config/resolve-tools-input"; +import { + RepositoryPropertyName, + ToolsModeRepositoryPropertyValue, +} from "../feature-flags/properties"; +import type { RepositoryProperties } from "../feature-flags/properties"; +import { + getRecordingLogger, + LoggedMessage, + setupTests, +} from "../testing-utils"; + +setupTests(test); + +test("resolveToolsInput returns undefined when no tools input or repository property is set", (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + + const result = resolveToolsInput(undefined, false, {}, logger); + + t.is(result, undefined); + t.is(loggedMessages.length, 0); +}); + +test("resolveToolsInput returns workflow input when only workflow input is provided", (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + + const result = resolveToolsInput("latest", false, {}, logger); + + t.is(result, "latest"); + t.is(loggedMessages.length, 1); + t.is( + loggedMessages[0].message, + "Setting tools: latest based on workflow input.", + ); +}); + +test("resolveToolsInput returns repository property when only repository property is provided", (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 (mode: 'enforce').", + ); +}); + +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 (mode: 'enforce').", + ); +}); + +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 (mode: 'enforce').", + ); +}); + +test("resolveToolsInput does not log when workflow input and repository property are not set", (t) => { + const loggedMessages: LoggedMessage[] = []; + const logger = getRecordingLogger(loggedMessages); + + const result = resolveToolsInput(undefined, false, {}, logger); + + 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 (mode: 'enforce').", + ); +}); + +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 (mode: 'dynamic').", + ); +}); + +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.", + ); +}); + +test("resolveToolsInputWithMetadata reports workflow input source", (t) => { + const logger = getRecordingLogger([]); + + const result = resolveToolsInputWithMetadata("latest", false, {}, logger); + + t.is(result.effectiveToolsInput, "latest"); + t.is( + result.effectiveToolsInputSource, + EffectiveToolsInputSource.WorkflowInput, + ); + t.is(result.toolsRepoPropertyMode, undefined); +}); + +test("resolveToolsInputWithMetadata reports repository property source and mode", (t) => { + const logger = getRecordingLogger([]); + + const result = resolveToolsInputWithMetadata( + undefined, + false, + { + [RepositoryPropertyName.TOOLS]: "toolcache", + [RepositoryPropertyName.TOOLS_MODE]: + ToolsModeRepositoryPropertyValue.Enforce, + }, + logger, + ); + + t.is(result.effectiveToolsInput, "toolcache"); + t.is( + result.effectiveToolsInputSource, + EffectiveToolsInputSource.RepositoryProperty, + ); + t.is(result.toolsRepoPropertyMode, ToolsModeRepositoryPropertyValue.Enforce); +}); + +test("resolveToolsInputWithMetadata reports dynamic-mode skip on static workflows", (t) => { + const logger = getRecordingLogger([]); + + const result = resolveToolsInputWithMetadata( + undefined, + false, + { + [RepositoryPropertyName.TOOLS]: "toolcache", + [RepositoryPropertyName.TOOLS_MODE]: + ToolsModeRepositoryPropertyValue.Dynamic, + }, + logger, + ); + + t.is(result.effectiveToolsInput, undefined); + t.is(result.effectiveToolsInputSource, EffectiveToolsInputSource.None); + t.is(result.toolsRepoPropertyMode, ToolsModeRepositoryPropertyValue.Dynamic); +}); diff --git a/src/config/resolve-tools-input.ts b/src/config/resolve-tools-input.ts new file mode 100644 index 0000000000..06f0ec126c --- /dev/null +++ b/src/config/resolve-tools-input.ts @@ -0,0 +1,100 @@ +import { + RepositoryProperties, + RepositoryPropertyName, + ToolsModeRepositoryPropertyValue, +} from "../feature-flags/properties"; +import { Logger } from "../logging"; + +export enum EffectiveToolsInputSource { + WorkflowInput = "workflow-input", + RepositoryProperty = "repository-property", + None = "none", +} + +export type ResolvedToolsInput = { + effectiveToolsInput: string | undefined; + effectiveToolsInputSource: EffectiveToolsInputSource; + toolsRepoPropertyMode: ToolsModeRepositoryPropertyValue | undefined; +}; + +/** + * 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). 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 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, + isDynamicWorkflow: boolean, + repositoryProperties: RepositoryProperties, + logger: Logger, +): string | undefined { + return resolveToolsInputWithMetadata( + toolsWorkflowInput, + isDynamicWorkflow, + repositoryProperties, + logger, + ).effectiveToolsInput; +} + +export function resolveToolsInputWithMetadata( + toolsWorkflowInput: string | undefined, + isDynamicWorkflow: boolean, + repositoryProperties: RepositoryProperties, + logger: Logger, +): ResolvedToolsInput { + if (toolsWorkflowInput) { + logger.info( + `Setting tools: ${toolsWorkflowInput} based on workflow input.`, + ); + return { + effectiveToolsInput: toolsWorkflowInput, + effectiveToolsInputSource: EffectiveToolsInputSource.WorkflowInput, + toolsRepoPropertyMode: undefined, + }; + } + + 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 { + effectiveToolsInput: undefined, + effectiveToolsInputSource: EffectiveToolsInputSource.None, + toolsRepoPropertyMode: toolsMode, + }; + } + + if (toolsPropertyValue) { + logger.info( + `Setting tools: ${toolsPropertyValue} based on the '${RepositoryPropertyName.TOOLS}' repository property (mode: '${toolsMode}').`, + ); + return { + effectiveToolsInput: toolsPropertyValue, + effectiveToolsInputSource: EffectiveToolsInputSource.RepositoryProperty, + toolsRepoPropertyMode: toolsMode, + }; + } + + return { + effectiveToolsInput: undefined, + effectiveToolsInputSource: EffectiveToolsInputSource.None, + toolsRepoPropertyMode: undefined, + }; +} diff --git a/src/database-upload.test.ts b/src/database-upload.test.ts index 1cfbaecad6..45be8b5d26 100644 --- a/src/database-upload.test.ts +++ b/src/database-upload.test.ts @@ -12,7 +12,7 @@ import { createStubCodeQL } from "./codeql"; import { Config } from "./config-utils"; import { cleanupAndUploadDatabases } from "./database-upload"; import * as gitUtils from "./git-utils"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { RepositoryNwo } from "./repository"; import { checkExpectedLogMessages, diff --git a/src/debug-artifacts.ts b/src/debug-artifacts.ts index 016fcdf7c4..2f97721547 100644 --- a/src/debug-artifacts.ts +++ b/src/debug-artifacts.ts @@ -13,7 +13,7 @@ import { type CodeQL } from "./codeql"; import { Config } from "./config-utils"; import { EnvVar } from "./environment"; import * as json from "./json"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { Logger, withGroup } from "./logging"; import { isSafeArtifactUpload, diff --git a/src/dependency-caching.test.ts b/src/dependency-caching.test.ts index e611cd03eb..944f12c3c4 100644 --- a/src/dependency-caching.test.ts +++ b/src/dependency-caching.test.ts @@ -27,7 +27,7 @@ import { CacheStoreResult, } from "./dependency-caching"; import { Feature } from "./feature-flags"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { setupTests, createFeatures, diff --git a/src/dependency-caching.ts b/src/dependency-caching.ts index f04d38f46c..fa8ff7b13e 100644 --- a/src/dependency-caching.ts +++ b/src/dependency-caching.ts @@ -11,7 +11,7 @@ import { CodeQL } from "./codeql"; import { Config } from "./config-utils"; import { EnvVar } from "./environment"; import { Feature, FeatureEnablement } from "./feature-flags"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { Logger } from "./logging"; import { getErrorMessage, getRequiredEnvParam } from "./util"; diff --git a/src/diagnostics.ts b/src/diagnostics.ts index 65e82ce1af..5bad57229b 100644 --- a/src/diagnostics.ts +++ b/src/diagnostics.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from "fs"; import path from "path"; import type { Config } from "./config-utils"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { getActionsLogger } from "./logging"; import { getCodeQLDatabasePath } from "./util"; diff --git a/src/feature-flags/properties.test.ts b/src/feature-flags/properties.test.ts index 2676c2dcda..f8b088aa5b 100644 --- a/src/feature-flags/properties.test.ts +++ b/src/feature-flags/properties.test.ts @@ -78,6 +78,8 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => { url: "", 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, }); @@ -87,9 +89,45 @@ test.serial("loadPropertiesFromApi loads known properties", async (t) => { logger, mockRepositoryNwo, ); - t.deepEqual(response, { "github-codeql-extra-queries": "+queries" }); + 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 12ba280bec..b27ee414ce 100644 --- a/src/feature-flags/properties.ts +++ b/src/feature-flags/properties.ts @@ -1,7 +1,10 @@ +import * as github from "@actions/github"; + import { isDynamicWorkflow } from "../actions-util"; import { getRepositoryProperties } from "../api-client"; import { Logger } from "../logging"; import { RepositoryNwo } from "../repository"; +import { getErrorMessage, Result, Success, Failure } from "../util"; /** The common prefix that we expect all of our repository properties to have. */ export const GITHUB_CODEQL_PROPERTY_PREFIX = "github-codeql-"; @@ -13,6 +16,13 @@ export enum RepositoryPropertyName { DISABLE_OVERLAY = "github-codeql-disable-overlay", 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. */ @@ -20,6 +30,8 @@ export type AllRepositoryProperties = { [RepositoryPropertyName.DISABLE_OVERLAY]: boolean; [RepositoryPropertyName.EXTRA_QUERIES]: string; [RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean; + [RepositoryPropertyName.TOOLS]: string; + [RepositoryPropertyName.TOOLS_MODE]: ToolsModeRepositoryPropertyValue; }; /** Parsed repository properties. */ @@ -30,6 +42,8 @@ export type RepositoryPropertyApiType = { [RepositoryPropertyName.DISABLE_OVERLAY]: string; [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. */ @@ -77,6 +91,11 @@ const repositoryPropertyParsers: { [RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty, [RepositoryPropertyName.EXTRA_QUERIES]: stringProperty, [RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty, + [RepositoryPropertyName.TOOLS]: stringProperty, + [RepositoryPropertyName.TOOLS_MODE]: { + validate: isString, + parse: parseToolsModeRepositoryProperty, + }, }; /** @@ -172,6 +191,38 @@ export async function loadPropertiesFromApi( } } +/** + * Loads [repository properties](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) if applicable. + */ +export async function loadRepositoryProperties( + repositoryNwo: RepositoryNwo, + logger: Logger, +): Promise> { + // See if we can skip loading repository properties early. In particular, + // repositories owned by users cannot have repository properties, so we can + // skip the API call entirely in that case. + const repositoryOwnerType = github.context.payload.repository?.owner.type; + logger.debug( + `Repository owner type is '${repositoryOwnerType ?? "unknown"}'.`, + ); + if (repositoryOwnerType === "User") { + logger.debug( + "Skipping loading repository properties because the repository is owned by a user and " + + "therefore cannot have repository properties.", + ); + return new Success({}); + } + + try { + return new Success(await loadPropertiesFromApi(logger, repositoryNwo)); + } catch (error) { + logger.info( + `Failed to load repository properties: ${getErrorMessage(error)}`, + ); + return new Failure(error); + } +} + /** * Validate that `value` has the correct type for `K` and, if so, update the partial set of repository * properties with the parsed value of the specified property. @@ -217,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 9d2619b1d1..747871b090 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -2,12 +2,12 @@ import * as fs from "fs"; import * as path from "path"; import * as core from "@actions/core"; -import * as github from "@actions/github"; import * as io from "@actions/io"; import * as semver from "semver"; import { v4 as uuidV4 } from "uuid"; import { + isDynamicWorkflow, FileCmdNotFoundError, getActionVersion, getFileType, @@ -24,6 +24,10 @@ import { shouldRestoreCache, } from "./caching-utils"; import { CodeQL } from "./codeql"; +import { + EffectiveToolsInputSource, + resolveToolsInputWithMetadata, +} from "./config/resolve-tools-input"; import * as configUtils from "./config-utils"; import { DependencyCacheRestoreStatusReport, @@ -40,8 +44,8 @@ import { import { EnvVar } from "./environment"; import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; import { - loadPropertiesFromApi, - RepositoryProperties, + loadRepositoryProperties, + ToolsModeRepositoryPropertyValue, } from "./feature-flags/properties"; import { checkInstallPython311, @@ -53,14 +57,14 @@ import { initConfig, runDatabaseInitCluster, } from "./init"; -import { JavaEnvVars, BuiltInLanguage } from "./languages"; +import { JavaEnvVars, BuiltInLanguage } from "./languages/index"; import { getActionsLogger, Logger, withGroupAsync } from "./logging"; import { downloadOverlayBaseDatabaseFromCache, OverlayBaseDatabaseDownloadStats, } from "./overlay/caching"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; -import { getRepositoryNwo, RepositoryNwo } from "./repository"; +import { getRepositoryNwo } from "./repository"; import { ToolsSource } from "./setup-codeql"; import { ActionName, @@ -93,10 +97,7 @@ import { checkActionVersion, getErrorMessage, BuildMode, - Result, getOptionalEnvVar, - Success, - Failure, } from "./util"; import { checkWorkflow } from "./workflow"; @@ -140,6 +141,9 @@ async function sendCompletedStatusReport( toolsFeatureFlagsValid: boolean | undefined, toolsSource: ToolsSource, toolsVersion: string, + effectiveToolsInput: string | undefined, + effectiveToolsInputSource: EffectiveToolsInputSource, + toolsRepoPropertyMode: ToolsModeRepositoryPropertyValue | undefined, overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined, dependencyCachingResults: DependencyCacheRestoreStatusReport | undefined, logger: Logger, @@ -165,6 +169,9 @@ async function sendCompletedStatusReport( const initStatusReport: InitStatusReport = { ...statusReportBase, tools_input: getOptionalInput("tools") || "", + effective_tools_input: effectiveToolsInput || "", + effective_tools_input_source: effectiveToolsInputSource, + tools_repo_property_mode: toolsRepoPropertyMode || "", tools_resolved_version: toolsVersion, tools_source: toolsSource || ToolsSource.Unknown, workflow_languages: workflowLanguages || "", @@ -219,6 +226,9 @@ async function run(startedAt: Date) { let toolsSource: ToolsSource; let toolsVersion: string; let zstdAvailability: ZstdAvailability | undefined; + let effectiveToolsInput: string | undefined; + let effectiveToolsInputSource: EffectiveToolsInputSource; + let toolsRepoPropertyMode: ToolsModeRepositoryPropertyValue | undefined; try { initializeEnvironment(getActionVersion()); @@ -251,6 +261,7 @@ async function run(startedAt: Date) { repositoryNwo, logger, ); + const repositoryProperties = repositoryPropertiesResult.orElse({}); // Create a unique identifier for this run. const jobRunUuid = uuidV4(); @@ -296,6 +307,21 @@ async function run(startedAt: Date) { const codeQLDefaultVersionInfo = await features.getEnabledDefaultCliVersions(gitHubVersion.type); toolsFeatureFlagsValid = codeQLDefaultVersionInfo.toolsFeatureFlagsValid; + + // 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). + // If 'github-codeql-tools-mode' is set to 'dynamic', this fallback applies + // only to dynamic workflows. Otherwise, it applies to all workflows. + const resolvedToolsInput = resolveToolsInputWithMetadata( + getOptionalInput("tools"), + isDynamicWorkflow(), + repositoryProperties, + logger, + ); + effectiveToolsInput = resolvedToolsInput.effectiveToolsInput; + effectiveToolsInputSource = resolvedToolsInput.effectiveToolsInputSource; + toolsRepoPropertyMode = resolvedToolsInput.toolsRepoPropertyMode; const rawLanguages = configUtils.getRawLanguagesNoAutodetect( getOptionalInput("languages"), ); @@ -303,7 +329,7 @@ async function run(startedAt: Date) { analysisKinds?.length === 1 && analysisKinds[0] === AnalysisKind.CodeScanning; const initCodeQLResult = await initCodeQL( - getOptionalInput("tools"), + effectiveToolsInput, apiDetails, getTemporaryDirectory(), gitHubVersion.type, @@ -350,7 +376,6 @@ async function run(startedAt: Date) { analysisKinds = await getAnalysisKinds(logger, features); const debugMode = getOptionalInput("debug") === "true" || core.isDebug(); - const repositoryProperties = repositoryPropertiesResult.orElse({}); const fileCoverageResult = await getFileCoverageInformationEnabled( debugMode, codeql, @@ -769,6 +794,9 @@ async function run(startedAt: Date) { toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, overlayBaseDatabaseStats, dependencyCachingStatus, logger, @@ -786,44 +814,15 @@ async function run(startedAt: Date) { toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, overlayBaseDatabaseStats, dependencyCachingStatus, logger, ); } -/** - * Loads [repository properties](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization) if applicable. - */ -async function loadRepositoryProperties( - repositoryNwo: RepositoryNwo, - logger: Logger, -): Promise> { - // See if we can skip loading repository properties early. In particular, - // repositories owned by users cannot have repository properties, so we can - // skip the API call entirely in that case. - const repositoryOwnerType = github.context.payload.repository?.owner.type; - logger.debug( - `Repository owner type is '${repositoryOwnerType ?? "unknown"}'.`, - ); - if (repositoryOwnerType === "User") { - logger.debug( - "Skipping loading repository properties because the repository is owned by a user and " + - "therefore cannot have repository properties.", - ); - return new Success({}); - } - - try { - return new Success(await loadPropertiesFromApi(logger, repositoryNwo)); - } catch (error) { - logger.warning( - `Failed to load repository properties: ${getErrorMessage(error)}`, - ); - return new Failure(error); - } -} - async function recordZstdAvailability( config: configUtils.Config, zstdAvailability: ZstdAvailability, diff --git a/src/init.test.ts b/src/init.test.ts index 88ad0c9b18..e92424fd7c 100644 --- a/src/init.test.ts +++ b/src/init.test.ts @@ -15,7 +15,7 @@ import { getFileCoverageInformationEnabled, logFileCoverageOnPrsDeprecationWarning, } from "./init"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { createFeatures, LoggedMessage, diff --git a/src/init.ts b/src/init.ts index 2533d9a894..e88f825249 100644 --- a/src/init.ts +++ b/src/init.ts @@ -26,7 +26,7 @@ import { RepositoryProperties, RepositoryPropertyName, } from "./feature-flags/properties"; -import { BuiltInLanguage, Language } from "./languages"; +import { BuiltInLanguage, Language } from "./languages/index"; import { Logger, withGroupAsync } from "./logging"; import { ToolsSource } from "./setup-codeql"; import { ZstdAvailability } from "./tar"; diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index d3e0e7dbcc..2e25d30587 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -2,6 +2,7 @@ import * as core from "@actions/core"; import { v4 as uuidV4 } from "uuid"; import { + isDynamicWorkflow, getActionVersion, getOptionalInput, getRequiredInput, @@ -10,9 +11,17 @@ import { import { AnalysisKind, getAnalysisKinds } from "./analyses"; import { getGitHubVersion } from "./api-client"; import { CodeQL } from "./codeql"; +import { + EffectiveToolsInputSource, + resolveToolsInputWithMetadata, +} from "./config/resolve-tools-input"; import { getRawLanguagesNoAutodetect } from "./config-utils"; import { EnvVar } from "./environment"; import { initFeatures } from "./feature-flags"; +import { + loadRepositoryProperties, + ToolsModeRepositoryPropertyValue, +} from "./feature-flags/properties"; import { initCodeQL } from "./init"; import { getActionsLogger, Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; @@ -48,6 +57,9 @@ async function sendCompletedStatusReport( toolsFeatureFlagsValid: boolean | undefined, toolsSource: ToolsSource, toolsVersion: string, + effectiveToolsInput: string | undefined, + effectiveToolsInputSource: EffectiveToolsInputSource, + toolsRepoPropertyMode: ToolsModeRepositoryPropertyValue | undefined, logger: Logger, error?: Error, ): Promise { @@ -69,6 +81,9 @@ async function sendCompletedStatusReport( const initStatusReport: InitStatusReport = { ...statusReportBase, tools_input: getOptionalInput("tools") || "", + effective_tools_input: effectiveToolsInput || "", + effective_tools_input_source: effectiveToolsInputSource, + tools_repo_property_mode: toolsRepoPropertyMode || "", tools_resolved_version: toolsVersion, tools_source: toolsSource || ToolsSource.Unknown, workflow_languages: "", @@ -99,6 +114,9 @@ async function run(startedAt: Date): Promise { let toolsFeatureFlagsValid: boolean | undefined; let toolsSource: ToolsSource; let toolsVersion: string; + let effectiveToolsInput: string | undefined; + let effectiveToolsInputSource: EffectiveToolsInputSource; + let toolsRepoPropertyMode: ToolsModeRepositoryPropertyValue | undefined; try { initializeEnvironment(getActionVersion()); @@ -141,12 +159,35 @@ async function run(startedAt: Date): Promise { const codeQLDefaultVersionInfo = await features.getEnabledDefaultCliVersions(gitHubVersion.type); toolsFeatureFlagsValid = codeQLDefaultVersionInfo.toolsFeatureFlagsValid; + + // Fetch the values of known repository properties that affect us. + const repositoryPropertiesResult = await loadRepositoryProperties( + repositoryNwo, + logger, + ); + const repositoryProperties = repositoryPropertiesResult.orElse({}); + + // 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). + // If 'github-codeql-tools-mode' is set to 'dynamic', this fallback applies + // only to dynamic workflows. Otherwise, it applies to all workflows. + const resolvedToolsInput = resolveToolsInputWithMetadata( + getOptionalInput("tools"), + isDynamicWorkflow(), + repositoryProperties, + logger, + ); + effectiveToolsInput = resolvedToolsInput.effectiveToolsInput; + effectiveToolsInputSource = resolvedToolsInput.effectiveToolsInputSource; + toolsRepoPropertyMode = resolvedToolsInput.toolsRepoPropertyMode; const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages"), ); const analysisKinds = await getAnalysisKinds(logger, features); + const initCodeQLResult = await initCodeQL( - getOptionalInput("tools"), + effectiveToolsInput, apiDetails, getTemporaryDirectory(), gitHubVersion.type, @@ -191,6 +232,9 @@ async function run(startedAt: Date): Promise { toolsFeatureFlagsValid, toolsSource, toolsVersion, + effectiveToolsInput, + effectiveToolsInputSource, + toolsRepoPropertyMode, logger, ); } diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index 3db0b6ca4d..c654ba27a7 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -559,7 +559,7 @@ export async function getCodeQLSource( ); } else { if (allowToolcacheValueFF) { - logger.warning( + logger.info( `Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`, ); } else { diff --git a/src/start-proxy-action.ts b/src/start-proxy-action.ts index 609b576441..e3750af8ae 100644 --- a/src/start-proxy-action.ts +++ b/src/start-proxy-action.ts @@ -6,7 +6,7 @@ import * as core from "@actions/core"; import * as actionsUtil from "./actions-util"; import { getGitHubVersion } from "./api-client"; import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; -import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; +import { BuiltInLanguage, parseBuiltInLanguage } from "./languages/index"; import { getActionsLogger, Logger } from "./logging"; import { getRepositoryNwo } from "./repository"; import { diff --git a/src/start-proxy.test.ts b/src/start-proxy.test.ts index 9f12656f62..57822923f8 100644 --- a/src/start-proxy.test.ts +++ b/src/start-proxy.test.ts @@ -10,7 +10,7 @@ import * as defaults from "./defaults.json"; import { setUpFeatureFlagTests } from "./feature-flags/testing-util"; import { UnvalidatedObject, validateSchema } from "./json"; import { makeFromSchema } from "./json/testing-util"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getRunnerLogger, Logger } from "./logging"; import * as startProxyExports from "./start-proxy"; import * as statusReport from "./status-report"; diff --git a/src/start-proxy.ts b/src/start-proxy.ts index d6111510f6..3a123a7bb5 100644 --- a/src/start-proxy.ts +++ b/src/start-proxy.ts @@ -18,7 +18,7 @@ import { FeatureEnablement, } from "./feature-flags"; import * as json from "./json"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { Logger } from "./logging"; import { Address, diff --git a/src/status-report.test.ts b/src/status-report.test.ts index 52132b7649..0455eedd6e 100644 --- a/src/status-report.test.ts +++ b/src/status-report.test.ts @@ -2,9 +2,10 @@ import test from "ava"; import * as sinon from "sinon"; import * as actionsUtil from "./actions-util"; +import { EffectiveToolsInputSource } from "./config/resolve-tools-input"; import { Config } from "./config-utils"; import { EnvVar } from "./environment"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getRunnerLogger } from "./logging"; import { ToolsSource } from "./setup-codeql"; import { @@ -316,6 +317,9 @@ const testCreateInitWithConfigStatusReport = makeMacro({ const initStatusReport: InitStatusReport = { ...statusReportBase, tools_input: "", + effective_tools_input: "", + effective_tools_input_source: EffectiveToolsInputSource.None, + tools_repo_property_mode: "", tools_resolved_version: "foo", tools_source: ToolsSource.Unknown, workflow_languages: "actions", @@ -347,6 +351,8 @@ testCreateInitWithConfigStatusReport.serial( languages: [BuiltInLanguage.java, BuiltInLanguage.swift], }), { + effective_tools_input_source: EffectiveToolsInputSource.None, + tools_repo_property_mode: "", trap_cache_download_size_bytes: 1024, registries: "[]", query_filters: "[]", @@ -354,6 +360,63 @@ testCreateInitWithConfigStatusReport.serial( }, ); +test.serial( + "createInitWithConfigStatusReport preserves tools telemetry fields", + async (t) => { + await withTmpDir(async (tmpDir: string) => { + setupEnvironmentAndStub(tmpDir); + + const config = createTestConfig({ + buildMode: BuildMode.None, + languages: [BuiltInLanguage.java], + }); + + const statusReportBase = await createStatusReportBase( + ActionName.Init, + "failure", + new Date("May 19, 2023 05:19:00"), + config, + { numAvailableBytes: 100, numTotalBytes: 500 }, + getRunnerLogger(false), + "failure cause", + "exception stack trace", + ); + + if (t.truthy(statusReportBase)) { + const initStatusReport: InitStatusReport = { + ...statusReportBase, + tools_input: "", + effective_tools_input: "toolcache", + effective_tools_input_source: + EffectiveToolsInputSource.RepositoryProperty, + tools_repo_property_mode: "dynamic", + tools_resolved_version: "foo", + tools_source: ToolsSource.Unknown, + workflow_languages: "actions", + }; + + const initWithConfigStatusReport = + await createInitWithConfigStatusReport( + config, + initStatusReport, + undefined, + 1024, + undefined, + undefined, + ); + + if (t.truthy(initWithConfigStatusReport)) { + t.is( + initWithConfigStatusReport.effective_tools_input_source, + EffectiveToolsInputSource.RepositoryProperty, + ); + t.is(initWithConfigStatusReport.tools_repo_property_mode, "dynamic"); + } + } + }); + }, +); + testCreateInitWithConfigStatusReport.serial( "includes packs for a single language", createTestConfig({ diff --git a/src/status-report.ts b/src/status-report.ts index b3e3628b36..c3165d96fc 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -12,6 +12,7 @@ import { isSelfHostedRunner, } from "./actions-util"; import { getAnalysisKey, getApiClient } from "./api-client"; +import { EffectiveToolsInputSource } from "./config/resolve-tools-input"; import { parseRegistriesWithoutCredentials, type Config } from "./config-utils"; import { DependencyCacheRestoreStatusReport } from "./dependency-caching"; import { DocUrl } from "./doc-url"; @@ -482,6 +483,12 @@ export async function sendStatusReport( export interface InitStatusReport extends StatusReportBase { /** Value given by the user as the "tools" input. */ tools_input: string; + /** The effective tools input that was used, after applying defaults and repository properties. */ + effective_tools_input: string; + /** Indicates where the effective tools input was resolved from. */ + effective_tools_input_source: EffectiveToolsInputSource; + /** The value of the tools repository property mode, if relevant. */ + tools_repo_property_mode: string; /** Version of the bundle used. */ tools_resolved_version: string; /** Where the bundle originated from. */ diff --git a/src/tracer-config.test.ts b/src/tracer-config.test.ts index 58f844b8e7..b28749cfb2 100644 --- a/src/tracer-config.test.ts +++ b/src/tracer-config.test.ts @@ -6,7 +6,7 @@ import * as sinon from "sinon"; import { CodeQL, getCodeQLForTesting } from "./codeql"; import * as configUtils from "./config-utils"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { createTestConfig, makeVersionInfo, setupTests } from "./testing-utils"; import { ToolsFeature } from "./tools-features"; import { getCombinedTracerConfig } from "./tracer-config"; diff --git a/src/trap-caching.test.ts b/src/trap-caching.test.ts index 9805475796..bc819721ef 100644 --- a/src/trap-caching.test.ts +++ b/src/trap-caching.test.ts @@ -15,7 +15,7 @@ import { import * as configUtils from "./config-utils"; import { Feature } from "./feature-flags"; import * as gitUtils from "./git-utils"; -import { BuiltInLanguage } from "./languages"; +import { BuiltInLanguage } from "./languages/index"; import { getRunnerLogger } from "./logging"; import { createFeatures, diff --git a/src/trap-caching.ts b/src/trap-caching.ts index 216122d47e..c105f201d1 100644 --- a/src/trap-caching.ts +++ b/src/trap-caching.ts @@ -10,7 +10,7 @@ import { type Config } from "./config-utils"; import { DocUrl } from "./doc-url"; import { Feature, FeatureEnablement } from "./feature-flags"; import * as gitUtils from "./git-utils"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { Logger } from "./logging"; import { asHTTPError, diff --git a/src/util.ts b/src/util.ts index e2331461bd..2e053457da 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,7 +15,7 @@ import type { Pack } from "./config/db-config"; import type { Config } from "./config-utils"; import { EnvVar } from "./environment"; import * as json from "./json"; -import { Language } from "./languages"; +import { Language } from "./languages/index"; import { Logger } from "./logging"; /**