Use per-language CodeQL bundles - #4146
henrymercer wants to merge 14 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Warning
- Copilot's review of this pull request may be incomplete because some of the changed files are excluded by your Copilot content exclusion settings. See Excluding content from Copilot for details.
Copilot review overview
🟡 Changes recommended
Fallback telemetry underreports total setup duration by excluding the failed per-language request.
Get a fresh assessment by requesting another Copilot review.
Review tier: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds feature-gated per-language CodeQL bundle selection, extraction, fallback, telemetry, and validation.
Changes:
- Selects eligible release/nightly per-language bundles with 404 fallback.
- Keeps partial bundles out of the toolcache and reports telemetry.
- Adds unit and generated end-to-end validation across ten languages.
| File | Description |
|---|---|
src/per-language-bundles.ts |
Implements bundle eligibility and URL recognition. |
src/per-language-bundles.test.ts |
Tests eligibility and recognition. |
src/setup-codeql.ts |
Resolves, downloads, caches, and falls back between bundles. |
src/setup-codeql.test.ts |
Tests selection, fallback, versions, and caching. |
src/tools-download.ts |
Extends download telemetry. |
src/status-report.ts |
Defines telemetry fields. |
src/init-action.ts |
Reports init telemetry. |
src/setup-codeql-action.ts |
Reports setup telemetry. |
src/feature-flags.ts |
Adds the disabled-by-default feature flag. |
pr-checks/sync.ts |
Supports custom generated matrices. |
pr-checks/checks/per-language-bundle-validation.yml |
Validates all supported language bundles. |
pr-checks/checks/bundle-toolcache.yml |
Preserves combined-bundle cache testing. |
.github/workflows/codescanning-config-cli.yml |
Ensures reusable combined-bundle caching. |
lib/entry-points.js |
Generated artifact; content excluded. |
.github/workflows/__bundle-toolcache.yml |
Generated workflow; content excluded. |
.github/workflows/__per-language-bundle-validation.yml |
Generated workflow; content excluded. |
Files excluded by content exclusion policy (3)
- .github/workflows/__bundle-toolcache.yml
- .github/workflows/__per-language-bundle-validation.yml
- lib/entry-points.js
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
mbg
left a comment
There was a problem hiding this comment.
I have had a look through the changes here and added quite a few detailed comments. Additionally, some high-level ones:
- Like with the other PR this is stacked on, this was a lot more difficult (and time-consuming) to review than it needed to be, because all of the changes were contained in one commit (except the follow-up to Copilot's review comment). There were clear opportunities for breaking the changes up and structuring them to be more incremental.
- Quite a few of the function-level comments and test names make sense in the context of this PR where it's obvious that they relate to per-language vs combined bundles, but wouldn't make sense outside of this context. Some of my review comments highlight specific cases of this, but I didn't comment on all of them.
- Try and remember to make use of the new
ActionStateinfrastructure whenever possible in new or changed code. In particular, thegetPerLanguageBundleLanguagefunction would benefit from this since the tests would not need to beserial.
| /** The language of the single-language bundle that was downloaded, if any. */ | ||
| tools_bundle_language?: string; | ||
| /** | ||
| * Whether we tried to download a single-language bundle, but it did not exist and we fell back to | ||
| * the combined bundle. | ||
| */ | ||
| tools_per_language_bundle_fallback?: boolean; |
There was a problem hiding this comment.
Minor: Consider whether it's worth creating an interface for this that can be used in ToolsDownloadStatusReport as well as here to avoid copying the differently named fields in src/init-action.ts and src/setup-codeql-action.ts.
| features: FeatureEnablement, | ||
| logger: Logger, |
There was a problem hiding this comment.
Consider replacing this with ActionState<["Logger", "FeatureFlags"]> and making it the first parameter.
|
|
||
| if (compressionMethod !== "zstd") { | ||
| // Per-language bundles are only published as zstd archives. | ||
| return explain(`the bundle would be downloaded as ${compressionMethod}`); |
There was a problem hiding this comment.
| return explain(`the bundle would be downloaded as ${compressionMethod}`); | |
| return explain(`the bundle would be downloaded as '${compressionMethod}'`); |
| if (!isGitHubHostedRunner()) { | ||
| // Per-language installs stay out of the toolcache; self-hosted runners should retain | ||
| // the reusable combined bundle instead. | ||
| return explain("the job is not running on a GitHub-hosted runner"); | ||
| } |
There was a problem hiding this comment.
I am wondering if this is the correct choice. Self-hosted runners could use custom images / persistent storage to pre-install the desired CLI bundle in the toolcache. If that's not available, wouldn't it be more desirable to download the language-specific bundle for improved performance like on GH-hosted runners?
There was a problem hiding this comment.
Self-hosted runner can indeed preinstall CodeQL, but I don't think we should assume it. I would prefer to have a better way to determine whether the toolcache is persistent or not before rolling out to self-hosted runners to avoid a situation where we install many per-language bundles and use an unnecessary amount of disk space.
| if (!semver.gte(cliVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION)) { | ||
| return explain( | ||
| `CodeQL ${cliVersion} is older than ${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}, which is the ` + | ||
| "first version that publishes per-language bundles", |
There was a problem hiding this comment.
Minor: The version itself doesn't publish anything:
| "first version that publishes per-language bundles", | |
| "first version for which per-language bundles are published", |
There was a problem hiding this comment.
Updated as suggested.
| const client = github.getOctokit("123", { | ||
| request: { | ||
| fetch: async () => | ||
| new Response(JSON.stringify([{ tag_name: tagName }]), { | ||
| headers: { "content-type": "application/json" }, | ||
| }), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
This is less precise than the stubbing that stubHostedNightly replaces. Consider whether to stub sinon.stub(client.rest.repos, "listReleases"); instead of request.
| } | ||
|
|
||
| test.serial( | ||
| "getCodeQLSource downloads the combined nightly bundle when not eligible", |
There was a problem hiding this comment.
Clear in the context of the PR, but not in general.
| "getCodeQLSource downloads the combined nightly bundle when not eligible", | |
| "getCodeQLSource downloads a combined nightly bundle when per-language bundle is not eligible" |
| { languages: ["java"], features: createFeatures([]) }, | ||
| { | ||
| languages: ["java", "python"], | ||
| features: createFeatures([Feature.PerLanguageBundles]), | ||
| }, |
There was a problem hiding this comment.
Minor: Add comments to the cases to state why they are not eligible.
| ], | ||
| }; | ||
|
|
||
| test.serial("getCodeQLBundleName names the per-language bundle", (t) => { |
There was a problem hiding this comment.
Considering the actual checks in this test:
| test.serial("getCodeQLBundleName names the per-language bundle", (t) => { | |
| test.serial("getCodeQLBundleName returns per-language bundle when a language is specified", (t) => { |
| ...result, | ||
| statusReport: { | ||
| ...result.statusReport, | ||
| totalDurationMs: Math.round(performance.now() - startTime), |
There was a problem hiding this comment.
Two thoughts here:
- Minor: It would be nice to avoid a potential divergence between how this is calculated (i.e.
Math.rounded) indownloadAndExtractand here by having a small wrapper function somewhere forfunction durationMsSince(startTime) { return Math.round(performance.now() - startTime) }and use that consistently. - I don't love that this (potentially) covers a bunch more code for the fallback case than for the non-fallback case. I am not sure that there is anything critical right now that would be included in this figure that shouldn't be, but perhaps it would be nice to guard against it if you can think of a not too complex way of getting the total duration from the first attempt at
downloadCodeQLin the error case.
There was a problem hiding this comment.
- Good idea, done.
- I had a look, but I think it's probably more complexity than it's worth.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Lots of good ideas, thank you for the detailed review! |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Download self-contained per-language bundles for eligible analyses on GitHub.com. Selection requires one explicitly configured language, a GitHub-hosted runner, zstd, and a supported language/platform combination. Release bundles require CodeQL 2.27.1 or newer; nightlies use the same eligibility rules without the release-version check. The
per_language_bundlesfeature flag remains off by default.Extract per-language bundles into the runner's temporary directory rather than the toolcache. If an automatically selected asset returns 404, fall back to the combined bundle from the same release. Explicit per-language
toolsURLs are not substituted.Add bundle-language and fallback telemetry, plus generated nightly PR checks for all ten languages covering extractor contents, toolcache isolation, database creation and analysis. Update existing cache-dependent checks to request multiple languages so they continue to use the combined bundle.
Risk assessment
Low risk: Automatic per-language bundle selection is feature-flagged and off by default. The unflagged behavior is limited to rare, explicitly supplied per-language bundle URLs and prevents those partial installs from entering the toolcache.
Which use cases does this change impact?
Workflow types:
Products:
Environments:
How did/will you validate this change?
If something goes wrong after this change is released, what are the mitigation and rollback strategies?
per_language_bundles.How will you know if something goes wrong after this change is released?
Are there any special considerations for merging or releasing this change?
Merge / deployment checklist