mirror of
https://github.com/github/codeql-action
synced 2026-10-31 20:01:33 +03:00
Compare commits
134
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486fec2a3e | ||
|
|
134624c67b | ||
|
|
ff43db8f98 | ||
|
|
4605e03a74 | ||
|
|
099c869cad | ||
|
|
db488ddef3 | ||
|
|
1845f5ba8b | ||
|
|
79a73408b4 | ||
|
|
f9d9f07d37 | ||
|
|
9ee088e136 | ||
|
|
1aef003397 | ||
|
|
508b83bc41 | ||
|
|
d97b3428e8 | ||
|
|
47fa622223 | ||
|
|
45693cc688 | ||
|
|
c2fd8f54d1 | ||
|
|
c56f48e9bd | ||
|
|
aa0eadc572 | ||
|
|
43250d671a | ||
|
|
5008effa71 | ||
|
|
053d41e61e | ||
|
|
1158e1c92a | ||
|
|
ff2f1c621b | ||
|
|
2d49edbac6 | ||
|
|
951a133f96 | ||
|
|
be7a3dbb81 | ||
|
|
9310334b11 | ||
|
|
6dc633238e | ||
|
|
b4d8a54218 | ||
|
|
ab5db2519c | ||
|
|
6c0d9018d4 | ||
|
|
bf96b0df93 | ||
|
|
337136ab8a | ||
|
|
a9baab8dee | ||
|
|
33d70867d5 | ||
|
|
38055a3c3c | ||
|
|
1f87aed5e6 | ||
|
|
dc1b98ad1c | ||
|
|
6f0220ee37 | ||
|
|
ca1c97228c | ||
|
|
0e8a5d99f8 | ||
|
|
54a084632e | ||
|
|
40f80a8df0 | ||
|
|
b222c3aaea | ||
|
|
11569df0a1 | ||
|
|
0a99875ae5 | ||
|
|
246018e041 | ||
|
|
1332611f51 | ||
|
|
4dc327a942 | ||
|
|
bb19330c5e | ||
|
|
0e85c0e99c | ||
|
|
bfcd769ba1 | ||
|
|
c16c0f3f28 | ||
|
|
208a88adc7 | ||
|
|
f47bb7b9aa | ||
|
|
c205ff6f09 | ||
|
|
b672c70acd | ||
|
|
7131139037 | ||
|
|
b5225f21c5 | ||
|
|
acb38565c9 | ||
|
|
9183a7b6e1 | ||
|
|
5f8c44ba62 | ||
|
|
794f5bc385 | ||
|
|
54109818e0 | ||
|
|
99caaa8b90 | ||
|
|
6117bb503a | ||
|
|
af767ec1f6 | ||
|
|
7d9249f5a5 | ||
|
|
8ebf1091b0 | ||
|
|
bdf39710a2 | ||
|
|
74cfae9be6 | ||
|
|
47a0a833bb | ||
|
|
6a90bf1f54 | ||
|
|
c5995f544d | ||
|
|
76c44396d3 | ||
|
|
fad141fa6c | ||
|
|
7d82f1132f | ||
|
|
37bdbde050 | ||
|
|
5595ccaf91 | ||
|
|
ec9c75796a | ||
|
|
45c8742e17 | ||
|
|
6a9359a1bd | ||
|
|
065cdc0394 | ||
|
|
f99dd5aeee | ||
|
|
1804b211a3 | ||
|
|
3020a2f462 | ||
|
|
93c3a5a40b | ||
|
|
d1ba80a13d | ||
|
|
e74600b0d9 | ||
|
|
266c7bdbd2 | ||
|
|
daa7fe6fba | ||
|
|
1cd4d01d58 | ||
|
|
d2bfc30bc3 | ||
|
|
68028fcb16 | ||
|
|
c29563eeaa | ||
|
|
155e522997 | ||
|
|
2d3b351ea6 | ||
|
|
5d3eb98e4a | ||
|
|
c5f739bd64 | ||
|
|
2e53f1cea2 | ||
|
|
d0ee43ac62 | ||
|
|
06f1d4ffed | ||
|
|
82f035a501 | ||
|
|
60a57910be | ||
|
|
3502f79575 | ||
|
|
8f0a4f23c4 | ||
|
|
780c00da3f | ||
|
|
f02afd42b0 | ||
|
|
d57c3ffcba | ||
|
|
8e6fdffc32 | ||
|
|
ba46ff760e | ||
|
|
b411bbcd4a | ||
|
|
0cebd1d28d | ||
|
|
d2f5cbbe91 | ||
|
|
36737508ec | ||
|
|
30c33c9286 | ||
|
|
3ca82bb259 | ||
|
|
42a3b94790 | ||
|
|
13d4882649 | ||
|
|
e55a57b808 | ||
|
|
51d51e8121 | ||
|
|
e893985e8b | ||
|
|
eb692f8b49 | ||
|
|
aac07d2a41 | ||
|
|
de57c4a441 | ||
|
|
2e251072b0 | ||
|
|
94a12eb6f6 | ||
|
|
e28cbacfa1 | ||
|
|
60834a0cd9 | ||
|
|
e9831f72a2 | ||
|
|
766928d055 | ||
|
|
049af32c59 | ||
|
|
c7ae51bb2d | ||
|
|
1f9caf0118 |
No files matched your search
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
+2
-2
@@ -71,8 +71,8 @@ jobs:
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/results"
|
||||
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
|
||||
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'."
|
||||
if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
|
||||
exit 1
|
||||
else
|
||||
echo "Found job run UUID '$actual'."
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ jobs:
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: github/enterprise-releases
|
||||
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
|
||||
token: ${{ secrets.CODEQL_CI_ENTERPRISE_RELEASE_PAT }}
|
||||
path: ${{ github.workspace }}/enterprise-releases/
|
||||
sparse-checkout: releases.json
|
||||
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.8 - 21 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.7 - 13 Aug 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3). [#4085](https://github.com/github/codeql-action/pull/4085)
|
||||
|
||||
## 4.37.6 - 04 Aug 2026
|
||||
|
||||
- Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to `.github/codeql-config.yml` to align it with the suggested path that is used elsewhere. [#4070](https://github.com/github/codeql-action/pull/4070)
|
||||
|
||||
## 4.37.5 - 03 Aug 2026
|
||||
|
||||
- Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the `init` Action instead of falling back to downloading the bundle before extracting it. [#4061](https://github.com/github/codeql-action/pull/4061)
|
||||
|
||||
## 4.37.4 - 29 Jul 2026
|
||||
|
||||
- This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037)
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.2",
|
||||
"cliVersion": "2.26.2",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.1",
|
||||
"priorCliVersion": "2.26.1"
|
||||
"bundleVersion": "codeql-bundle-v2.26.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
}
|
||||
Generated
+2569
-1721
File diff suppressed because it is too large.
Load diff
Generated
+387
-351
File diff suppressed because it is too large.
Load diff
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.4",
|
||||
"version": "4.37.9",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -30,22 +30,22 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.2.1",
|
||||
"js-yaml": "^5.2.3",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.1",
|
||||
"undici": "^6.24.0"
|
||||
"undici": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -58,22 +58,22 @@
|
||||
"@types/node": "^20.19.43",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.1",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"globals": "^17.11.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.64.0"
|
||||
"typescript-eslint": "^8.67.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
@@ -95,6 +95,6 @@
|
||||
"semver": ">=6.3.1"
|
||||
},
|
||||
"glob": "^13.0.6",
|
||||
"undici": "^6.24.0"
|
||||
"undici": "^6.28.0"
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ steps:
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/results"
|
||||
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
|
||||
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'."
|
||||
if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
|
||||
exit 1
|
||||
else
|
||||
echo "Found job run UUID '$actual'."
|
||||
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.5",
|
||||
@@ -12,6 +12,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.23.1"
|
||||
"tsx": "^4.23.12"
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"03ad4de0992f5dab5e18fcb136590ce7c4a0ac95",
|
||||
"v5.6.0",
|
||||
"b6effb05e454b25005698d916606bdc6ffcbf961",
|
||||
"v5.7.0",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import * as core from "@actions/core";
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import * as common from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as environment from "./environment";
|
||||
import * as logging from "./logging";
|
||||
import { ActionName } from "./status-report";
|
||||
import * as statusReport from "./status-report";
|
||||
import {
|
||||
getTestActionsEnv,
|
||||
getTestEnv,
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
import { getErrorMessage } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
interface RunInActionsTestOpts {
|
||||
runFn?: () => Promise<any>;
|
||||
expectedErrorMessage?: string;
|
||||
expectedTelemetryError?: string;
|
||||
}
|
||||
|
||||
const runInActionsMacro = makeMacro({
|
||||
exec: async (t, opts: RunInActionsTestOpts) => {
|
||||
const expectFailure = opts?.expectedErrorMessage !== undefined;
|
||||
|
||||
const logger = new RecordingLogger();
|
||||
const getActionsLogger = sinon
|
||||
.stub(logging, "getActionsLogger")
|
||||
.returns(logger);
|
||||
|
||||
const env = getTestEnv();
|
||||
const getEnv = sinon.stub(environment, "getEnv").returns(env);
|
||||
|
||||
const actionsEnv = getTestActionsEnv(env);
|
||||
const getActionsEnv = sinon
|
||||
.stub(actionsUtil, "getActionsEnv")
|
||||
.returns(actionsEnv);
|
||||
|
||||
const getJobUUID = sinon
|
||||
.stub(statusReport, "getJobUUID")
|
||||
.returns("test-job-uuid");
|
||||
|
||||
const setFailed = sinon.stub(core, "setFailed");
|
||||
const sendUnhandledErrorStatusReport = sinon.stub(
|
||||
statusReport,
|
||||
"sendUnhandledErrorStatusReport",
|
||||
);
|
||||
|
||||
const name = ActionName.Init;
|
||||
const run = sinon.stub();
|
||||
|
||||
if (opts?.runFn) {
|
||||
run.callsFake(opts.runFn);
|
||||
}
|
||||
|
||||
const transformTelemetryError = sinon
|
||||
.stub()
|
||||
.callsFake((err) => opts?.expectedTelemetryError ?? getErrorMessage(err));
|
||||
const testAction: common.Action = {
|
||||
name,
|
||||
run,
|
||||
transformTelemetryError,
|
||||
};
|
||||
|
||||
await common.runInActions(testAction);
|
||||
|
||||
// These always should have been called once.
|
||||
t.true(getActionsLogger.calledOnce);
|
||||
t.true(getEnv.calledOnce);
|
||||
t.true(getActionsEnv.calledOnce);
|
||||
|
||||
const expectedActionState = {
|
||||
actions: actionsEnv,
|
||||
env,
|
||||
logger,
|
||||
name: ActionName.Init,
|
||||
};
|
||||
|
||||
t.true(getJobUUID.calledOnceWithExactly(sinon.match(expectedActionState)));
|
||||
t.true(run.calledOnceWithExactly(sinon.match(expectedActionState)));
|
||||
|
||||
t.is(setFailed.calledOnce, expectFailure ?? false);
|
||||
t.is(sendUnhandledErrorStatusReport.calledOnce, expectFailure ?? false);
|
||||
|
||||
if (expectFailure) {
|
||||
t.true(
|
||||
setFailed.calledOnceWithExactly(
|
||||
`${statusReport.getDisplayActionName(name)} action failed: ${opts?.expectedErrorMessage}`,
|
||||
),
|
||||
);
|
||||
t.true(
|
||||
sendUnhandledErrorStatusReport.calledOnceWithExactly(
|
||||
name,
|
||||
sinon.match.any,
|
||||
opts?.expectedTelemetryError ?? opts?.expectedErrorMessage,
|
||||
logger,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
title: (providedTitle) => `runInActions - ${providedTitle}`,
|
||||
});
|
||||
|
||||
runInActionsMacro.serial("calls run", {});
|
||||
runInActionsMacro.serial("handles run exceptions", {
|
||||
runFn: () => {
|
||||
throw new Error("Test failure");
|
||||
},
|
||||
expectedErrorMessage: "Test failure",
|
||||
});
|
||||
runInActionsMacro.serial("transforms run exceptions", {
|
||||
runFn: () => {
|
||||
throw new Error("Test failure");
|
||||
},
|
||||
expectedErrorMessage: "Test failure",
|
||||
expectedTelemetryError: "Transformed failure message",
|
||||
});
|
||||
+26
-4
@@ -8,9 +8,10 @@ import { getActionsLogger, Logger } from "./logging";
|
||||
import {
|
||||
ActionName,
|
||||
getDisplayActionName,
|
||||
getJobUUID,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage } from "./util";
|
||||
import { getEnv, getErrorMessage, wrapError } from "./util";
|
||||
|
||||
/** Base state that is available to an Action on startup. */
|
||||
export interface BaseState {
|
||||
@@ -78,6 +79,12 @@ export interface Action {
|
||||
name: ActionName;
|
||||
/** The entry point for the Action. */
|
||||
run: ActionMain;
|
||||
/**
|
||||
* An optional function that transforms a caught error into a message suitable for
|
||||
* inclusion in a status report. This is primarily intended for the `start-proxy`
|
||||
* action to replace the thrown `Error`'s message with a safe one.
|
||||
*/
|
||||
transformTelemetryError?: (error: Error) => string;
|
||||
}
|
||||
|
||||
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
|
||||
@@ -88,17 +95,32 @@ export async function runInActions(action: Action) {
|
||||
const actionsEnv = getActionsEnv();
|
||||
|
||||
try {
|
||||
await action.run({
|
||||
const actionState = {
|
||||
name: action.name,
|
||||
startedAt,
|
||||
logger,
|
||||
env,
|
||||
actions: actionsEnv,
|
||||
});
|
||||
};
|
||||
|
||||
// Create a unique identifier for this run.
|
||||
getJobUUID(actionState);
|
||||
|
||||
await action.run(actionState);
|
||||
} catch (error) {
|
||||
core.setFailed(
|
||||
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
|
||||
|
||||
const statusReportError =
|
||||
action.transformTelemetryError !== undefined
|
||||
? action.transformTelemetryError(wrapError(error))
|
||||
: error;
|
||||
await sendUnhandledErrorStatusReport(
|
||||
action.name,
|
||||
startedAt,
|
||||
statusReportError,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -27,14 +27,20 @@ declare const __CODEQL_ACTION_VERSION__: string;
|
||||
* global functions in tests.
|
||||
*/
|
||||
export interface ActionsEnv {
|
||||
getRequiredInput: (name: string) => string;
|
||||
getOptionalInput: (name: string) => string | undefined;
|
||||
exportVariable: (name: string, value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the real `ActionsEnv` used by production code.
|
||||
*/
|
||||
export function getActionsEnv(): ActionsEnv {
|
||||
return { getOptionalInput };
|
||||
return {
|
||||
getRequiredInput,
|
||||
getOptionalInput,
|
||||
exportVariable: core.exportVariable,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function runWrapper() {
|
||||
logger,
|
||||
);
|
||||
if (config !== undefined) {
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const version = await codeql.getVersion();
|
||||
await debugArtifacts.uploadCombinedSarifArtifacts(
|
||||
logger,
|
||||
|
||||
@@ -255,7 +255,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
if (hasBadExpectErrorInput()) {
|
||||
throw new util.ConfigurationError(
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
|
||||
{"maximumVersion":"3.23","minimumVersion":"3.17"}
|
||||
@@ -99,7 +99,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
languages = await determineAutobuildLanguages(codeql, config, logger);
|
||||
if (languages !== undefined) {
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ export async function runAutobuild(
|
||||
logger: Logger,
|
||||
) {
|
||||
logger.startGroup(`Attempting to automatically build ${language} code`);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd);
|
||||
const codeQL = await getCodeQL(logger, config.codeQLCmd);
|
||||
if (language === BuiltInLanguage.cpp) {
|
||||
await setupCppAutobuild(codeQL, logger);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { EnvVar } from "../environment";
|
||||
import { getRunnerLogger } from "../logging";
|
||||
import { getTestEnv, setupTests } from "../testing-utils";
|
||||
import * as util from "../util";
|
||||
|
||||
import * as outputCache from "./output-cache";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "codeql-action-command-cache.json");
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.deepEqual(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
{
|
||||
version: "2.20.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
fs.writeFileSync(cacheFile, "not valid json", "utf8");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
|
||||
const testValues = [
|
||||
{ cmd: "/path/to/codeql" },
|
||||
{ entries: { version: { version: "2.20.0" } } },
|
||||
{ cmd: "/path/to/codeql", entries: {} },
|
||||
{ cmd: "/path/to/codeql", entries: null },
|
||||
{ cmd: "/path/to/codeql", entries: { version: {} } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: null } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: "2.20.0" } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: null } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2.2 } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2 } } },
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", overlayVersion: "1" } },
|
||||
},
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", features: "nope" } },
|
||||
},
|
||||
].map((v) => JSON.stringify(v));
|
||||
|
||||
for (const value of testValues) {
|
||||
fs.writeFileSync(cacheFile, value, "utf8");
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
value,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.notThrows(() => {
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
import type { VersionInfo } from "./types";
|
||||
|
||||
/**
|
||||
* The keys of the command cache. Each key corresponds to a command whose output we cache.
|
||||
*/
|
||||
export type CommandCacheKey = string;
|
||||
|
||||
/**
|
||||
* The type of the command cache that is persisted to disk.
|
||||
*/
|
||||
export interface OutputCache {
|
||||
cmd: string;
|
||||
entries: Record<CommandCacheKey, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the temporary file that backs the on-disk cache of
|
||||
* CLI responses between workflow steps.
|
||||
*/
|
||||
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
||||
|
||||
/**
|
||||
* The module-global variable that caches the CodeQL CLI version in-memory.
|
||||
*/
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the temporary file that backs the
|
||||
* on-disk cache of CLI responses between workflow steps.
|
||||
*/
|
||||
function getCommandCacheFilePath(env: Env): string {
|
||||
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the CodeQL CLI version both in-memory and on disk.
|
||||
* @param env The environment variables to use.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
* @param version The version information to cache.
|
||||
*/
|
||||
export function cacheCodeQlVersion(
|
||||
env: Env,
|
||||
cmd: string,
|
||||
version: VersionInfo,
|
||||
): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
const outputCache = {
|
||||
cmd,
|
||||
entries: { version },
|
||||
} satisfies OutputCache;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
fs.writeFileSync(
|
||||
getCommandCacheFilePath(env),
|
||||
JSON.stringify(outputCache),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached CodeQL CLI version, if any.
|
||||
* @param logger The logger to use for logging messages.
|
||||
* @param env The environment variables to use.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
*/
|
||||
export function getCachedCodeQlVersion(
|
||||
logger: Logger,
|
||||
env: Env,
|
||||
cmd?: string,
|
||||
): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
let serialized: string;
|
||||
try {
|
||||
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
|
||||
} catch (e) {
|
||||
logger.debug(
|
||||
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch (e) {
|
||||
logger.debug(`Cannot parse CLI-cache data as JSON: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isOutputCache(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.entries.version as VersionInfo;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `VersionInfo` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const candidate = x as Partial<VersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.version === "string" &&
|
||||
(candidate.features === undefined ||
|
||||
(typeof candidate.features === "object" &&
|
||||
candidate.features !== null)) &&
|
||||
(candidate.overlayVersion === undefined ||
|
||||
typeof candidate.overlayVersion === "number")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `OutputCache` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isOutputCache(x: unknown): x is OutputCache {
|
||||
const candidate = x as Partial<OutputCache> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
candidate.entries !== undefined &&
|
||||
isVersionInfo(candidate.entries.version)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
+25
-4
@@ -51,6 +51,31 @@ test.beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - true for expected errors", async (t) => {
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(new Error("ENOSPC: Out of disk space")),
|
||||
);
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(
|
||||
new Error(
|
||||
"EACCES: permission denied, mkdir /opt/hostedtoolcache/CodeQL/",
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - false for other errors", async (t) => {
|
||||
t.false(codeql.isDiskConfigurationError("Not an Error instance"));
|
||||
|
||||
const otherMessages = [
|
||||
"Does not contain an error code we test for",
|
||||
"ENOSP: Not quite the full error code",
|
||||
];
|
||||
for (const otherMessage of otherMessages) {
|
||||
t.false(codeql.isDiskConfigurationError(new Error(otherMessage)));
|
||||
}
|
||||
});
|
||||
|
||||
async function installIntoToolcache({
|
||||
apiDetails = SAMPLE_DOTCOM_API_DETAILS,
|
||||
cliVersion,
|
||||
@@ -580,7 +605,6 @@ const injectedConfigMacro = makeMacro({
|
||||
"",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -856,7 +880,6 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
"/path/to/qlconfig.yml",
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -887,7 +910,6 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
undefined, // undefined qlconfigFile
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as any[];
|
||||
@@ -1066,7 +1088,6 @@ test.serial(
|
||||
"sourceRoot",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(false),
|
||||
);
|
||||
|
||||
t.true(runnerConstructorStub.calledOnce);
|
||||
|
||||
+33
-26
@@ -12,10 +12,12 @@ import {
|
||||
runTool,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import * as outputCache from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import { CliError, wrapCliConfigurationError } from "./cli-errors";
|
||||
import { appendExtraQueryExclusions, type Config } from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { isAnalyzingDefaultBranch } from "./git-utils";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "./overlay";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import * as setupCodeql from "./setup-codeql";
|
||||
@@ -91,7 +93,6 @@ export interface CodeQL {
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Runs the autobuilder for the given language.
|
||||
@@ -215,20 +216,6 @@ export interface CodeQL {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
|
||||
export interface ResolveDatabaseOutput {
|
||||
overlayBaseSpecifier?: string;
|
||||
}
|
||||
@@ -286,6 +273,26 @@ const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
|
||||
/**
|
||||
* Decides whether `e` is a disk-related error outside of our control
|
||||
* that should be classified as a `ConfigurationError`.
|
||||
*
|
||||
* @param e The error to check.
|
||||
* @returns True if the error should be treated as a `ConfigurationError` or false if not.
|
||||
*/
|
||||
export function isDiskConfigurationError(e: unknown): boolean {
|
||||
if (!(e instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
// out of disk space
|
||||
e.message.includes("ENOSPC") ||
|
||||
// access denied
|
||||
e.message.includes("EACCES")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up CodeQL CLI access.
|
||||
*
|
||||
@@ -346,7 +353,7 @@ export async function setupCodeQL(
|
||||
);
|
||||
}
|
||||
|
||||
cachedCodeQL = await getCodeQLForCmd(codeqlCmd, checkVersion);
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, codeqlCmd, checkVersion);
|
||||
return {
|
||||
codeql: cachedCodeQL,
|
||||
toolsDownloadStatusReport,
|
||||
@@ -356,8 +363,7 @@ export async function setupCodeQL(
|
||||
} catch (rawError) {
|
||||
const e = api.wrapApiConfigurationError(rawError);
|
||||
const ErrorClass =
|
||||
e instanceof util.ConfigurationError ||
|
||||
(e instanceof Error && e.message.includes("ENOSPC")) // out of disk space
|
||||
e instanceof util.ConfigurationError || isDiskConfigurationError(e)
|
||||
? util.ConfigurationError
|
||||
: Error;
|
||||
|
||||
@@ -372,9 +378,9 @@ export async function setupCodeQL(
|
||||
/**
|
||||
* Use the CodeQL executable located at the given path.
|
||||
*/
|
||||
export async function getCodeQL(cmd: string): Promise<CodeQL> {
|
||||
export async function getCodeQL(logger: Logger, cmd: string): Promise<CodeQL> {
|
||||
if (cachedCodeQL === undefined) {
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true);
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, cmd, true);
|
||||
}
|
||||
return cachedCodeQL;
|
||||
}
|
||||
@@ -481,8 +487,9 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||
*/
|
||||
export async function getCodeQLForTesting(
|
||||
cmd = "codeql-for-testing",
|
||||
logger: Logger = getRunnerLogger(true),
|
||||
): Promise<CodeQL> {
|
||||
return getCodeQLForCmd(cmd, false);
|
||||
return getCodeQLForCmd(logger, cmd, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,6 +501,7 @@ export async function getCodeQLForTesting(
|
||||
* @returns A new CodeQL object
|
||||
*/
|
||||
async function getCodeQLForCmd(
|
||||
logger: Logger,
|
||||
cmd: string,
|
||||
checkVersion: boolean,
|
||||
): Promise<CodeQL> {
|
||||
@@ -502,7 +510,7 @@ async function getCodeQLForCmd(
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = util.getCachedCodeQlVersion(cmd);
|
||||
let result = outputCache.getCachedCodeQlVersion(logger, getEnv(), cmd);
|
||||
if (result === undefined) {
|
||||
result = await runCliJson<VersionInfo>(
|
||||
cmd,
|
||||
@@ -511,7 +519,7 @@ async function getCodeQLForCmd(
|
||||
noStreamStdout: true,
|
||||
},
|
||||
);
|
||||
util.cacheCodeQlVersion(cmd, result);
|
||||
outputCache.cacheCodeQlVersion(getEnv(), cmd, result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -539,7 +547,6 @@ async function getCodeQLForCmd(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
) {
|
||||
const extraArgs = config.languages.map(
|
||||
(language) => `--language=${language}`,
|
||||
|
||||
@@ -1295,13 +1295,12 @@ checkOverlayEnablementMacro.serial(
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"No overlay-base database on default branch if runner disk space is below v2 limit and v2 resource checks enabled",
|
||||
"No overlay-base database on default branch if runner disk space is below minimum",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1315,13 +1314,12 @@ checkOverlayEnablementMacro.serial(
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"Overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks enabled",
|
||||
"Overlay-base database on default branch if runner disk space is above minimum",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1335,17 +1333,93 @@ checkOverlayEnablementMacro.serial(
|
||||
},
|
||||
);
|
||||
|
||||
// Check that each feature flag lowers the limit to the threshold that its name
|
||||
// declares. Both sides of the boundary are needed to pin the threshold down: a
|
||||
// mapping to a lower value would still pass the case at the limit, and one to a
|
||||
// higher value would still fail the case below it.
|
||||
for (const [feature, thresholdGb] of [
|
||||
[Feature.OverlayAnalysisMinDisk8Gb, 8],
|
||||
[Feature.OverlayAnalysisMinDisk9Gb, 9],
|
||||
[Feature.OverlayAnalysisMinDisk10Gb, 10],
|
||||
[Feature.OverlayAnalysisMinDisk11Gb, 11],
|
||||
[Feature.OverlayAnalysisMinDisk12Gb, 12],
|
||||
[Feature.OverlayAnalysisMinDisk13Gb, 13],
|
||||
] as Array<[Feature, number]>) {
|
||||
const features = [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
feature,
|
||||
];
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
`Overlay-base database on default branch if ${feature} is enabled and runner disk space is at its limit`,
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features,
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: thresholdGb * 1_000_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
useOverlayDatabaseCaching: true,
|
||||
},
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
`No overlay-base database on default branch if ${feature} is enabled and runner disk space is below its limit`,
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features,
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: thresholdGb * 1_000_000_000 - 1_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
disabledReason: OverlayDisabledReason.InsufficientDiskSpace,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"No overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks not enabled",
|
||||
"Overlay-base database on default branch if runner disk space is exactly at the lowest limit enabled by a feature flag",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisMinDisk9Gb,
|
||||
Feature.OverlayAnalysisMinDisk12Gb,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: 15_000_000_000,
|
||||
numAvailableBytes: 9_000_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
useOverlayDatabaseCaching: true,
|
||||
},
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"No overlay-base database on default branch if runner disk space is below the lowest limit enabled by a feature flag",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisMinDisk9Gb,
|
||||
Feature.OverlayAnalysisMinDisk12Gb,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: 8_500_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
|
||||
+46
-24
@@ -48,7 +48,7 @@ import {
|
||||
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import * as errorMessages from "./error-messages";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
@@ -101,19 +101,23 @@ export { type Config } from "./config/action-config";
|
||||
* whether to perform overlay analysis, then the action will not perform overlay
|
||||
* analysis unless overlay analysis has been explicitly enabled via environment
|
||||
* variable.
|
||||
*
|
||||
* This threshold can be lowered by the feature flags in
|
||||
* `OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE`.
|
||||
*/
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 20000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES =
|
||||
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB * 1_000_000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 14000;
|
||||
|
||||
/**
|
||||
* The v2 minimum available disk space (in MB) required to perform overlay
|
||||
* analysis. This is a lower threshold than the v1 limit, allowing overlay
|
||||
* analysis to run on runners with less available disk space.
|
||||
* Minimum available disk space (in MB) enabled by each overlay feature flag.
|
||||
*/
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB = 14000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES =
|
||||
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB * 1_000_000;
|
||||
const OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE = {
|
||||
[Feature.OverlayAnalysisMinDisk8Gb]: 8000,
|
||||
[Feature.OverlayAnalysisMinDisk9Gb]: 9000,
|
||||
[Feature.OverlayAnalysisMinDisk10Gb]: 10000,
|
||||
[Feature.OverlayAnalysisMinDisk11Gb]: 11000,
|
||||
[Feature.OverlayAnalysisMinDisk12Gb]: 12000,
|
||||
[Feature.OverlayAnalysisMinDisk13Gb]: 13000,
|
||||
} satisfies Partial<Record<FeatureWithoutCLI, number>>;
|
||||
|
||||
/**
|
||||
* The minimum memory (in MB) that must be available for CodeQL to perform overlay analysis. If
|
||||
@@ -588,24 +592,44 @@ async function checkOverlayAnalysisFeatureEnabled(
|
||||
return new Success(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum available disk space (in MB) required to perform overlay
|
||||
* analysis, which is the lowest threshold enabled by a feature flag, or the
|
||||
* default threshold if no such feature flag is enabled.
|
||||
*/
|
||||
async function getMinimumDiskSpaceMb(
|
||||
features: FeatureEnablement,
|
||||
): Promise<number> {
|
||||
let minimumMb = OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB;
|
||||
for (const [feature, thresholdMb] of Object.entries(
|
||||
OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE,
|
||||
)) {
|
||||
if (await features.getValue(feature as FeatureWithoutCLI)) {
|
||||
minimumMb = Math.min(minimumMb, thresholdMb);
|
||||
}
|
||||
}
|
||||
return minimumMb;
|
||||
}
|
||||
|
||||
/** Checks if the runner has enough disk space for overlay analysis. */
|
||||
function runnerHasSufficientDiskSpace(
|
||||
diskUsage: DiskUsage,
|
||||
logger: Logger,
|
||||
useV2ResourceChecks: boolean,
|
||||
minimumDiskSpaceMb: number,
|
||||
): boolean {
|
||||
const minimumDiskSpaceBytes = useV2ResourceChecks
|
||||
? OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES
|
||||
: OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES;
|
||||
if (diskUsage.numAvailableBytes < minimumDiskSpaceBytes) {
|
||||
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
|
||||
const minimumDiskSpaceMb = Math.round(minimumDiskSpaceBytes / 1_000_000);
|
||||
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
|
||||
if (diskUsage.numAvailableBytes < minimumDiskSpaceMb * 1_000_000) {
|
||||
logger.info(
|
||||
`Setting overlay database mode to ${OverlayDatabaseMode.None} ` +
|
||||
`due to insufficient disk space (${diskSpaceMb} MB, needed ${minimumDiskSpaceMb} MB).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Disk space available for CodeQL analysis is ${diskSpaceMb} MB, which is at or above the ` +
|
||||
`minimum of ${minimumDiskSpaceMb} MB.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -637,7 +661,7 @@ async function runnerHasSufficientMemory(
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
|
||||
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is at or above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -648,12 +672,13 @@ async function runnerHasSufficientMemory(
|
||||
*/
|
||||
async function checkRunnerResources(
|
||||
codeql: CodeQL,
|
||||
features: FeatureEnablement,
|
||||
diskUsage: DiskUsage,
|
||||
ramInput: string | undefined,
|
||||
logger: Logger,
|
||||
useV2ResourceChecks: boolean,
|
||||
): Promise<Result<void, OverlayDisabledReason>> {
|
||||
if (!runnerHasSufficientDiskSpace(diskUsage, logger, useV2ResourceChecks)) {
|
||||
const minimumDiskSpaceMb = await getMinimumDiskSpaceMb(features);
|
||||
if (!runnerHasSufficientDiskSpace(diskUsage, logger, minimumDiskSpaceMb)) {
|
||||
return new Failure(OverlayDisabledReason.InsufficientDiskSpace);
|
||||
}
|
||||
if (!(await runnerHasSufficientMemory(codeql, ramInput, logger))) {
|
||||
@@ -752,9 +777,6 @@ export async function checkOverlayEnablement(
|
||||
Feature.OverlayAnalysisSkipResourceChecks,
|
||||
codeql,
|
||||
));
|
||||
const useV2ResourceChecks = await features.getValue(
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
);
|
||||
const checkOverlayStatus = await features.getValue(
|
||||
Feature.OverlayAnalysisStatusCheck,
|
||||
);
|
||||
@@ -770,10 +792,10 @@ export async function checkOverlayEnablement(
|
||||
performResourceChecks && diskUsage !== undefined
|
||||
? await checkRunnerResources(
|
||||
codeql,
|
||||
features,
|
||||
diskUsage,
|
||||
ramInput,
|
||||
logger,
|
||||
useV2ResourceChecks,
|
||||
)
|
||||
: new Success<void>(undefined);
|
||||
if (resourceResult.isFailure()) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { getActionsEnv } from "../actions-util";
|
||||
import { ActionsEnv } from "../actions-util";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { RepositoryPropertyName } from "../feature-flags/properties";
|
||||
import { callee } from "../testing-utils";
|
||||
@@ -22,32 +22,26 @@ const expectedRepositoryPropertyResult: ComputedInput = {
|
||||
value: "repo-property-input-value",
|
||||
};
|
||||
|
||||
function stubGetToolsInput() {
|
||||
const actions = getActionsEnv();
|
||||
function stubGetToolsInput(actions: ActionsEnv) {
|
||||
sinon
|
||||
.stub(actions, "getOptionalInput")
|
||||
.withArgs(InputName.Tools)
|
||||
.returns(expectedWorkflowResult.value);
|
||||
return actions;
|
||||
}
|
||||
|
||||
const workflowLogMessage = `Using ${InputName.Tools} input from workflow:`;
|
||||
|
||||
test("getToolsInput - returns workflow input if available", async (t) => {
|
||||
const actions = stubGetToolsInput();
|
||||
|
||||
await callee(getToolsInput)
|
||||
.withActions(actions)
|
||||
.withActions(stubGetToolsInput)
|
||||
.withArgs({})
|
||||
.logs(t, workflowLogMessage)
|
||||
.passes(t.deepEqual, expectedWorkflowResult);
|
||||
});
|
||||
|
||||
test("getToolsInput - returns repository property value if enforced", async (t) => {
|
||||
const actions = stubGetToolsInput();
|
||||
|
||||
const target = callee(getToolsInput)
|
||||
.withActions(actions)
|
||||
.withActions(stubGetToolsInput)
|
||||
.withArgs({
|
||||
[RepositoryPropertyName.TOOLS]: `!${expectedRepositoryPropertyResult.value}`,
|
||||
});
|
||||
@@ -65,10 +59,8 @@ test("getToolsInput - returns repository property value if enforced", async (t)
|
||||
});
|
||||
|
||||
test("getToolsInput - prefers workflow input", async (t) => {
|
||||
const actions = stubGetToolsInput();
|
||||
|
||||
const target = callee(getToolsInput)
|
||||
.withActions(actions)
|
||||
.withActions(stubGetToolsInput)
|
||||
.withArgs({
|
||||
[RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value,
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface RemoteFileAddress {
|
||||
}
|
||||
|
||||
/** The default file path to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-config.yml";
|
||||
|
||||
/** The default ref to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.2",
|
||||
"cliVersion": "2.26.2",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.1",
|
||||
"priorCliVersion": "2.26.1"
|
||||
"bundleVersion": "codeql-bundle-v2.26.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
}
|
||||
+6
-7
@@ -39,12 +39,6 @@ export enum EnvVar {
|
||||
*/
|
||||
CODE_SCANNING_REF = "CODE_SCANNING_REF",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
*/
|
||||
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
|
||||
|
||||
/** Whether the CodeQL Action has invoked the Go autobuilder. */
|
||||
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
|
||||
|
||||
@@ -88,7 +82,7 @@ export enum EnvVar {
|
||||
LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION",
|
||||
|
||||
/** UUID representing the current job run. */
|
||||
JOB_RUN_UUID = "JOB_RUN_UUID",
|
||||
JOB_RUN_UUID = "CODEQL_ACTION_JOB_RUN_UUID",
|
||||
|
||||
/** Status for the entire job, submitted to the status report in `init-post` */
|
||||
JOB_STATUS = "CODEQL_ACTION_JOB_STATUS",
|
||||
@@ -270,6 +264,11 @@ export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
|
||||
return Object.create(this, { vars: { value: { ...this.vars } } }) as this;
|
||||
}
|
||||
|
||||
/** Gets a copy of the underlying environment. */
|
||||
public get(): Record<string, T> {
|
||||
return { ...this.vars };
|
||||
}
|
||||
|
||||
/** Tries to get the value for `name` and throws if there isn't one. */
|
||||
public getRequired(name: string): string {
|
||||
return getRequiredEnvVar(this.vars, name);
|
||||
|
||||
+56
-6
@@ -121,12 +121,37 @@ export enum Feature {
|
||||
* `OverlayAnalysisMatchCodeqlVersion` overrides this flag.
|
||||
*/
|
||||
OverlayAnalysisMatchCodeqlVersionDryRun = "overlay_analysis_match_codeql_version_dry_run",
|
||||
OverlayAnalysisPython = "overlay_analysis_python",
|
||||
/**
|
||||
* Controls whether lower disk space requirements are used for overlay hardware checks.
|
||||
* Has no effect if `OverlayAnalysisSkipResourceChecks` is enabled.
|
||||
* Lowers the overlay minimum available disk space to 8 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisResourceChecksV2 = "overlay_analysis_resource_checks_v2",
|
||||
OverlayAnalysisMinDisk8Gb = "overlay_analysis_min_disk_8_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 9 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk9Gb = "overlay_analysis_min_disk_9_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 10 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk10Gb = "overlay_analysis_min_disk_10_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 11 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk11Gb = "overlay_analysis_min_disk_11_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 12 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk12Gb = "overlay_analysis_min_disk_12_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 13 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk13Gb = "overlay_analysis_min_disk_13_gb",
|
||||
OverlayAnalysisPython = "overlay_analysis_python",
|
||||
OverlayAnalysisRuby = "overlay_analysis_ruby",
|
||||
/** Controls whether hardware checks are skipped for overlay analysis. */
|
||||
OverlayAnalysisSkipResourceChecks = "overlay_analysis_skip_resource_checks",
|
||||
@@ -354,9 +379,34 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MATCH_CODEQL_VERSION_DRY_RUN",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisResourceChecksV2]: {
|
||||
[Feature.OverlayAnalysisMinDisk8Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RESOURCE_CHECKS_V2",
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_8_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk9Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_9_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk10Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_10_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk11Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_11_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk12Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_12_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk13Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_13_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisStatusCheck]: {
|
||||
|
||||
@@ -123,6 +123,7 @@ async function prepareFailedSarif(
|
||||
const category = `/language:${language}`;
|
||||
const checkoutPath = ".";
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -146,6 +147,7 @@ async function prepareFailedSarif(
|
||||
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
|
||||
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -156,6 +158,7 @@ async function prepareFailedSarif(
|
||||
}
|
||||
|
||||
async function generateFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: Config,
|
||||
category: string | undefined,
|
||||
@@ -163,7 +166,7 @@ async function generateFailedSarif(
|
||||
sarifFile?: string,
|
||||
) {
|
||||
const databasePath = config.dbLocation;
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
// Set the filename for the SARIF file if not already set.
|
||||
if (sarifFile === undefined) {
|
||||
|
||||
@@ -75,7 +75,7 @@ async function run(startedAt: Date) {
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
|
||||
);
|
||||
} else {
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
uploadFailedSarifResult = await initActionPostHelper.uploadFailureInfo(
|
||||
debugArtifacts.tryUploadAllAvailableDebugArtifacts,
|
||||
|
||||
@@ -4,7 +4,6 @@ import * as path from "path";
|
||||
import * as core from "@actions/core";
|
||||
import * as io from "@actions/io";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
@@ -255,11 +254,6 @@ async function run(
|
||||
);
|
||||
const repositoryProperties = repositoryPropertiesResult.orElse({});
|
||||
|
||||
// Create a unique identifier for this run.
|
||||
const jobRunUuid = uuidV4();
|
||||
logger.info(`Job run UUID is ${jobRunUuid}.`);
|
||||
core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
// path.resolve() respects the intended semantics of source-root. If
|
||||
@@ -695,7 +689,6 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
|
||||
// To check custom query packs for compatibility with overlay analysis, we
|
||||
@@ -724,7 +717,6 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
fs.mkdirSync(config.dbLocation, { recursive: true });
|
||||
await configUtils.wrapEnvironment(
|
||||
@@ -100,7 +99,6 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot,
|
||||
processName,
|
||||
qlconfigFile,
|
||||
logger,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+27
-5
@@ -35,6 +35,11 @@ export function isNumber(value: unknown): value is number {
|
||||
return typeof value === "number";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is a boolean. */
|
||||
export function isBoolean(value: unknown): value is boolean {
|
||||
return typeof value === "boolean";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is either a string or undefined. */
|
||||
export function isStringOrUndefined(
|
||||
value: unknown,
|
||||
@@ -62,14 +67,11 @@ function defaultCheck(
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
|
||||
function makeValidator<T>(
|
||||
validate: (arg: unknown) => arg is T,
|
||||
required: boolean = true,
|
||||
) {
|
||||
function makeValidator<T>(validate: (arg: unknown) => arg is T) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required,
|
||||
required: true,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
@@ -82,6 +84,9 @@ export const string = makeValidator(isString);
|
||||
/** A validator for number fields in schemas. */
|
||||
export const number = makeValidator(isNumber);
|
||||
|
||||
/** A validator for boolean fields in schemas. */
|
||||
export const boolean = makeValidator(isBoolean);
|
||||
|
||||
/** A validator for arrays. */
|
||||
export function array<T>(validator: Validator<T>) {
|
||||
const validate = (val: unknown) => {
|
||||
@@ -221,6 +226,23 @@ export function validateSchema<
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that `arr` is an array whose elements satisfy at least `elementSchema`.
|
||||
* Additional keys are accepted in each element.
|
||||
*
|
||||
* @param elementSchema The schema to validate the elements against.
|
||||
* @param arr The array to validate.
|
||||
* @returns Asserts that `arr` has elements of `schema`'s type if validation is successful.
|
||||
*/
|
||||
export function validateArray<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedArray = Array<FromSchema<S>>,
|
||||
>(elementSchema: S, arr: UnvalidatedArray): arr is T {
|
||||
const elementValidator = object(elementSchema);
|
||||
|
||||
return array(elementValidator).validate(arr);
|
||||
}
|
||||
|
||||
export interface CheckSchemaOptions {
|
||||
/** Whether to stop validation after the first error. */
|
||||
failFast?: boolean;
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function runResolveBuildEnvironment(
|
||||
) {
|
||||
logger.startGroup(`Attempting to resolve build environment for ${language}`);
|
||||
|
||||
const codeql = await getCodeQL(cmd);
|
||||
const codeql = await getCodeQL(logger, cmd);
|
||||
|
||||
if (workingDir !== undefined) {
|
||||
logger.info(`Using ${workingDir} as the working directory.`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
@@ -95,7 +94,7 @@ async function sendCompletedStatusReport(
|
||||
|
||||
/** The main behaviour of this action. */
|
||||
async function run(
|
||||
actionState: ActionState<["Base", "Logger", "Actions"]>,
|
||||
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
): Promise<void> {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
@@ -140,10 +139,6 @@ async function run(
|
||||
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
|
||||
const jobRunUuid = uuidV4();
|
||||
logger.info(`Job run UUID is ${jobRunUuid}.`);
|
||||
core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
|
||||
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
ActionName.SetupCodeQL,
|
||||
"starting",
|
||||
|
||||
+14
-19
@@ -3,11 +3,12 @@ import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { FeatureEnablement, initFeatures } from "./feature-flags";
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import {
|
||||
credentialToStr,
|
||||
@@ -23,14 +24,14 @@ import {
|
||||
import { generateCertificateAuthority } from "./start-proxy/ca";
|
||||
import { checkProxyEnvironment } from "./start-proxy/environment";
|
||||
import { checkConnections } from "./start-proxy/reachability";
|
||||
import { ActionName, sendUnhandledErrorStatusReport } from "./status-report";
|
||||
import { ActionName } from "./status-report";
|
||||
import * as util from "./util";
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
const startedAt = action.startedAt;
|
||||
const logger = action.logger;
|
||||
let features: FeatureEnablement | undefined;
|
||||
let language: BuiltInLanguage | undefined;
|
||||
|
||||
@@ -122,21 +123,15 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
/** Defines the `start-proxy` Action. */
|
||||
const startProxyAction: Action = {
|
||||
name: ActionName.StartProxy,
|
||||
run,
|
||||
transformTelemetryError: getSafeErrorMessage,
|
||||
};
|
||||
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`start-proxy action failed: ${util.getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.StartProxy,
|
||||
startedAt,
|
||||
getSafeErrorMessage(util.wrapError(error)),
|
||||
logger,
|
||||
);
|
||||
}
|
||||
export async function runWrapper() {
|
||||
await runInActions(startProxyAction);
|
||||
}
|
||||
|
||||
async function startProxy(
|
||||
|
||||
+1
-7
@@ -83,12 +83,6 @@ export class StartProxyError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
interface StartProxyStatus extends StatusReportBase {
|
||||
// A comma-separated list of registry types which are configured for CodeQL.
|
||||
// This only includes registry types we support, not all that are configured.
|
||||
registry_types: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a status report for the `start-proxy` action indicating a successful outcome.
|
||||
*
|
||||
@@ -112,7 +106,7 @@ export async function sendSuccessStatusReport(
|
||||
logger,
|
||||
);
|
||||
if (statusReportBase !== undefined) {
|
||||
const statusReport: StartProxyStatus = {
|
||||
const statusReport: StatusReportBase = {
|
||||
...statusReportBase,
|
||||
registry_types: registry_types.join(","),
|
||||
};
|
||||
|
||||
@@ -254,13 +254,19 @@ export function credentialToStr(credential: Credential): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A package registry is identified by its type and address. */
|
||||
export type Registry = {
|
||||
/** The schema for `RegistryBase` objects. */
|
||||
export const registryBaseSchema = {
|
||||
/** The type of the package registry. */
|
||||
type: string;
|
||||
type: json.string,
|
||||
/** Whether the registry replaces the base registry for the ecosystem. */
|
||||
"replaces-base"?: boolean;
|
||||
} & Address;
|
||||
"replaces-base": json.optional(json.boolean),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Information about a registry, other than its address. */
|
||||
export type RegistryBase = json.FromSchema<typeof registryBaseSchema>;
|
||||
|
||||
/** A package registry is identified by its type and address. */
|
||||
export type Registry = RegistryBase & Address;
|
||||
|
||||
// If a registry has an `url`, then that takes precedence over the `host` which may or may
|
||||
// not be defined.
|
||||
|
||||
+104
-1
@@ -1,17 +1,21 @@
|
||||
import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, RegistryProxyVars } from "./environment";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import type { Registry } from "./start-proxy";
|
||||
import {
|
||||
ActionName,
|
||||
createInitWithConfigStatusReport,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
getRegistryTypesFromEnv,
|
||||
getJobUUID,
|
||||
InitStatusReport,
|
||||
InitWithConfigStatusReport,
|
||||
} from "./status-report";
|
||||
@@ -20,11 +24,106 @@ import {
|
||||
setupActionsVars,
|
||||
createTestConfig,
|
||||
makeMacro,
|
||||
getTestEnv,
|
||||
RecordingLogger,
|
||||
callee,
|
||||
} from "./testing-utils";
|
||||
import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getRegistryTypesFromEnv - gets unique registry types from environment", async (t) => {
|
||||
const logger = new RecordingLogger(true);
|
||||
const env = getTestEnv({
|
||||
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([
|
||||
{ type: "git_source", url: "https://example.com" },
|
||||
{ type: "git_source", url: "https://github.com" },
|
||||
{ type: "docker_registry", url: "https://registry.example.com" },
|
||||
] satisfies Array<Partial<Registry>>),
|
||||
});
|
||||
|
||||
const result = getRegistryTypesFromEnv(logger, env);
|
||||
t.deepEqual(result, ["git_source", "docker_registry"].sort().join(","));
|
||||
});
|
||||
|
||||
test("getRegistryTypesFromEnv - returns undefined if the env var is not set", async (t) => {
|
||||
const logger = new RecordingLogger(true);
|
||||
const env = getTestEnv({});
|
||||
|
||||
const result = getRegistryTypesFromEnv(logger, env);
|
||||
t.is(result, undefined);
|
||||
});
|
||||
|
||||
test("getRegistryTypesFromEnv - returns undefined if the env var is not valid JSON", async (t) => {
|
||||
const logger = new RecordingLogger(true);
|
||||
const env = getTestEnv({ [RegistryProxyVars.PROXY_URLS]: "[" });
|
||||
|
||||
const result = getRegistryTypesFromEnv(logger, env);
|
||||
t.is(result, undefined);
|
||||
});
|
||||
|
||||
test("getRegistryTypesFromEnv - returns undefined if the env var is unexpected JSON", async (t) => {
|
||||
const logger = new RecordingLogger(true);
|
||||
|
||||
t.is(
|
||||
getRegistryTypesFromEnv(
|
||||
logger,
|
||||
getTestEnv({
|
||||
// Top-level object rather than an array of objects.
|
||||
[RegistryProxyVars.PROXY_URLS]: JSON.stringify({ type: "git_source" }),
|
||||
}),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
t.is(
|
||||
getRegistryTypesFromEnv(
|
||||
logger,
|
||||
getTestEnv({
|
||||
// Object has no "type" key.
|
||||
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([{}]),
|
||||
}),
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("getJobUUID - generates valid UUIDs", async (t) => {
|
||||
await callee(getJobUUID)
|
||||
.withArgs()
|
||||
.logs(t, "Job run UUID is ")
|
||||
.hasEnv(t, (val) => {
|
||||
return {
|
||||
[EnvVar.JOB_RUN_UUID]: val,
|
||||
};
|
||||
})
|
||||
.passes((val) => {
|
||||
t.true(uuid.validate(val));
|
||||
});
|
||||
});
|
||||
|
||||
test("getJobUUID - retrieves existing job UUIDs", async (t) => {
|
||||
const existingJobUuid = uuid.v4();
|
||||
await callee(getJobUUID)
|
||||
.withArgs()
|
||||
.withEnv((env) => {
|
||||
env.set(EnvVar.JOB_RUN_UUID, existingJobUuid);
|
||||
})
|
||||
.logs(t, `Existing job run UUID is ${existingJobUuid}.`)
|
||||
.passes(t.deepEqual, existingJobUuid);
|
||||
});
|
||||
|
||||
test("getJobUUID - doesn't retrieve invalid UUIDs", async (t) => {
|
||||
const existingJobUuid = "not-a-uuid";
|
||||
await callee(getJobUUID)
|
||||
.withArgs()
|
||||
.withEnv((env) => {
|
||||
env.set(EnvVar.JOB_RUN_UUID, existingJobUuid);
|
||||
})
|
||||
.logs(t, `Job run UUID is `)
|
||||
.notLogs(t, `Existing job run UUID is ${existingJobUuid}.`)
|
||||
.passes(t.notDeepEqual, existingJobUuid);
|
||||
});
|
||||
|
||||
function setupEnvironmentAndStub(tmpDir: string) {
|
||||
setupActionsVars(tmpDir, tmpDir, {
|
||||
GITHUB_EVENT_NAME: "dynamic",
|
||||
@@ -34,6 +133,9 @@ function setupEnvironmentAndStub(tmpDir: string) {
|
||||
|
||||
process.env[EnvVar.ANALYSIS_KEY] = "analysis-key";
|
||||
process.env["ImageVersion"] = "2023.05.19.1";
|
||||
process.env[RegistryProxyVars.PROXY_URLS] = JSON.stringify([
|
||||
{ type: "maven_repository" },
|
||||
] satisfies Array<Partial<Registry>>);
|
||||
|
||||
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
|
||||
getRequiredInput.withArgs("matrix").resolves("input/matrix");
|
||||
@@ -77,6 +179,7 @@ test.serial("createStatusReportBase", async (t) => {
|
||||
t.is(typeof statusReport.job_run_uuid, "string");
|
||||
t.is(statusReport.languages, "java,swift");
|
||||
t.is(statusReport.ref, process.env["GITHUB_REF"]!);
|
||||
t.is(statusReport.registry_types, "maven_repository");
|
||||
t.is(statusReport.runner_available_disk_space_bytes, 100);
|
||||
t.is(statusReport.runner_image_version, process.env["ImageVersion"]);
|
||||
t.is(statusReport.runner_os, process.env["RUNNER_OS"]!);
|
||||
|
||||
+82
-3
@@ -1,7 +1,9 @@
|
||||
import * as os from "os";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
import type { ActionState } from "./action-common";
|
||||
import {
|
||||
getWorkflowEventName,
|
||||
getOptionalInput,
|
||||
@@ -12,21 +14,23 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import { getCachedCodeQlVersion } from "./cli/output-cache";
|
||||
import type { Config } from "./config/action-config";
|
||||
import type { ComputedInput, InputName } from "./config/inputs";
|
||||
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
||||
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
|
||||
import { getRef } from "./git-utils";
|
||||
import * as json from "./json";
|
||||
import type { Logger } from "./logging";
|
||||
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import type { ToolsSource } from "./setup-codeql";
|
||||
import { registryBaseSchema } from "./start-proxy/types";
|
||||
import {
|
||||
ConfigurationError,
|
||||
getRequiredEnvParam,
|
||||
getCachedCodeQlVersion,
|
||||
isInTestMode,
|
||||
GITHUB_DOTCOM_URL,
|
||||
DiskUsage,
|
||||
@@ -59,6 +63,30 @@ export function getDisplayActionName(actionName: ActionName): string {
|
||||
return actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Either creates a UUIDv4 for the analysis or retrieves an existing one from the
|
||||
* environment and returns it.
|
||||
* If a new UUID is generated, it is also exported as an environment variable.
|
||||
*/
|
||||
export function getJobUUID(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv", "Actions"]>,
|
||||
) {
|
||||
// Check if we already have a UUID for the analysis and return it if so.
|
||||
const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID);
|
||||
|
||||
if (existingJobRunUuid !== undefined && uuid.validate(existingJobRunUuid)) {
|
||||
action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`);
|
||||
return existingJobRunUuid;
|
||||
}
|
||||
|
||||
// Otherwise generate a new UUID.
|
||||
const jobRunUuid = uuid.v4();
|
||||
action.logger.info(`Job run UUID is ${jobRunUuid}.`);
|
||||
|
||||
action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
|
||||
return jobRunUuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a boolean indicating whether the analysis is considered to be first party.
|
||||
*
|
||||
@@ -159,6 +187,12 @@ export interface StatusReportBase {
|
||||
ml_powered_javascript_queries?: string;
|
||||
/** Ref that the workflow was triggered on. */
|
||||
ref: string;
|
||||
/**
|
||||
* A comma-separated list of private registry types which are configured for CodeQL.
|
||||
* This only includes registry types we support (as determined by the `start-proxy` action),
|
||||
* not all that are configured.
|
||||
*/
|
||||
registry_types?: string;
|
||||
/** Action runner hardware architecture (context runner.arch). */
|
||||
runner_arch?: string;
|
||||
/** Available disk space on the runner, in bytes. */
|
||||
@@ -262,6 +296,50 @@ export interface EventReport {
|
||||
started_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve a list of private registry types from the `CODEQL_PROXY_URLS` environment
|
||||
* variable and returns it as a comma-separated string if successful. Returns `undefined` otherwise.
|
||||
*/
|
||||
export function getRegistryTypesFromEnv(
|
||||
logger: Logger,
|
||||
env: ReadOnlyEnv = getEnv(),
|
||||
): string | undefined {
|
||||
// Try to get the value of the environment variable.
|
||||
const value = env.getOptional(RegistryProxyVars.PROXY_URLS);
|
||||
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try to parse the JSON we expect to find in it and return the comma-separated list of
|
||||
// (unique) registry types.
|
||||
try {
|
||||
const data = JSON.parse(value) as unknown;
|
||||
|
||||
// Check that the parsed JSON meets our expectations.
|
||||
if (!json.isArray(data)) {
|
||||
logger.debug(
|
||||
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array, but got '${typeof data}'.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
if (!json.validateArray(registryBaseSchema, data)) {
|
||||
logger.debug(
|
||||
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array of registry objects, but got something else.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const types = new Set(data.map((r) => r.type));
|
||||
return Array.from(types).sort().join(",");
|
||||
} catch (err) {
|
||||
logger.debug(
|
||||
`Failed to parse '${RegistryProxyVars.PROXY_URLS}': ${getErrorMessage(err)}.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a StatusReport.
|
||||
*
|
||||
@@ -298,7 +376,7 @@ export async function createStatusReportBase(
|
||||
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
|
||||
}
|
||||
const runnerOs = getRequiredEnvParam("RUNNER_OS");
|
||||
const codeQlCliVersion = getCachedCodeQlVersion();
|
||||
const codeQlCliVersion = getCachedCodeQlVersion(logger, getEnv());
|
||||
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
|
||||
const testingEnvironment = getTestingEnvironment();
|
||||
// re-export the testing environment variable so that it is available to subsequent steps,
|
||||
@@ -324,6 +402,7 @@ export async function createStatusReportBase(
|
||||
job_name: jobName,
|
||||
job_run_uuid: jobRunUUID,
|
||||
ref,
|
||||
registry_types: getRegistryTypesFromEnv(logger),
|
||||
runner_os: runnerOs,
|
||||
started_at: workflowStartedAt,
|
||||
status,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as path from "path";
|
||||
import * as stream from "stream";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { extractTarZst } from "./tar";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { withTmpDir } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("extractTarZst rejects if the input stream errors", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const archive = new stream.PassThrough();
|
||||
const promise = extractTarZst(
|
||||
archive,
|
||||
path.join(tmpDir, "dest"),
|
||||
{ type: "gnu", version: "1.34" },
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
archive.destroy(
|
||||
Object.assign(new Error("socket hang up"), {
|
||||
code: "ECONNRESET",
|
||||
}),
|
||||
);
|
||||
|
||||
await t.throwsAsync(promise, {
|
||||
message: /Error while downloading and extracting tar/,
|
||||
});
|
||||
});
|
||||
});
|
||||
+9
-4
@@ -194,10 +194,15 @@ export async function extractTarZst(
|
||||
});
|
||||
|
||||
if (tar instanceof stream.Readable) {
|
||||
tar.pipe(tarProcess.stdin).on("error", (err) => {
|
||||
reject(
|
||||
new Error(`Error while downloading and extracting tar: ${err}`),
|
||||
);
|
||||
// Use `pipeline` rather than `pipe` so that an error on either stream is reported here
|
||||
// rather than being emitted as an unhandled `error` event, and so that `tar`'s standard
|
||||
// input is closed if the download fails partway through.
|
||||
stream.pipeline(tar, tarProcess.stdin, (err) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new Error(`Error while downloading and extracting tar: ${err}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+86
-27
@@ -18,6 +18,8 @@ import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
import { GitHubApiDetails } from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { resetCachedCodeQlVersion } from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import * as codeql from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
@@ -34,11 +36,13 @@ import { ActionName } from "./status-report";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
Failure,
|
||||
getEnv,
|
||||
GitHubVariant,
|
||||
GitHubVersion,
|
||||
HTTPError,
|
||||
resetCachedCodeQlVersion,
|
||||
Result,
|
||||
Success,
|
||||
} from "./util";
|
||||
|
||||
export const SAMPLE_DOTCOM_API_DETAILS = {
|
||||
@@ -182,13 +186,32 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
|
||||
return getEnv(testEnv);
|
||||
}
|
||||
|
||||
/** An implementation of `ActionsEnv` for use in tests. */
|
||||
class TestActionsEnv implements ActionsEnv {
|
||||
constructor(private readonly env: Env) {}
|
||||
|
||||
public clone(env: Env): this {
|
||||
return Object.create(this, { env: { value: env } }) as this;
|
||||
}
|
||||
|
||||
public getRequiredInput(name: string): string {
|
||||
throw new Error(`Input required and not supplied: ${name}`);
|
||||
}
|
||||
|
||||
public getOptionalInput(_name: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public exportVariable(name: string, value: string): void {
|
||||
this.env.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an `ActionsEnv` instance for use in tests.
|
||||
*/
|
||||
export function getTestActionsEnv(): ActionsEnv {
|
||||
return {
|
||||
getOptionalInput: () => undefined,
|
||||
};
|
||||
export function getTestActionsEnv(env: Env): TestActionsEnv {
|
||||
return new TestActionsEnv(env);
|
||||
}
|
||||
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
@@ -206,12 +229,13 @@ type AllState = [
|
||||
export function initAllState(
|
||||
overrides?: Partial<ActionState<AllState>>,
|
||||
): ActionState<AllState> {
|
||||
const env = getTestEnv();
|
||||
return {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
logger: new RecordingLogger(),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
env,
|
||||
actions: getTestActionsEnv(env),
|
||||
apiClient: github.getOctokit("123"),
|
||||
features: createFeatures([]),
|
||||
...overrides,
|
||||
@@ -222,9 +246,13 @@ type DelayedCheck<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
|
||||
> = (
|
||||
env: Readonly<BaseEnvBuilder<Args, R, Fs>>,
|
||||
result: Result<Awaited<R>, ThrownError<ErrorConstructor | Error>>,
|
||||
) => Promise<any>;
|
||||
|
||||
export type ValueOrMutation<T> = T | ((val: T) => void);
|
||||
export type Mutation<T> = (val: T) => void;
|
||||
export type ValueOrMutation<T> = T | Mutation<T>;
|
||||
|
||||
/**
|
||||
* Wraps a function that accepts an `ActionState` for testing in different environments.
|
||||
@@ -236,6 +264,7 @@ abstract class BaseEnvBuilder<
|
||||
> {
|
||||
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
|
||||
private logger: RecordingLogger;
|
||||
private actions: TestActionsEnv;
|
||||
protected state: ActionState<AllState>;
|
||||
protected checks: Array<DelayedCheck<Args, R, Fs>>;
|
||||
|
||||
@@ -245,15 +274,26 @@ abstract class BaseEnvBuilder<
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.logger = new RecordingLogger();
|
||||
this.state =
|
||||
cloneFrom !== undefined
|
||||
? ({
|
||||
...cloneFrom.state,
|
||||
env: cloneFrom.state.env.clone(),
|
||||
actions: Object.create(cloneFrom.state.actions),
|
||||
logger: this.logger,
|
||||
} satisfies ActionState<AllState>)
|
||||
: initAllState({ logger: this.logger });
|
||||
|
||||
if (cloneFrom !== undefined) {
|
||||
const env = cloneFrom.state.env.clone();
|
||||
this.actions = cloneFrom.actions.clone(env);
|
||||
this.state = {
|
||||
...cloneFrom.state,
|
||||
env,
|
||||
actions: this.actions,
|
||||
logger: this.logger,
|
||||
} satisfies ActionState<AllState>;
|
||||
} else {
|
||||
const env = getTestEnv();
|
||||
this.actions = getTestActionsEnv(env);
|
||||
this.state = initAllState({
|
||||
logger: this.logger,
|
||||
env,
|
||||
actions: this.actions,
|
||||
});
|
||||
}
|
||||
|
||||
this.checks = [...(cloneFrom?.checks ?? [])];
|
||||
}
|
||||
|
||||
@@ -320,13 +360,10 @@ abstract class BaseEnvBuilder<
|
||||
return result;
|
||||
}
|
||||
|
||||
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
|
||||
/** Applies `fn` to the `ActionsEnv`. */
|
||||
public withActions(fn: Mutation<ActionsEnv>): this {
|
||||
const result = this.clone();
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.actions);
|
||||
} else {
|
||||
result.state.actions = arg;
|
||||
}
|
||||
fn(result.state.actions);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -342,6 +379,28 @@ abstract class BaseEnvBuilder<
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that the environment variables returned by `fn`
|
||||
* are present in the environment after the main assertion passes.
|
||||
*/
|
||||
public hasEnv(
|
||||
t: ExecutionContext<unknown>,
|
||||
fn: (
|
||||
value: Awaited<R> | undefined,
|
||||
error: ThrownError<ErrorConstructor | Error> | undefined,
|
||||
) => Record<string, string | undefined>,
|
||||
): this {
|
||||
const result = this.clone();
|
||||
result.checks.push(async (env, r) => {
|
||||
const value = r.orElse(undefined);
|
||||
const error = r.isFailure() ? r.value : undefined;
|
||||
const expected = fn(value, error);
|
||||
|
||||
t.like(env.getState().env.get(), expected);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that `messages` are not logged. The check will be
|
||||
* performed after the main assertion passes.
|
||||
@@ -439,7 +498,7 @@ class CallableEnvBuilder<
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
await delayedCheck(this, new Success(result));
|
||||
}
|
||||
|
||||
// Return the results of the function call and the main assertion.
|
||||
@@ -465,7 +524,7 @@ class CallableEnvBuilder<
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
await delayedCheck(this, new Failure(error));
|
||||
}
|
||||
|
||||
// Return the error.
|
||||
@@ -814,7 +873,7 @@ export const makeVersionInfo = (
|
||||
version: string,
|
||||
features?: { [name: string]: boolean },
|
||||
overlayVersion?: number,
|
||||
): codeql.VersionInfo => ({
|
||||
): VersionInfo => ({
|
||||
version,
|
||||
features,
|
||||
overlayVersion,
|
||||
|
||||
@@ -38,6 +38,43 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract falls back to downloading before extracting if streaming fails",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
const downloadTool = sinon
|
||||
.stub(toolcache, "downloadTool")
|
||||
.resolves(archivePath);
|
||||
const extract = sinon.stub(tar, "extract").resolves(destination);
|
||||
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
|
||||
const request = nock("https://example.com")
|
||||
.get("/codeql-bundle.tar.zst")
|
||||
.replyWithError(
|
||||
Object.assign(new Error("socket hang up"), { code: "ECONNRESET" }),
|
||||
);
|
||||
|
||||
const statusReport = await downloadAndExtract(
|
||||
"https://example.com/codeql-bundle.tar.zst",
|
||||
"zstd",
|
||||
destination,
|
||||
undefined,
|
||||
{},
|
||||
{ type: "gnu", version: "1.34" },
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
t.assert(Number.isInteger(statusReport.downloadDurationMs));
|
||||
t.true(request.isDone());
|
||||
t.false(extractTarZst.called);
|
||||
t.true(downloadTool.calledOnce);
|
||||
t.true(extract.calledOnce);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract omits the download duration when streaming extraction",
|
||||
async (t) => {
|
||||
|
||||
+24
-4
@@ -19,6 +19,12 @@ import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
|
||||
*/
|
||||
const STREAMING_HIGH_WATERMARK_BYTES = 4 * 1024 * 1024; // 4 MiB
|
||||
|
||||
/**
|
||||
* How long the streaming download of the CodeQL tools may stall for before we abort it. This
|
||||
* applies both to establishing the connection and to gaps between chunks of the response body.
|
||||
*/
|
||||
const STREAMING_STALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* The name of the tool cache directory for the CodeQL tools.
|
||||
*/
|
||||
@@ -137,8 +143,8 @@ async function downloadAndExtractZstdWithStreaming(
|
||||
authorization ? { authorization } : {},
|
||||
headers,
|
||||
);
|
||||
const response = await new Promise<IncomingMessage>((resolve) =>
|
||||
https.get(
|
||||
const response = await new Promise<IncomingMessage>((resolve, reject) => {
|
||||
const request = https.get(
|
||||
codeqlURL,
|
||||
{
|
||||
headers,
|
||||
@@ -148,10 +154,24 @@ async function downloadAndExtractZstdWithStreaming(
|
||||
agent,
|
||||
} as unknown as RequestOptions,
|
||||
(r) => resolve(r),
|
||||
),
|
||||
);
|
||||
);
|
||||
// Without this listener, connection failures such as `ECONNRESET` are emitted as unhandled
|
||||
// `error` events, which terminate the process instead of letting us fall back to downloading
|
||||
// the bundle before extracting it. This listener stays attached after the response arrives, so
|
||||
// it also handles errors that occur while the response is being streamed.
|
||||
request.on("error", reject);
|
||||
request.setTimeout(STREAMING_STALL_TIMEOUT_MS, () => {
|
||||
request.destroy(
|
||||
new Error(
|
||||
`No data received for ${formatDuration(STREAMING_STALL_TIMEOUT_MS)}.`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
// Discard the response body so that the connection can be released.
|
||||
response.resume();
|
||||
throw new Error(
|
||||
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import type { VersionInfo } from "./codeql";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
|
||||
export enum ToolsFeature {
|
||||
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ async function combineSarifFilesUsingCLI(
|
||||
|
||||
const config = await getConfig(tempDir, logger);
|
||||
if (config !== undefined) {
|
||||
codeQL = await getCodeQL(config.codeQLCmd);
|
||||
codeQL = await getCodeQL(logger, config.codeQLCmd);
|
||||
tempDir = config.tempDir;
|
||||
} else {
|
||||
logger.info(
|
||||
|
||||
@@ -532,58 +532,3 @@ test("Failure.orElse returns the default value for a failure result", (t) => {
|
||||
const result = new util.Failure(new Error("test error"));
|
||||
t.is(result.orElse("default value"), "default value");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql"), {
|
||||
version: "2.20.0",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = "not valid json";
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
(t) => {
|
||||
for (const value of [
|
||||
JSON.stringify({ cmd: "/path/to/codeql" }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: {} }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: { version: 2 } }),
|
||||
JSON.stringify({ version: { version: "2.20.0" } }),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", overlayVersion: "1" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", features: "nope" },
|
||||
}),
|
||||
]) {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = value;
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined, value);
|
||||
}
|
||||
},
|
||||
);
|
||||
+1
-85
@@ -10,7 +10,7 @@ import * as yaml from "js-yaml";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { CodeQL } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar, getRequiredEnvParam } from "./environment";
|
||||
@@ -598,90 +598,6 @@ export function asHTTPError(arg: any): HTTPError | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/** The persisted version together with the CLI path it was obtained from. */
|
||||
interface PersistedVersionInfo {
|
||||
cmd: string;
|
||||
version: VersionInfo;
|
||||
}
|
||||
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const candidate = x as Partial<VersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.version === "string" &&
|
||||
(candidate.features === undefined ||
|
||||
(typeof candidate.features === "object" &&
|
||||
candidate.features !== null)) &&
|
||||
(candidate.overlayVersion === undefined ||
|
||||
typeof candidate.overlayVersion === "number")
|
||||
);
|
||||
}
|
||||
|
||||
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
|
||||
const candidate = x as Partial<PersistedVersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
isVersionInfo(candidate.version)
|
||||
);
|
||||
}
|
||||
|
||||
export function cacheCodeQlVersion(cmd: string, version: VersionInfo): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
core.exportVariable(
|
||||
EnvVar.CODEQL_VERSION_INFO,
|
||||
JSON.stringify({ cmd, version }),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
const serialized = process.env[EnvVar.CODEQL_VERSION_INFO];
|
||||
if (!serialized) {
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isPersistedVersionInfo(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.version;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
export async function codeQlVersionAtLeast(
|
||||
codeql: CodeQL,
|
||||
requiredVersion: string,
|
||||
|
||||
Reference in new issue
Block a user