Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ec5eecb
Use per-language CodeQL bundles
henrymercer Sep 15, 2026
eb76062
Include failed bundle attempts in fallback timing
henrymercer Sep 15, 2026
289376d
Use per-language CodeQL bundles
henrymercer Sep 15, 2026
59ce3a2
Include failed bundle attempts in fallback timing
henrymercer Sep 15, 2026
2d47caf
Isolate per-language bundle eligibility state
henrymercer Sep 16, 2026
dfb9bf5
Share CodeQL bundle platform definitions
henrymercer Sep 16, 2026
f536ef4
Centralize CodeQL download telemetry fields
henrymercer Sep 16, 2026
2f552a9
Clarify bundle resolution and latest-nightly selection
henrymercer Sep 16, 2026
bd2ddba
Extract explicit CodeQL bundle URL classification
henrymercer Sep 16, 2026
89606bb
Return toolcache rejection reasons with Result
henrymercer Sep 16, 2026
f4fa111
Share elapsed-time rounding for bundle downloads
henrymercer Sep 16, 2026
69f4715
Use Result.orElse for bundle extraction paths
henrymercer Sep 16, 2026
5a2f29e
Merge published per-language bundle history
henrymercer Sep 16, 2026
28b8f59
Use shared test state for bundle eligibility
henrymercer Sep 17, 2026
ed3a24c
Group bundle eligibility test state overrides
henrymercer Sep 17, 2026
549d498
Simplify per-language platform eligibility checks
henrymercer Sep 17, 2026
ead1f7d
Rename the platform module
henrymercer Sep 17, 2026
79fe3a1
Move download telemetry into the status-report directory
henrymercer Sep 17, 2026
ecec9b5
Share per-language telemetry fields without renaming
henrymercer Sep 17, 2026
f18f353
Describe the bundle URL resolver
henrymercer Sep 17, 2026
07fa87d
Clarify the latest-nightly eligibility exception
henrymercer Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Isolate per-language bundle eligibility state
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
henrymercer and Copilot committed Sep 16, 2026
commit 2d47caf1235ac0fd1400d51b879c3c468927c125
48 changes: 26 additions & 22 deletions lib/entry-points.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/actions-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as github from "@actions/github";
import * as io from "@actions/io";

