mirror of
https://github.com/github/codeql-action.git
synced 2026-06-02 03:44:42 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7211b7c807 | |||
| 7740f2fb21 | |||
| ebc2d9e2bc | |||
| d1f74b777c | |||
| 2dc40cec39 | |||
| 84498526a0 | |||
| 72ac23c6d1 | |||
| f3f52bf568 | |||
| a14f75e3ac | |||
| 2c8faa5e9f | |||
| 15a712bbc2 | |||
| 9b6438e936 | |||
| b5b50d62f1 | |||
| 5a80681bb6 | |||
| bcffb2b658 | |||
| 6f8805e224 | |||
| 4fc0f3e51b |
+112
-32
@@ -33,6 +33,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: pr-checks-unit-tests-${{ github.ref }}-${{ github.event_name }}-${{ matrix.os }}-node${{ matrix['node-version'] }}
|
||||
|
||||
steps:
|
||||
- name: Prepare git (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
@@ -71,22 +75,21 @@ jobs:
|
||||
sarif_file: eslint.sarif
|
||||
category: eslint
|
||||
|
||||
# Verifying the PR checks are up-to-date requires Node 24. The PR checks are not dependent
|
||||
# on the main codebase and therefore do not need to be run as part of the same matrix that
|
||||
# we use for the `unit-tests` job.
|
||||
verify-pr-checks:
|
||||
name: Verify PR checks
|
||||
# These checks do not need to be run as part of the same matrix that we use for the `unit-tests`
|
||||
# job.
|
||||
other-checks:
|
||||
name: Other checks
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-slim
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Prepare git (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --global core.autocrlf false
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: pr-checks-pr-checks-${{ github.ref }}-${{ github.event_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
@@ -97,34 +100,22 @@ jobs:
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
id: install-deps
|
||||
run: npm ci
|
||||
|
||||
- name: Verify PR checks up to date
|
||||
if: always()
|
||||
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
|
||||
run: .github/workflows/script/verify-pr-checks.sh
|
||||
|
||||
- name: Run pr-checks tests
|
||||
if: always()
|
||||
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
|
||||
working-directory: pr-checks
|
||||
run: npx tsx --test
|
||||
|
||||
check-node-version:
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
name: Check Action Node versions
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: head-version
|
||||
name: Verify all Actions use the same Node version
|
||||
- name: Verify all Actions use the same Node version
|
||||
id: head-version
|
||||
run: |
|
||||
NODE_VERSION=$(find . -name "action.yml" -exec yq -e '.runs.using' {} \; | grep node | sort | uniq)
|
||||
NODE_VERSION=$(find . -path "*/node_modules" -prune -o -name "action.yml" -exec yq -o=json '.runs.using' {} \; | jq -rs '[.[] | select(. != null and startswith("node"))] | unique | .[]')
|
||||
echo "NODE_VERSION: ${NODE_VERSION}"
|
||||
if [[ $(echo "$NODE_VERSION" | wc -l) -gt 1 ]]; then
|
||||
echo "::error::More than one node version used in 'action.yml' files."
|
||||
@@ -132,22 +123,111 @@ jobs:
|
||||
fi
|
||||
echo "node_version=${NODE_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout-base
|
||||
name: 'Backport: Check out base ref'
|
||||
- name: Fetch base commit
|
||||
id: fetch-base
|
||||
# Forks and Dependabot PRs don't have permission to write comments, so skip the repo size
|
||||
# check in those cases.
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Compare against the merge base so the size delta reflects only the commits actually
|
||||
# added by this PR, ignoring any changes that have landed on the base branch since the
|
||||
# PR branched off.
|
||||
merge_base=$(gh api "repos/$GITHUB_REPOSITORY/compare/$BASE_SHA...$HEAD_SHA" --jq '.merge_base_commit.sha')
|
||||
echo "merge_base=$merge_base" >> "$GITHUB_OUTPUT"
|
||||
git fetch --no-tags --depth=1 origin "$merge_base" "$HEAD_SHA"
|
||||
|
||||
- name: Check repo size
|
||||
if: steps.fetch-base.outcome == 'success'
|
||||
working-directory: pr-checks
|
||||
env:
|
||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
BASE_SHA: ${{ steps.fetch-base.outputs.merge_base }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: npx tsx check-repo-size.ts --output-dir "$RUNNER_TEMP/repo-size"
|
||||
|
||||
- name: Upload repo size comment
|
||||
if: steps.fetch-base.outcome == 'success'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: repo-size-comment
|
||||
path: ${{ runner.temp }}/repo-size/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 'Backport: Check out base ref'
|
||||
id: checkout-base
|
||||
if: ${{ startsWith(github.head_ref, 'backport-') }}
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ env.BASE_REF }}
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: 'Backport: Verify Node versions unchanged'
|
||||
if: steps.checkout-base.outcome == 'success'
|
||||
env:
|
||||
HEAD_VERSION: ${{ steps.head-version.outputs.node_version }}
|
||||
run: |
|
||||
BASE_VERSION=$(find . -name "action.yml" -exec yq -e '.runs.using' {} \; | grep node | sort | uniq)
|
||||
BASE_VERSION=$(find . -path "*/node_modules" -prune -o -name "action.yml" -exec yq -o=json '.runs.using' {} \; | jq -rs '[.[] | select(. != null and startswith("node"))] | unique | .[]')
|
||||
echo "HEAD_VERSION: ${HEAD_VERSION}"
|
||||
echo "BASE_VERSION: ${BASE_VERSION}"
|
||||
if [[ "$BASE_VERSION" != "$HEAD_VERSION" ]]; then
|
||||
echo "::error::Cannot change the Node version of an Action in a backport PR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
post-repo-size-comment:
|
||||
name: Post repo size comment
|
||||
needs: other-checks
|
||||
# Keep write permissions isolated from the job that checks out and tests PR code. This job only
|
||||
# posts the candidate comment body produced by the read-only `pr-checks` job.
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]' &&
|
||||
needs.other-checks.result == 'success'
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: check-repo-size-${{ github.event.pull_request.number }}
|
||||
|
||||
steps:
|
||||
- name: Download repo size comment
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: repo-size-comment
|
||||
path: repo-size-comment
|
||||
|
||||
- name: Post repo size comment
|
||||
env:
|
||||
COMMENT_MARKER: "<!-- repo-size-diff-bot -->"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
significant=$(jq -r '.significant' repo-size-comment/metadata.json)
|
||||
comment_id=$(
|
||||
gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
|
||||
--paginate \
|
||||
--jq ".[] | select(.body | contains(\"$COMMENT_MARKER\")) | .id" \
|
||||
| head -n 1
|
||||
)
|
||||
|
||||
if [[ -n "$comment_id" ]]; then
|
||||
echo "Updating existing comment $comment_id."
|
||||
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" --field body=@repo-size-comment/body.md
|
||||
elif [[ "$significant" == "true" ]]; then
|
||||
echo "Creating new repo size comment."
|
||||
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --field body=@repo-size-comment/body.md
|
||||
else
|
||||
echo "Skipping repo size comment because the delta is below the threshold and no sticky comment exists."
|
||||
fi
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## [UNRELEASED]
|
||||
## 4.36.0 - 22 May 2026
|
||||
|
||||
- _Breaking change_: Bump the minimum required CodeQL bundle version to 2.19.4. [#3894](https://github.com/github/codeql-action/pull/3894)
|
||||
- Add support for SHA-256 Git object IDs. [#3893](https://github.com/github/codeql-action/pull/3893)
|
||||
- Update default CodeQL bundle version to [2.25.5](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.5). [#3926](https://github.com/github/codeql-action/pull/3926)
|
||||
|
||||
## 4.35.5 - 15 May 2026
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.4",
|
||||
"cliVersion": "2.25.4",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.3",
|
||||
"priorCliVersion": "2.25.3"
|
||||
"bundleVersion": "codeql-bundle-v2.25.5",
|
||||
"cliVersion": "2.25.5",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.4",
|
||||
"priorCliVersion": "2.25.4"
|
||||
}
|
||||
|
||||
Generated
+17
-60
@@ -148871,8 +148871,8 @@ function wrapApiConfigurationError(e) {
|
||||
}
|
||||
|
||||
// src/defaults.json
|
||||
var bundleVersion = "codeql-bundle-v2.25.4";
|
||||
var cliVersion = "2.25.4";
|
||||
var bundleVersion = "codeql-bundle-v2.25.5";
|
||||
var cliVersion = "2.25.5";
|
||||
|
||||
// src/overlay/index.ts
|
||||
var fs4 = __toESM(require("fs"));
|
||||
@@ -151640,7 +151640,6 @@ async function initActionState({
|
||||
computedConfig,
|
||||
tempDir,
|
||||
codeQLCmd: codeql.getPath(),
|
||||
codeQLMetadata: codeql.getCliMetadata(),
|
||||
gitHubVersion: githubVersion,
|
||||
dbLocation: dbLocationOrDefault(dbLocation, tempDir),
|
||||
debugMode,
|
||||
@@ -153794,29 +153793,19 @@ Details: ${e.stack}` : ""}`
|
||||
);
|
||||
}
|
||||
}
|
||||
async function getCodeQL(cmd, cliMetadata) {
|
||||
async function getCodeQL(cmd) {
|
||||
if (cachedCodeQL === void 0) {
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true, cliMetadata);
|
||||
} else {
|
||||
cachedCodeQL.hydrateCliMetadata(cliMetadata);
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true);
|
||||
}
|
||||
return cachedCodeQL;
|
||||
}
|
||||
function cacheCodeQlVersionForStatusReports(versionInfo) {
|
||||
if (getCachedCodeQlVersion() === void 0) {
|
||||
cacheCodeQlVersion(versionInfo);
|
||||
}
|
||||
}
|
||||
async function getCodeQLForCmd(cmd, checkVersion, initialCliMetadata) {
|
||||
const cliMetadata = { codeQLCmd: cmd };
|
||||
let cachedVersion;
|
||||
let cachedUnfilteredBetterResolveLanguages;
|
||||
async function getCodeQLForCmd(cmd, checkVersion) {
|
||||
const codeql = {
|
||||
getPath() {
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = cachedVersion;
|
||||
let result = getCachedCodeQlVersion();
|
||||
if (result === void 0) {
|
||||
const output = await runCli(cmd, ["version", "--format=json"], {
|
||||
noStreamStdout: true
|
||||
@@ -153828,17 +153817,12 @@ async function getCodeQLForCmd(cmd, checkVersion, initialCliMetadata) {
|
||||
`Invalid JSON output from \`version --format=json\`: ${output}`
|
||||
);
|
||||
}
|
||||
cachedVersion = result;
|
||||
cacheCodeQlVersion(result);
|
||||
}
|
||||
cacheCodeQlVersionForStatusReports(result);
|
||||
return result;
|
||||
},
|
||||
async printVersion() {
|
||||
const version = await this.getVersion();
|
||||
process.stdout.write(`[command]${cmd} version --format=json
|
||||
`);
|
||||
process.stdout.write(`${JSON.stringify(version)}
|
||||
`);
|
||||
await runCli(cmd, ["version", "--format=json"]);
|
||||
},
|
||||
async supportsFeature(feature) {
|
||||
return isSupportedToolsFeature(await this.getVersion(), feature);
|
||||
@@ -154008,9 +153992,6 @@ async function getCodeQLForCmd(cmd, checkVersion, initialCliMetadata) {
|
||||
async betterResolveLanguages({
|
||||
filterToLanguagesWithQueries
|
||||
} = { filterToLanguagesWithQueries: false }) {
|
||||
if (!filterToLanguagesWithQueries && cachedUnfilteredBetterResolveLanguages) {
|
||||
return cachedUnfilteredBetterResolveLanguages;
|
||||
}
|
||||
const codeqlArgs = [
|
||||
"resolve",
|
||||
"languages",
|
||||
@@ -154022,11 +154003,7 @@ async function getCodeQLForCmd(cmd, checkVersion, initialCliMetadata) {
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
try {
|
||||
const result = JSON.parse(output);
|
||||
if (!filterToLanguagesWithQueries) {
|
||||
cachedUnfilteredBetterResolveLanguages = result;
|
||||
}
|
||||
return result;
|
||||
return JSON.parse(output);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`
|
||||
@@ -154185,10 +154162,6 @@ ${output}`
|
||||
await new toolrunner3.ToolRunner(cmd, args).exec();
|
||||
},
|
||||
async resolveExtractor(language) {
|
||||
const cachedExtractorPath = cliMetadata.extractorPaths?.[language];
|
||||
if (cachedExtractorPath !== void 0) {
|
||||
return cachedExtractorPath;
|
||||
}
|
||||
let extractorPath = "";
|
||||
await new toolrunner3.ToolRunner(
|
||||
cmd,
|
||||
@@ -154212,10 +154185,7 @@ ${output}`
|
||||
}
|
||||
}
|
||||
).exec();
|
||||
const resolvedExtractorPath = JSON.parse(extractorPath);
|
||||
cliMetadata.extractorPaths ??= {};
|
||||
cliMetadata.extractorPaths[language] = resolvedExtractorPath;
|
||||
return resolvedExtractorPath;
|
||||
return JSON.parse(extractorPath);
|
||||
},
|
||||
async resolveQueriesStartingPacks(queries) {
|
||||
const codeqlArgs = [
|
||||
@@ -154268,21 +154238,8 @@ ${output}`
|
||||
args.push("--sarif-merge-runs-from-equal-category");
|
||||
}
|
||||
await runCli(cmd, args);
|
||||
},
|
||||
getCliMetadata() {
|
||||
return cliMetadata;
|
||||
},
|
||||
hydrateCliMetadata(metadata) {
|
||||
if (metadata?.codeQLCmd !== cliMetadata.codeQLCmd) {
|
||||
return;
|
||||
}
|
||||
cliMetadata.extractorPaths = {
|
||||
...metadata.extractorPaths,
|
||||
...cliMetadata.extractorPaths
|
||||
};
|
||||
}
|
||||
};
|
||||
codeql.hydrateCliMetadata(initialCliMetadata);
|
||||
if (checkVersion && !await codeQlVersionAtLeast(codeql, CODEQL_MINIMUM_VERSION)) {
|
||||
throw new ConfigurationError(
|
||||
`Expected a CodeQL CLI with version at least ${CODEQL_MINIMUM_VERSION} but got version ${(await codeql.getVersion()).version}`
|
||||
@@ -154467,7 +154424,7 @@ async function setupCppAutobuild(codeql, logger) {
|
||||
}
|
||||
async function runAutobuild(config, language, logger) {
|
||||
logger.startGroup(`Attempting to automatically build ${language} code`);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd);
|
||||
if (language === "cpp" /* cpp */) {
|
||||
await setupCppAutobuild(codeQL, logger);
|
||||
}
|
||||
@@ -157051,7 +157008,7 @@ async function combineSarifFilesUsingCLI(sarifFiles, gitHubVersion, features, lo
|
||||
let tempDir = getTemporaryDirectory();
|
||||
const config = await getConfig(tempDir, logger);
|
||||
if (config !== void 0) {
|
||||
codeQL = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
codeQL = await getCodeQL(config.codeQLCmd);
|
||||
tempDir = config.tempDir;
|
||||
} else {
|
||||
logger.info(
|
||||
@@ -157785,7 +157742,7 @@ async function run(startedAt) {
|
||||
"Config file could not be found at expected location. Has the 'init' action been called?"
|
||||
);
|
||||
}
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
if (hasBadExpectErrorInput()) {
|
||||
throw new ConfigurationError(
|
||||
"`expect-error` input parameter is for internal use only. It should only be set by codeql-action or a fork."
|
||||
@@ -158554,7 +158511,7 @@ async function runWrapper2() {
|
||||
logger
|
||||
);
|
||||
if (config !== void 0) {
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const version = await codeql.getVersion();
|
||||
await uploadCombinedSarifArtifacts(
|
||||
logger,
|
||||
@@ -158635,7 +158592,7 @@ async function run2(startedAt) {
|
||||
"Config file could not be found at expected location. Has the 'init' action been called?"
|
||||
);
|
||||
}
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
languages = await determineAutobuildLanguages(codeql, config, logger);
|
||||
if (languages !== void 0) {
|
||||
const workingDirectory = getOptionalInput("working-directory");
|
||||
@@ -159567,7 +159524,7 @@ async function prepareFailedSarif(logger, features, config) {
|
||||
}
|
||||
async function generateFailedSarif(features, config, category, checkoutPath, sarifFile) {
|
||||
const databasePath = config.dbLocation;
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
if (sarifFile === void 0) {
|
||||
sarifFile = "../codeql-failed-run.sarif";
|
||||
}
|
||||
@@ -159833,7 +159790,7 @@ async function run4(startedAt) {
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any."
|
||||
);
|
||||
} else {
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
uploadFailedSarifResult = await uploadFailureInfo(
|
||||
tryUploadAllAvailableDebugArtifacts,
|
||||
printDebugLogs,
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/*
|
||||
Tests for check-repo-size.ts.
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
COMMENT_MARKER,
|
||||
DEFAULT_BASE_REF,
|
||||
buildCommentBody,
|
||||
formatBytes,
|
||||
formatPercent,
|
||||
isDeltaSignificant,
|
||||
measureArchiveSize,
|
||||
readArgs,
|
||||
} from "./check-repo-size";
|
||||
|
||||
describe("formatBytes", async () => {
|
||||
const cases: Array<[number, boolean, string]> = [
|
||||
// Unsigned values, including sub-KiB amounts which round to 0.00.
|
||||
[0, false, "0.00 KiB"],
|
||||
[512, false, "0.50 KiB"],
|
||||
[1024, false, "1.00 KiB"],
|
||||
[1024 * 1024, false, "1024.00 KiB"],
|
||||
[2 * 1024 * 1024, false, "2048.00 KiB"],
|
||||
// Negative values always use a leading minus.
|
||||
[-2 * 1024 * 1024, false, "-2048.00 KiB"],
|
||||
// signed=true prepends a + to non-negative values.
|
||||
[0, true, "+0.00 KiB"],
|
||||
[2 * 1024 * 1024, true, "+2048.00 KiB"],
|
||||
[-2 * 1024 * 1024, true, "-2048.00 KiB"],
|
||||
];
|
||||
for (const [bytes, signed, expected] of cases) {
|
||||
await it(`formats ${bytes} (signed=${signed}) as ${expected}`, () => {
|
||||
assert.equal(formatBytes(bytes, signed), expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("formatPercent", async () => {
|
||||
await it("formats positive fractions with a leading +", () => {
|
||||
assert.equal(formatPercent(0.1), "+10.00%");
|
||||
assert.equal(formatPercent(0.0123), "+1.23%");
|
||||
});
|
||||
|
||||
await it("formats negative fractions with a leading -", () => {
|
||||
assert.equal(formatPercent(-0.1), "-10.00%");
|
||||
});
|
||||
|
||||
await it("formats zero without a sign", () => {
|
||||
assert.equal(formatPercent(0), "0.00%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDeltaSignificant", async () => {
|
||||
const cases: Array<[number, number, number, boolean]> = [
|
||||
// At and above threshold (both signs).
|
||||
[100, 1000, 0.1, true],
|
||||
[101, 1000, 0.1, true],
|
||||
[-100, 1000, 0.1, true],
|
||||
// Below threshold (both signs, plus exact zero).
|
||||
[99, 1000, 0.1, false],
|
||||
[-99, 1000, 0.1, false],
|
||||
[0, 1000, 0.1, false],
|
||||
];
|
||||
for (const [delta, base, fraction, expected] of cases) {
|
||||
await it(`returns ${expected} for delta=${delta}, base=${base}, fraction=${fraction}`, () => {
|
||||
assert.equal(isDeltaSignificant(delta, base, fraction), expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("buildCommentBody", async () => {
|
||||
await it("includes the marker, the base/PR/delta rows, and the run URL", () => {
|
||||
const body = buildCommentBody({
|
||||
baseRef: "main",
|
||||
baseSize: 2_000_000,
|
||||
prSize: 2_300_000,
|
||||
runUrl: "https://example.test/run",
|
||||
});
|
||||
|
||||
assert.match(body, new RegExp(`^${escapeRegExp(COMMENT_MARKER)}`));
|
||||
assert.match(body, /Base \(`main`\) \| 1953\.13 KiB \(2000000 bytes\)/);
|
||||
assert.match(body, /This PR \| 2246\.09 KiB \(2300000 bytes\)/);
|
||||
assert.match(
|
||||
body,
|
||||
/\*\*Delta\*\* \| \*\*\+292\.97 KiB \(\+300000 bytes, \+15\.00%\)\*\*/,
|
||||
);
|
||||
assert.match(body, /\[workflow run\]\(https:\/\/example\.test\/run\)/);
|
||||
});
|
||||
|
||||
await it("formats negative deltas with a leading minus and omits the run URL when missing", () => {
|
||||
const body = buildCommentBody({
|
||||
baseRef: "main",
|
||||
baseSize: 2_000_000,
|
||||
prSize: 1_800_000,
|
||||
});
|
||||
assert.match(
|
||||
body,
|
||||
/\*\*Delta\*\* \| \*\*-195\.31 KiB \(-200000 bytes, -10\.00%\)\*\*/,
|
||||
);
|
||||
assert.doesNotMatch(body, /workflow run/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readArgs", async () => {
|
||||
await it("defaults the base ref and head commit for local runs", () => {
|
||||
const originalEnv = process.env;
|
||||
const originalArgv = process.argv;
|
||||
|
||||
try {
|
||||
process.env = {};
|
||||
process.argv = ["node", "check-repo-size.ts", "--output-dir", "/tmp/out"];
|
||||
|
||||
const args = readArgs();
|
||||
|
||||
assert.equal(args.baseRef, DEFAULT_BASE_REF);
|
||||
assert.equal(args.baseCommitish, `origin/${DEFAULT_BASE_REF}`);
|
||||
assert.equal(args.headCommitish, "HEAD");
|
||||
assert.equal(args.outputDir, "/tmp/out");
|
||||
assert.equal(args.runUrl, undefined);
|
||||
} finally {
|
||||
process.env = originalEnv;
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
|
||||
await it("uses the base and head SHAs when provided by the workflow", () => {
|
||||
const originalEnv = process.env;
|
||||
const originalArgv = process.argv;
|
||||
|
||||
try {
|
||||
process.env = {
|
||||
BASE_REF: "main",
|
||||
BASE_SHA: "abc123",
|
||||
HEAD_SHA: "def456",
|
||||
RUN_URL: "https://example.test/run",
|
||||
};
|
||||
process.argv = ["node", "check-repo-size.ts", "--output-dir", "/tmp/out"];
|
||||
|
||||
const args = readArgs();
|
||||
|
||||
assert.equal(args.baseRef, "main");
|
||||
assert.equal(args.baseCommitish, "abc123");
|
||||
assert.equal(args.headCommitish, "def456");
|
||||
assert.equal(args.outputDir, "/tmp/out");
|
||||
assert.equal(args.runUrl, "https://example.test/run");
|
||||
} finally {
|
||||
process.env = originalEnv;
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
|
||||
await it("throws when --output-dir is missing", () => {
|
||||
const originalEnv = process.env;
|
||||
const originalArgv = process.argv;
|
||||
|
||||
try {
|
||||
process.env = {};
|
||||
process.argv = ["node", "check-repo-size.ts"];
|
||||
assert.throws(() => readArgs(), /--output-dir is required/);
|
||||
} finally {
|
||||
process.env = originalEnv;
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let repoDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "check-repo-size-test-"));
|
||||
execFileSync("git", ["init", "--initial-branch=main", "-q"], {
|
||||
cwd: repoDir,
|
||||
});
|
||||
execFileSync("git", ["config", "user.email", "test@example.test"], {
|
||||
cwd: repoDir,
|
||||
});
|
||||
execFileSync("git", ["config", "user.name", "Test"], { cwd: repoDir });
|
||||
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: repoDir });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(repoDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function commit(name: string, content: string, message: string) {
|
||||
fs.writeFileSync(path.join(repoDir, name), content);
|
||||
execFileSync("git", ["add", name], { cwd: repoDir });
|
||||
execFileSync("git", ["commit", "-q", "-m", message], { cwd: repoDir });
|
||||
}
|
||||
|
||||
describe("measureArchiveSize", async () => {
|
||||
await it("returns a positive byte count for a non-empty repo", async () => {
|
||||
commit("a.txt", "hello world\n", "first");
|
||||
const size = await measureArchiveSize("HEAD", repoDir);
|
||||
assert.ok(size > 0, `expected size > 0, got ${size}`);
|
||||
});
|
||||
|
||||
await it("returns the same size on repeated runs (deterministic)", async () => {
|
||||
commit("a.txt", "hello world\n", "first");
|
||||
const a = await measureArchiveSize("HEAD", repoDir);
|
||||
const b = await measureArchiveSize("HEAD", repoDir);
|
||||
assert.equal(a, b);
|
||||
});
|
||||
|
||||
await it("returns a larger size when more content is added", async () => {
|
||||
commit("a.txt", "hello world\n", "first");
|
||||
const small = await measureArchiveSize("HEAD", repoDir);
|
||||
|
||||
// Use random bytes so the new content is incompressible and the archive
|
||||
// is guaranteed to grow even after gzip.
|
||||
commit("b.bin", randomBytes(8192).toString("base64"), "second");
|
||||
const big = await measureArchiveSize("HEAD", repoDir);
|
||||
assert.ok(
|
||||
big > small,
|
||||
`expected ${big} > ${small} after adding more content`,
|
||||
);
|
||||
});
|
||||
|
||||
await it("ignores untracked files (e.g. node_modules)", async () => {
|
||||
commit("a.txt", "hello\n", "first");
|
||||
commit(".gitignore", "node_modules/\n", "ignore node_modules");
|
||||
const sizeBefore = await measureArchiveSize("HEAD", repoDir);
|
||||
|
||||
fs.mkdirSync(path.join(repoDir, "node_modules"));
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, "node_modules", "huge.bin"),
|
||||
"x".repeat(1_000_000),
|
||||
);
|
||||
|
||||
const sizeAfter = await measureArchiveSize("HEAD", repoDir);
|
||||
assert.equal(
|
||||
sizeAfter,
|
||||
sizeBefore,
|
||||
"untracked node_modules should not affect the archive size",
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects when the ref does not exist", async () => {
|
||||
commit("a.txt", "hello\n", "first");
|
||||
await assert.rejects(
|
||||
() => measureArchiveSize("does-not-exist", repoDir),
|
||||
/git archive does-not-exist exited with code/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/*
|
||||
Measures the difference in the `.tar.gz`'d checkout size of the repo between the PR head and the PR
|
||||
base. This size is relevant because it corresponds to the duration of the "Download action
|
||||
repository" step that happens at the start of every job that uses this Action.
|
||||
|
||||
Writes the candidate sticky-comment body and a small metadata file to `--output-dir`. A separate
|
||||
workflow job consumes those artifacts and decides whether to create or update a PR comment.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { REPO_ROOT } from "./config";
|
||||
|
||||
/** Hidden marker used to find the existing sticky comment on a PR. */
|
||||
export const COMMENT_MARKER = "<!-- repo-size-diff-bot -->";
|
||||
|
||||
export const DEFAULT_BASE_REF = "main";
|
||||
|
||||
/**
|
||||
* Fraction of the base archive size at which a delta is considered significant enough to warrant
|
||||
* a new sticky comment. We always update an existing comment regardless, so the comment stays in
|
||||
* sync as the diff evolves.
|
||||
*/
|
||||
export const SIGNIFICANT_DELTA_FRACTION = 0.1;
|
||||
|
||||
/**
|
||||
* Stream `git archive --format=tar.gz <ref>` and count the compressed bytes.
|
||||
*
|
||||
* `git archive` only includes tracked files, so untracked directories like `node_modules` and
|
||||
* `build` aren't counted in the size downloaded when starting up a CodeQL job.
|
||||
*/
|
||||
export async function measureArchiveSize(
|
||||
ref: string,
|
||||
cwd: string,
|
||||
): Promise<number> {
|
||||
const git = spawn("git", ["archive", "--format=tar.gz", ref], { cwd });
|
||||
|
||||
let stderr = "";
|
||||
git.stderr.on("data", (chunk: Buffer) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
let size = 0;
|
||||
git.stdout.on("data", (chunk: Buffer) => {
|
||||
size += chunk.length;
|
||||
});
|
||||
|
||||
const exitCode = await new Promise<number>((resolve, reject) => {
|
||||
git.on("error", reject);
|
||||
git.on("close", resolve);
|
||||
});
|
||||
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`git archive ${ref} exited with code ${exitCode}: ${stderr.trim()}`,
|
||||
);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a byte count as KiB. If `signed` is true, a leading `+` is prepended for non-negative
|
||||
* values so gains and losses are visually distinct.
|
||||
*/
|
||||
export function formatBytes(bytes: number, signed = false): string {
|
||||
const sign = bytes < 0 ? "-" : signed ? "+" : "";
|
||||
const kib = Math.abs(bytes) / 1024;
|
||||
return `${sign}${kib.toFixed(2)} KiB`;
|
||||
}
|
||||
|
||||
/** Format a fraction as a signed percentage with 2 decimal places. */
|
||||
export function formatPercent(fraction: number): string {
|
||||
const pct = fraction * 100;
|
||||
const sign = pct > 0 ? "+" : "";
|
||||
return `${sign}${pct.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export interface CommentBodyOptions {
|
||||
baseRef: string;
|
||||
baseSize: number;
|
||||
prSize: number;
|
||||
/** Optional URL of the workflow run, included in the comment footer. */
|
||||
runUrl?: string;
|
||||
}
|
||||
|
||||
export function buildCommentBody(opts: CommentBodyOptions): string {
|
||||
const { baseRef, baseSize, prSize, runUrl } = opts;
|
||||
const delta = prSize - baseSize;
|
||||
const signedDelta = delta >= 0 ? `+${delta}` : `${delta}`;
|
||||
const runUrlLine = runUrl
|
||||
? ` See the [workflow run](${runUrl}) for details.`
|
||||
: "";
|
||||
|
||||
return [
|
||||
COMMENT_MARKER,
|
||||
"### Repository checkout size",
|
||||
"",
|
||||
"| | Compressed archive size |",
|
||||
"|---|---|",
|
||||
`| Base (\`${baseRef}\`) | ${formatBytes(baseSize)} (${baseSize} bytes) |`,
|
||||
`| This PR | ${formatBytes(prSize)} (${prSize} bytes) |`,
|
||||
`| **Delta** | **${formatBytes(delta, true)} (${signedDelta} bytes, ${formatPercent(delta / baseSize)})** |`,
|
||||
"",
|
||||
"Sizes are measured by streaming `git archive --format=tar.gz <ref>`, " +
|
||||
"which includes tracked files and excludes untracked files such as " +
|
||||
"`node_modules`. The compressed checkout is " +
|
||||
"downloaded by every consumer of this Action, so changes here directly " +
|
||||
`affect Action download time.${runUrlLine}`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the absolute delta is at least `fraction` of the base size. Both increases and
|
||||
* decreases are considered significant, so we report wins as well as losses.
|
||||
*/
|
||||
export function isDeltaSignificant(
|
||||
delta: number,
|
||||
baseSize: number,
|
||||
fraction: number,
|
||||
): boolean {
|
||||
return Math.abs(delta) >= baseSize * fraction;
|
||||
}
|
||||
|
||||
interface MainArgs {
|
||||
/** Base ref of the PR. Defaults to `main`. Used as the label in the PR comment. */
|
||||
baseRef: string;
|
||||
/** Base commit-ish to archive. Defaults to `origin/<baseRef>` for local runs. */
|
||||
baseCommitish: string;
|
||||
/** Head commit-ish to archive. Defaults to `HEAD` for local runs. */
|
||||
headCommitish: string;
|
||||
/** Optional URL of the workflow run, surfaced in the comment footer. */
|
||||
runUrl?: string;
|
||||
/** Directory where `body.md` and `metadata.json` are written. */
|
||||
outputDir: string;
|
||||
}
|
||||
|
||||
export function readArgs(): MainArgs {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
"output-dir": { type: "string" },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const outputDir = values["output-dir"];
|
||||
if (!outputDir) {
|
||||
throw new Error("--output-dir is required");
|
||||
}
|
||||
|
||||
const baseRef = process.env.BASE_REF ?? DEFAULT_BASE_REF;
|
||||
const baseCommitish = process.env.BASE_SHA ?? `origin/${baseRef}`;
|
||||
const headCommitish = process.env.HEAD_SHA ?? "HEAD";
|
||||
|
||||
return {
|
||||
baseRef,
|
||||
baseCommitish,
|
||||
headCommitish,
|
||||
runUrl: process.env.RUN_URL,
|
||||
outputDir,
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const args = readArgs();
|
||||
|
||||
console.log(`Measuring base archive size for ${args.baseCommitish}...`);
|
||||
const baseSize = await measureArchiveSize(args.baseCommitish, REPO_ROOT);
|
||||
console.log(` ${baseSize} bytes`);
|
||||
|
||||
console.log(`Measuring PR archive size for ${args.headCommitish}...`);
|
||||
const prSize = await measureArchiveSize(args.headCommitish, REPO_ROOT);
|
||||
console.log(` ${prSize} bytes`);
|
||||
|
||||
const delta = prSize - baseSize;
|
||||
const significant = isDeltaSignificant(
|
||||
delta,
|
||||
baseSize,
|
||||
SIGNIFICANT_DELTA_FRACTION,
|
||||
);
|
||||
console.log(
|
||||
`Delta: ${delta} bytes (significant=${significant}, threshold=${(
|
||||
SIGNIFICANT_DELTA_FRACTION * 100
|
||||
).toFixed(2)}%)`,
|
||||
);
|
||||
|
||||
const body = buildCommentBody({
|
||||
baseRef: args.baseRef,
|
||||
baseSize,
|
||||
prSize,
|
||||
runUrl: args.runUrl,
|
||||
});
|
||||
|
||||
fs.mkdirSync(args.outputDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(args.outputDir, "body.md"), body);
|
||||
fs.writeFileSync(
|
||||
path.join(args.outputDir, "metadata.json"),
|
||||
`${JSON.stringify(
|
||||
{ significant, baseRef: args.baseRef, baseSize, prSize, delta },
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
console.log(`Wrote body.md and metadata.json to ${args.outputDir}.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
process.exit(await main());
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
void run();
|
||||
}
|
||||
+5
-2
@@ -6,14 +6,17 @@ export const OLDEST_SUPPORTED_MAJOR_VERSION = 3;
|
||||
/** The `pr-checks` directory. */
|
||||
export const PR_CHECKS_DIR = __dirname;
|
||||
|
||||
/** The repository root. */
|
||||
export const REPO_ROOT = path.join(PR_CHECKS_DIR, "..");
|
||||
|
||||
/** The path of the file configuring which checks shouldn't be required. */
|
||||
export const PR_CHECK_EXCLUDED_FILE = path.join(PR_CHECKS_DIR, "excluded.yml");
|
||||
|
||||
/** The path to the esbuild metadata file. */
|
||||
export const BUNDLE_METADATA_FILE = path.join(PR_CHECKS_DIR, "..", "meta.json");
|
||||
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||
|
||||
/** The `src` directory. */
|
||||
const SOURCE_ROOT = path.join(PR_CHECKS_DIR, "..", "src");
|
||||
const SOURCE_ROOT = path.join(REPO_ROOT, "src");
|
||||
|
||||
/** The path to the built-in languages file. */
|
||||
export const BUILTIN_LANGUAGES_FILE = path.join(
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
# PR checks to exclude from required checks
|
||||
contains:
|
||||
- "https://"
|
||||
- "Update"
|
||||
- "ESLint"
|
||||
- "update"
|
||||
- "https://"
|
||||
- "test-setup-python-scripts"
|
||||
- "update"
|
||||
- "Update"
|
||||
is:
|
||||
- "Agent"
|
||||
- "check-expected-release-files"
|
||||
- "Cleanup artifacts"
|
||||
- "CodeQL"
|
||||
- "Dependabot"
|
||||
- "check-expected-release-files"
|
||||
- "Agent"
|
||||
- "Cleanup artifacts"
|
||||
- "Label PR with size"
|
||||
- "Post repo size comment"
|
||||
- "Prepare"
|
||||
- "Upload results"
|
||||
- "Label PR with size"
|
||||
|
||||
@@ -43,6 +43,7 @@ predicate envVarRead(DataFlow::Node node, string envVar) {
|
||||
from DataFlow::Node read, string envVar
|
||||
where
|
||||
envVarRead(read, envVar) and
|
||||
read.getFile().getRelativePath().matches("src/%") and
|
||||
not read.getFile().getBaseName().matches("%.test.ts") and
|
||||
not isSafeForDefaultSetup(envVar)
|
||||
select read,
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function runWrapper() {
|
||||
logger,
|
||||
);
|
||||
if (config !== undefined) {
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const version = await codeql.getVersion();
|
||||
await debugArtifacts.uploadCombinedSarifArtifacts(
|
||||
logger,
|
||||
|
||||
@@ -256,7 +256,7 @@ async function run(startedAt: Date) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
if (hasBadExpectErrorInput()) {
|
||||
throw new util.ConfigurationError(
|
||||
|
||||
@@ -101,7 +101,7 @@ async function run(startedAt: Date) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(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, config.codeQLMetadata);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd);
|
||||
if (language === BuiltInLanguage.cpp) {
|
||||
await setupCppAutobuild(codeQL, logger);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { ExecOptions } from "@actions/exec";
|
||||
import * as toolrunner from "@actions/exec/lib/toolrunner";
|
||||
@@ -124,166 +123,6 @@ async function stubCodeql(): Promise<codeql.CodeQL> {
|
||||
return codeqlObject;
|
||||
}
|
||||
|
||||
function stubSuccessfulToolRunner(
|
||||
stdoutForArgs: (args: string[]) => string | undefined,
|
||||
): sinon.SinonStub<any[], toolrunner.ToolRunner> {
|
||||
const runnerConstructorStub = sinon.stub(
|
||||
toolrunner,
|
||||
"ToolRunner",
|
||||
) as sinon.SinonStub<any[], toolrunner.ToolRunner>;
|
||||
|
||||
runnerConstructorStub.callsFake((_cmd, args, options: ExecOptions) => {
|
||||
return {
|
||||
exec: async () => {
|
||||
const stdout = stdoutForArgs(args as string[]);
|
||||
if (stdout !== undefined) {
|
||||
options.listeners?.stdout?.(Buffer.from(stdout));
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
} as toolrunner.ToolRunner;
|
||||
});
|
||||
|
||||
return runnerConstructorStub;
|
||||
}
|
||||
|
||||
test.serial("getVersion and printVersion share cached version", async (t) => {
|
||||
const version = { version: "2.30.0" };
|
||||
let versionCalls = 0;
|
||||
stubSuccessfulToolRunner((args) => {
|
||||
if (args.join(" ") === "version --format=json") {
|
||||
versionCalls++;
|
||||
return JSON.stringify(version);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||
|
||||
t.deepEqual(await codeqlObject.getVersion(), version);
|
||||
await codeqlObject.printVersion();
|
||||
|
||||
t.is(versionCalls, 1);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"betterResolveLanguages caches only the unfiltered result",
|
||||
async (t) => {
|
||||
const unfilteredLanguages = {
|
||||
aliases: { typescript: BuiltInLanguage.javascript },
|
||||
extractors: {
|
||||
html: [{ extractor_root: "/html" }],
|
||||
javascript: [{ extractor_root: "/javascript" }],
|
||||
},
|
||||
};
|
||||
const filteredLanguages = {
|
||||
aliases: { typescript: BuiltInLanguage.javascript },
|
||||
extractors: {
|
||||
javascript: [{ extractor_root: "/javascript" }],
|
||||
},
|
||||
};
|
||||
|
||||
let unfilteredCalls = 0;
|
||||
let filteredCalls = 0;
|
||||
stubSuccessfulToolRunner((args) => {
|
||||
if (args[0] === "resolve" && args[1] === "languages") {
|
||||
if (args.includes("--filter-to-languages-with-queries")) {
|
||||
filteredCalls++;
|
||||
return JSON.stringify(filteredLanguages);
|
||||
}
|
||||
unfilteredCalls++;
|
||||
return JSON.stringify(unfilteredLanguages);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||
|
||||
t.deepEqual(
|
||||
await codeqlObject.betterResolveLanguages(),
|
||||
unfilteredLanguages,
|
||||
);
|
||||
t.deepEqual(
|
||||
await codeqlObject.betterResolveLanguages(),
|
||||
unfilteredLanguages,
|
||||
);
|
||||
t.deepEqual(
|
||||
await codeqlObject.betterResolveLanguages({
|
||||
filterToLanguagesWithQueries: true,
|
||||
}),
|
||||
filteredLanguages,
|
||||
);
|
||||
t.deepEqual(
|
||||
await codeqlObject.betterResolveLanguages({
|
||||
filterToLanguagesWithQueries: true,
|
||||
}),
|
||||
filteredLanguages,
|
||||
);
|
||||
|
||||
// The unfiltered result is cached after the first call; the filtered
|
||||
// variant is not cached because nothing reuses it.
|
||||
t.is(unfilteredCalls, 1);
|
||||
t.is(filteredCalls, 2);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("resolveExtractor caches its result per language", async (t) => {
|
||||
await util.withTmpDir(async (tempDir) => {
|
||||
const extractorRoot = path.join(tempDir, "javascript");
|
||||
fs.mkdirSync(path.join(extractorRoot, "tools"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(extractorRoot, "tools", "tracing-config.lua"),
|
||||
"",
|
||||
);
|
||||
|
||||
let resolveExtractorCalls = 0;
|
||||
stubSuccessfulToolRunner((args) => {
|
||||
if (args[0] === "resolve" && args[1] === "extractor") {
|
||||
resolveExtractorCalls++;
|
||||
return JSON.stringify(extractorRoot);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||
|
||||
t.is(
|
||||
await codeqlObject.resolveExtractor(BuiltInLanguage.javascript),
|
||||
extractorRoot,
|
||||
);
|
||||
t.is(
|
||||
await codeqlObject.resolveExtractor(BuiltInLanguage.javascript),
|
||||
extractorRoot,
|
||||
);
|
||||
t.true(await codeqlObject.isTracedLanguage(BuiltInLanguage.javascript));
|
||||
t.is(resolveExtractorCalls, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"hydrateCliMetadata seeds the cache from persisted metadata",
|
||||
async (t) => {
|
||||
let cliCalls = 0;
|
||||
stubSuccessfulToolRunner((_args) => {
|
||||
cliCalls++;
|
||||
return "{}";
|
||||
});
|
||||
|
||||
const codeqlObject = await codeql.getCodeQLForTesting();
|
||||
|
||||
codeqlObject.hydrateCliMetadata({
|
||||
codeQLCmd: "codeql-for-testing",
|
||||
extractorPaths: { javascript: "/javascript" },
|
||||
});
|
||||
|
||||
t.is(
|
||||
await codeqlObject.resolveExtractor(BuiltInLanguage.javascript),
|
||||
"/javascript",
|
||||
);
|
||||
t.is(cliCalls, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloads and caches explicitly requested bundles that aren't in the toolcache",
|
||||
async (t) => {
|
||||
|
||||
+13
-87
@@ -218,10 +218,6 @@ export interface CodeQL {
|
||||
outputFile: string,
|
||||
options: { mergeRunsFromEqualCategory?: boolean },
|
||||
): Promise<void>;
|
||||
/** Return cacheable metadata gathered from the CodeQL CLI. */
|
||||
getCliMetadata(): CodeQLCliMetadata;
|
||||
/** Hydrate the CodeQL wrapper with cacheable metadata gathered earlier in the job. */
|
||||
hydrateCliMetadata(metadata: CodeQLCliMetadata | undefined): void;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
@@ -251,10 +247,12 @@ export interface BetterResolveLanguagesOutput {
|
||||
[alias: string]: string;
|
||||
};
|
||||
extractors: {
|
||||
[language: string]: Array<{
|
||||
extractor_root: string;
|
||||
extractor_options?: any;
|
||||
}>;
|
||||
[language: string]: [
|
||||
{
|
||||
extractor_root: string;
|
||||
extractor_options?: any;
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,11 +264,6 @@ export interface ResolveBuildEnvironmentOutput {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeQLCliMetadata {
|
||||
codeQLCmd: string;
|
||||
extractorPaths?: { [language: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the CodeQL object, and is populated by `setupCodeQL` or `getCodeQL`.
|
||||
*/
|
||||
@@ -399,14 +392,9 @@ export async function setupCodeQL(
|
||||
/**
|
||||
* Use the CodeQL executable located at the given path.
|
||||
*/
|
||||
export async function getCodeQL(
|
||||
cmd: string,
|
||||
cliMetadata?: CodeQLCliMetadata,
|
||||
): Promise<CodeQL> {
|
||||
export async function getCodeQL(cmd: string): Promise<CodeQL> {
|
||||
if (cachedCodeQL === undefined) {
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true, cliMetadata);
|
||||
} else {
|
||||
cachedCodeQL.hydrateCliMetadata(cliMetadata);
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true);
|
||||
}
|
||||
return cachedCodeQL;
|
||||
}
|
||||
@@ -504,14 +492,6 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||
),
|
||||
resolveDatabase: resolveFunction(partialCodeql, "resolveDatabase"),
|
||||
mergeResults: resolveFunction(partialCodeql, "mergeResults"),
|
||||
getCliMetadata: resolveFunction(partialCodeql, "getCliMetadata", () => ({
|
||||
codeQLCmd: partialCodeql.getPath?.() ?? "/tmp/dummy-path",
|
||||
})),
|
||||
hydrateCliMetadata: resolveFunction(
|
||||
partialCodeql,
|
||||
"hydrateCliMetadata",
|
||||
() => {},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -526,12 +506,6 @@ export async function getCodeQLForTesting(
|
||||
return getCodeQLForCmd(cmd, false);
|
||||
}
|
||||
|
||||
function cacheCodeQlVersionForStatusReports(versionInfo: VersionInfo): void {
|
||||
if (util.getCachedCodeQlVersion() === undefined) {
|
||||
util.cacheCodeQlVersion(versionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a CodeQL object for CodeQL CLI access.
|
||||
*
|
||||
@@ -543,24 +517,13 @@ function cacheCodeQlVersionForStatusReports(versionInfo: VersionInfo): void {
|
||||
async function getCodeQLForCmd(
|
||||
cmd: string,
|
||||
checkVersion: boolean,
|
||||
initialCliMetadata?: CodeQLCliMetadata,
|
||||
): Promise<CodeQL> {
|
||||
// Metadata persisted across the init/autobuild/analyze steps. Only extractor
|
||||
// paths are reused by a later step, so that's all this holds.
|
||||
const cliMetadata: CodeQLCliMetadata = { codeQLCmd: cmd };
|
||||
// In-process-only caches. These aren't persisted because no later step reuses
|
||||
// them: the CLI version always matches across steps, and `resolve languages`
|
||||
// is only re-read within a single step.
|
||||
let cachedVersion: VersionInfo | undefined;
|
||||
let cachedUnfilteredBetterResolveLanguages:
|
||||
| BetterResolveLanguagesOutput
|
||||
| undefined;
|
||||
const codeql: CodeQL = {
|
||||
getPath() {
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = cachedVersion;
|
||||
let result = util.getCachedCodeQlVersion();
|
||||
if (result === undefined) {
|
||||
const output = await runCli(cmd, ["version", "--format=json"], {
|
||||
noStreamStdout: true,
|
||||
@@ -572,15 +535,12 @@ async function getCodeQLForCmd(
|
||||
`Invalid JSON output from \`version --format=json\`: ${output}`,
|
||||
);
|
||||
}
|
||||
cachedVersion = result;
|
||||
util.cacheCodeQlVersion(result);
|
||||
}
|
||||
cacheCodeQlVersionForStatusReports(result);
|
||||
return result;
|
||||
},
|
||||
async printVersion() {
|
||||
const version = await this.getVersion();
|
||||
process.stdout.write(`[command]${cmd} version --format=json\n`);
|
||||
process.stdout.write(`${JSON.stringify(version)}\n`);
|
||||
await runCli(cmd, ["version", "--format=json"]);
|
||||
},
|
||||
async supportsFeature(feature: ToolsFeature) {
|
||||
return isSupportedToolsFeature(await this.getVersion(), feature);
|
||||
@@ -798,13 +758,6 @@ async function getCodeQLForCmd(
|
||||
filterToLanguagesWithQueries: boolean;
|
||||
} = { filterToLanguagesWithQueries: false },
|
||||
) {
|
||||
if (
|
||||
!filterToLanguagesWithQueries &&
|
||||
cachedUnfilteredBetterResolveLanguages
|
||||
) {
|
||||
return cachedUnfilteredBetterResolveLanguages;
|
||||
}
|
||||
|
||||
const codeqlArgs = [
|
||||
"resolve",
|
||||
"languages",
|
||||
@@ -819,11 +772,7 @@ async function getCodeQLForCmd(
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
const result = JSON.parse(output) as BetterResolveLanguagesOutput;
|
||||
if (!filterToLanguagesWithQueries) {
|
||||
cachedUnfilteredBetterResolveLanguages = result;
|
||||
}
|
||||
return result;
|
||||
return JSON.parse(output) as BetterResolveLanguagesOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
|
||||
@@ -1019,11 +968,6 @@ async function getCodeQLForCmd(
|
||||
await new toolrunner.ToolRunner(cmd, args).exec();
|
||||
},
|
||||
async resolveExtractor(language: Language): Promise<string> {
|
||||
const cachedExtractorPath = cliMetadata.extractorPaths?.[language];
|
||||
if (cachedExtractorPath !== undefined) {
|
||||
return cachedExtractorPath;
|
||||
}
|
||||
|
||||
// Request it using `format=json` so we don't need to strip the trailing new line generated by
|
||||
// the CLI.
|
||||
let extractorPath = "";
|
||||
@@ -1049,10 +993,7 @@ async function getCodeQLForCmd(
|
||||
},
|
||||
},
|
||||
).exec();
|
||||
const resolvedExtractorPath = JSON.parse(extractorPath) as string;
|
||||
cliMetadata.extractorPaths ??= {};
|
||||
cliMetadata.extractorPaths[language] = resolvedExtractorPath;
|
||||
return resolvedExtractorPath;
|
||||
return JSON.parse(extractorPath) as string;
|
||||
},
|
||||
async resolveQueriesStartingPacks(queries: string[]): Promise<string[]> {
|
||||
const codeqlArgs = [
|
||||
@@ -1117,22 +1058,7 @@ async function getCodeQLForCmd(
|
||||
|
||||
await runCli(cmd, args);
|
||||
},
|
||||
getCliMetadata() {
|
||||
return cliMetadata;
|
||||
},
|
||||
hydrateCliMetadata(metadata: CodeQLCliMetadata | undefined): void {
|
||||
if (metadata?.codeQLCmd !== cliMetadata.codeQLCmd) {
|
||||
return;
|
||||
}
|
||||
|
||||
cliMetadata.extractorPaths = {
|
||||
...metadata.extractorPaths,
|
||||
...cliMetadata.extractorPaths,
|
||||
};
|
||||
},
|
||||
};
|
||||
// Seed the cache with any metadata persisted by an earlier step.
|
||||
codeql.hydrateCliMetadata(initialCliMetadata);
|
||||
// To ensure that status reports include the CodeQL CLI version wherever
|
||||
// possible, we want to call getVersion(), which populates the version value
|
||||
// used by status reporting, at the earliest opportunity. But invoking
|
||||
|
||||
+1
-6
@@ -19,7 +19,7 @@ import {
|
||||
} from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind, getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL, type CodeQLCliMetadata } from "./codeql";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
@@ -177,10 +177,6 @@ export interface Config {
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Cacheable metadata gathered from the CodeQL CLI while initializing the workflow.
|
||||
*/
|
||||
codeQLMetadata?: CodeQLCliMetadata;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
@@ -565,7 +561,6 @@ export async function initActionState(
|
||||
computedConfig,
|
||||
tempDir,
|
||||
codeQLCmd: codeql.getPath(),
|
||||
codeQLMetadata: codeql.getCliMetadata(),
|
||||
gitHubVersion: githubVersion,
|
||||
dbLocation: dbLocationOrDefault(dbLocation, tempDir),
|
||||
debugMode,
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.4",
|
||||
"cliVersion": "2.25.4",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.3",
|
||||
"priorCliVersion": "2.25.3"
|
||||
"bundleVersion": "codeql-bundle-v2.25.5",
|
||||
"cliVersion": "2.25.5",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.4",
|
||||
"priorCliVersion": "2.25.4"
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ async function generateFailedSarif(
|
||||
sarifFile?: string,
|
||||
) {
|
||||
const databasePath = config.dbLocation;
|
||||
const codeql = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(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, config.codeQLMetadata);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
uploadFailedSarifResult = await initActionPostHelper.uploadFailureInfo(
|
||||
debugArtifacts.tryUploadAllAvailableDebugArtifacts,
|
||||
|
||||
@@ -560,7 +560,7 @@ export function mockBundleDownloadApi({
|
||||
}
|
||||
|
||||
export function createTestConfig(overrides: Partial<Config>): Config {
|
||||
const config = Object.assign(
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
version: getActionVersion(),
|
||||
@@ -590,8 +590,6 @@ export function createTestConfig(overrides: Partial<Config>): Config {
|
||||
} satisfies Config,
|
||||
overrides,
|
||||
);
|
||||
config.codeQLMetadata ??= { codeQLCmd: config.codeQLCmd };
|
||||
return config;
|
||||
}
|
||||
|
||||
export function makeTestToken(length: number = 36) {
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ async function combineSarifFilesUsingCLI(
|
||||
|
||||
const config = await getConfig(tempDir, logger);
|
||||
if (config !== undefined) {
|
||||
codeQL = await getCodeQL(config.codeQLCmd, config.codeQLMetadata);
|
||||
codeQL = await getCodeQL(config.codeQLCmd);
|
||||
tempDir = config.tempDir;
|
||||
} else {
|
||||
logger.info(
|
||||
|
||||
Reference in New Issue
Block a user