import type { Config } from "./config-utils";
import { Env, EnvVar, ActionsEnvVars } from "./environment";
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
import {
doesDirectoryExist,
Expand Down Expand Up @@ -292,7 +292,7 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
* `/opt/hostedtoolcache`.
*/
export function isGitHubHostedRunner(env: Env = getEnv()) {
export function isGitHubHostedRunner(env: ReadOnlyEnv = getEnv()) {
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
}

Expand Down
105 changes: 73 additions & 32 deletions src/per-language-bundles.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import test from "ava";

import { ActionsEnvVars } from "./environment";
import { ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Feature } from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import {
getPerLanguageBundleLanguage,
MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
PerLanguageBundleOptions,
tryGetBundleLanguageFromUrl,
} from "./per-language-bundles";
import { createFeatures, setupTests } from "./testing-utils";
import {
createFeatures,
getRecordingLogger,
getTestEnv,
LoggedMessage,
} from "./testing-utils";
import { GitHubVariant } from "./util";

setupTests(test);

/** Options for which we would use a per-language bundle. */
const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = {
rawLanguages: ["java"],
Expand All @@ -28,19 +30,21 @@ const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = {
async function checkEligibility(
overrides: Partial<PerLanguageBundleOptions>,
enabledFeatures: Feature[] = [Feature.PerLanguageBundles],
env: ReadOnlyEnv = getTestEnv({
[ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted",
}),
Comment thread
mbg marked this conversation as resolved.
Outdated
) {
return getPerLanguageBundleLanguage(
Comment thread
mbg marked this conversation as resolved.
{
env,
features: createFeatures(enabledFeatures),
logger: getRecordingLogger([], { logToConsole: false }),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Not needed if you don't inspect the log output. initAllState will automatically initialise a logger.

},
Comment thread
mbg marked this conversation as resolved.
Outdated
{ ...ELIGIBLE_OPTIONS, ...overrides },
createFeatures(enabledFeatures),
getRunnerLogger(true),
);
}

test.beforeEach(() => {
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
});

test.serial("uses Linux bundles for non-Swift languages", async (t) => {
test("getPerLanguageBundleLanguage selects Linux bundles for non-Swift languages", async (t) => {
for (const language of Object.values(BuiltInLanguage)) {
if (language === BuiltInLanguage.swift) {
continue;
Expand All @@ -49,14 +53,14 @@ test.serial("uses Linux bundles for non-Swift languages", async (t) => {
}
});

test.serial("normalizes an alias before selecting a bundle", async (t) => {
test("getPerLanguageBundleLanguage normalizes aliases before selecting a bundle", async (t) => {
Comment thread
mbg marked this conversation as resolved.
t.is(
await checkEligibility({ rawLanguages: ["java-kotlin"] }),
BuiltInLanguage.java,
);
});

test.serial("uses the macOS bundle for Swift", async (t) => {
test("getPerLanguageBundleLanguage selects the macOS bundle for Swift", async (t) => {
t.is(
await checkEligibility({ rawLanguages: ["swift"], platform: "osx64" }),
BuiltInLanguage.swift,
Expand All @@ -68,60 +72,91 @@ test.serial("uses the macOS bundle for Swift", async (t) => {
);
});

test.serial("only publishes non-Swift languages for Linux", async (t) => {
test("getPerLanguageBundleLanguage rejects unsupported platforms", async (t) => {
t.is(await checkEligibility({ platform: "osx64" }), undefined);
t.is(await checkEligibility({ platform: "win64" }), undefined);
// We do not publish per-language bundles for Linux Arm64 either.
t.is(await checkEligibility({ platform: "linux-arm64" }), undefined);
t.is(await checkEligibility({ platform: undefined }), undefined);
});

test.serial("requires exactly one language", async (t) => {
test("getPerLanguageBundleLanguage requires exactly one language", async (t) => {
t.is(await checkEligibility({ rawLanguages: undefined }), undefined);
t.is(await checkEligibility({ rawLanguages: [] }), undefined);
t.is(await checkEligibility({ rawLanguages: ["java", "python"] }), undefined);
});

test.serial("requires a language that CodeQL knows about", async (t) => {
test("getPerLanguageBundleLanguage requires a known language", async (t) => {
t.is(await checkEligibility({ rawLanguages: ["cobol"] }), undefined);
});

test.serial("requires a zstd bundle", async (t) => {
test("getPerLanguageBundleLanguage requires a zstd bundle", async (t) => {
t.is(await checkEligibility({ compressionMethod: "gzip" }), undefined);
});

test.serial("requires GitHub.com", async (t) => {
test("getPerLanguageBundleLanguage requires GitHub.com", async (t) => {
// Other products resolve the combined bundle against their own instance, so asking for a
// per-language bundle they do not mirror would move the download off that instance.
for (const variant of [GitHubVariant.GHES, GitHubVariant.GHEC_DR]) {
t.is(await checkEligibility({ variant }), undefined);
}
});

test.serial("requires a GitHub-hosted runner", async (t) => {
test("getPerLanguageBundleLanguage requires a GitHub-hosted runner", async (t) => {
// A self-hosted runner may have a toolcache that persists between jobs, which is worth more than
// a smaller download.
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "self-hosted";
t.is(await checkEligibility({}), undefined);
t.is(
await checkEligibility(
{},
[Feature.PerLanguageBundles],
getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
),
undefined,
);

// Self-hosted runners are routinely configured to look like hosted ones, for example by mounting
// a persistent volume at `/opt/hostedtoolcache`, so we require the service to tell us explicitly.
delete process.env[ActionsEnvVars.RUNNER_ENVIRONMENT];
process.env["RUNNER_TOOL_CACHE"] = "/opt/hostedtoolcache";
t.is(await checkEligibility({}), undefined);
t.is(
await checkEligibility(
{},
[Feature.PerLanguageBundles],
getTestEnv({ RUNNER_TOOL_CACHE: "/opt/hostedtoolcache" }),
),
undefined,
);
});

test.serial("requires a new enough CLI version", async (t) => {
test("getPerLanguageBundleLanguage requires a supported release version", async (t) => {
t.is(await checkEligibility({ cliVersion: undefined }), undefined);
t.is(await checkEligibility({ cliVersion: "2.27.0" }), undefined);
t.is(await checkEligibility({ cliVersion: "2.27.1" }), BuiltInLanguage.java);
});

test.serial("requires the feature flag", async (t) => {
test("getPerLanguageBundleLanguage requires the feature flag", async (t) => {
t.is(await checkEligibility({}, []), undefined);
});

test.serial("nightlies skip only the release version check", async (t) => {
test("getPerLanguageBundleLanguage explains a disabled feature before checking eligibility", async (t) => {
const messages: LoggedMessage[] = [];
const language = await getPerLanguageBundleLanguage(
{
env: getTestEnv(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: not required if it's not customised. initAllState will automatically call getTestEnv() to initialise env.

features: createFeatures([]),
logger: getRecordingLogger(messages, { logToConsole: false }),
},
{ ...ELIGIBLE_OPTIONS, rawLanguages: undefined, cliVersion: undefined },
);

t.is(language, undefined);
t.deepEqual(
messages.map((message) => message.message),
[
"Not using a per-language CodeQL bundle since the per_language_bundles feature is disabled.",
],
);
});

test("getPerLanguageBundleLanguage skips only the release version check for nightlies", async (t) => {
const nightly = { isNightly: true, cliVersion: undefined };
t.is(await checkEligibility(nightly), BuiltInLanguage.java);

Expand All @@ -136,11 +171,17 @@ test.serial("nightlies skip only the release version check", async (t) => {
t.is(await checkEligibility({ ...nightly, ...overrides }), undefined);
}
t.is(await checkEligibility(nightly, []), undefined);
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "self-hosted";
t.is(await checkEligibility(nightly), undefined);
t.is(
await checkEligibility(
nightly,
[Feature.PerLanguageBundles],
getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
),
undefined,
);
});

test.serial("recognizes a per-language bundle from its URL", (t) => {
test("tryGetBundleLanguageFromUrl recognizes per-language bundle URLs", (t) => {
const url = (name: string) =>
`https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${name}`;

Expand All @@ -165,7 +206,7 @@ test.serial("recognizes a per-language bundle from its URL", (t) => {
);
});

test.serial("does not mistake other bundles for per-language ones", (t) => {
test("tryGetBundleLanguageFromUrl rejects other bundle URLs", (t) => {
const url = (name: string) =>
`https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${name}`;

Expand Down
21 changes: 12 additions & 9 deletions src/per-language-bundles.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as semver from "semver";

import { ActionState } from "./action-common";
import { isGitHubHostedRunner } from "./actions-util";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Feature } from "./feature-flags";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { Logger } from "./logging";
import * as tar from "./tar";
import { GitHubVariant } from "./util";

Expand Down Expand Up @@ -61,9 +61,12 @@ export interface PerLanguageBundleOptions {

/** Returns the eligible bundle language, or undefined for the combined bundle. */
export async function getPerLanguageBundleLanguage(
{
env,
features,
logger,
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
options: PerLanguageBundleOptions,
features: FeatureEnablement,
logger: Logger,
): Promise<BuiltInLanguage | undefined> {
const {
rawLanguages,
Expand All @@ -79,6 +82,10 @@ export async function getPerLanguageBundleLanguage(
return undefined;
};

if (!(await features.getValue(Feature.PerLanguageBundles))) {
return explain(`the ${Feature.PerLanguageBundles} feature is disabled`);
}

if (rawLanguages?.length !== 1) {
return explain(
`exactly one language must be requested via the 'languages' input, but ${
Expand All @@ -103,7 +110,7 @@ export async function getPerLanguageBundleLanguage(
return explain(`we are running against ${variant}`);
}

if (!isGitHubHostedRunner()) {
if (!isGitHubHostedRunner(env)) {
// 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");
Expand Down Expand Up @@ -134,9 +141,5 @@ export async function getPerLanguageBundleLanguage(
);
}

if (!(await features.getValue(Feature.PerLanguageBundles))) {
return explain(`the ${Feature.PerLanguageBundles} feature is disabled`);
}

return language;
}
Loading