mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +00:00
Merge pull request #4124 from github/henrymercer/toolcache-bundle-cleanup
Delete unused CodeQL bundles from the toolcache before downloading
This commit is contained in:
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
@@ -191,5 +191,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
|
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||||
CODEQL_ACTION_TEST_MODE: true
|
CODEQL_ACTION_TEST_MODE: true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
|||||||
|
|
||||||
## [UNRELEASED]
|
## [UNRELEASED]
|
||||||
|
|
||||||
|
- On GitHub-hosted runners, the CodeQL Action now deletes unused CodeQL bundles from the toolcache before downloading a different bundle, which frees up disk space for the analysis. We expect to roll this change out to everyone in September. [#4124](https://github.com/github/codeql-action/pull/4124)
|
||||||
- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072)
|
- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072)
|
||||||
|
|
||||||
## 4.37.9 - 26 Aug 2026
|
## 4.37.9 - 26 Aug 2026
|
||||||
|
|||||||
890
lib/entry-points.js
generated
890
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ operatingSystems:
|
|||||||
- stable-v2.21.4
|
- stable-v2.21.4
|
||||||
- stable-v2.22.4
|
- stable-v2.22.4
|
||||||
env:
|
env:
|
||||||
|
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||||
installGo: true
|
installGo: true
|
||||||
installDotNet: true
|
installDotNet: true
|
||||||
|
|||||||
@@ -283,6 +283,19 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
|
|||||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the job is running on a runner that GitHub hosts, and whose toolcache is therefore thrown
|
||||||
|
* away once the job has finished.
|
||||||
|
*
|
||||||
|
* Unlike `looksLikeHostedRunner`, this is based on what the service reports for the job rather than
|
||||||
|
* on how the runner's filesystem happens to be laid out, so it does not match self-hosted runners
|
||||||
|
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
|
||||||
|
* `/opt/hostedtoolcache`.
|
||||||
|
*/
|
||||||
|
export function isGitHubHostedRunner(env: Env = getEnv()) {
|
||||||
|
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
|
||||||
|
}
|
||||||
|
|
||||||
/** Determines whether the workflow trigger is `dynamic`. */
|
/** Determines whether the workflow trigger is `dynamic`. */
|
||||||
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
||||||
return getWorkflowEventName(env) === "dynamic";
|
return getWorkflowEventName(env) === "dynamic";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as core from "@actions/core";
|
|||||||
import { getOptionalInput, isDefaultSetup } from "./actions-util";
|
import { getOptionalInput, isDefaultSetup } from "./actions-util";
|
||||||
import { EnvVar } from "./environment";
|
import { EnvVar } from "./environment";
|
||||||
import { Logger } from "./logging";
|
import { Logger } from "./logging";
|
||||||
import { isHostedRunner, tryGetFolderBytes } from "./util";
|
import { looksLikeHostedRunner, tryGetFolderBytes } from "./util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total size of all the specified paths.
|
* Returns the total size of all the specified paths.
|
||||||
@@ -109,7 +109,7 @@ export function getDependencyCachingEnabled(): CachingKind {
|
|||||||
if (dependencyCaching !== undefined) return getCachingKind(dependencyCaching);
|
if (dependencyCaching !== undefined) return getCachingKind(dependencyCaching);
|
||||||
|
|
||||||
// On self-hosted runners which may have dependencies installed centrally, disable caching by default
|
// On self-hosted runners which may have dependencies installed centrally, disable caching by default
|
||||||
if (!isHostedRunner()) return CachingKind.None;
|
if (!looksLikeHostedRunner()) return CachingKind.None;
|
||||||
|
|
||||||
// Disable in advanced workflows by default.
|
// Disable in advanced workflows by default.
|
||||||
if (!isDefaultSetup()) return CachingKind.None;
|
if (!isDefaultSetup()) return CachingKind.None;
|
||||||
|
|||||||
@@ -90,9 +90,8 @@ import {
|
|||||||
Result,
|
Result,
|
||||||
Success,
|
Success,
|
||||||
Failure,
|
Failure,
|
||||||
isHostedRunner,
|
looksLikeHostedRunner,
|
||||||
} from "./util";
|
} from "./util";
|
||||||
|
|
||||||
export { type Config } from "./config/action-config";
|
export { type Config } from "./config/action-config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -938,7 +937,7 @@ export async function isTrapCachingEnabled(
|
|||||||
if (trapCaching !== undefined) return trapCaching === "true";
|
if (trapCaching !== undefined) return trapCaching === "true";
|
||||||
|
|
||||||
// On self-hosted runners which may have slow network access, disable TRAP caching by default.
|
// On self-hosted runners which may have slow network access, disable TRAP caching by default.
|
||||||
if (!isHostedRunner()) return false;
|
if (!looksLikeHostedRunner()) return false;
|
||||||
|
|
||||||
// If overlay analysis is enabled, then disable TRAP caching since overlay analysis supersedes it.
|
// If overlay analysis is enabled, then disable TRAP caching since overlay analysis supersedes it.
|
||||||
// This change is gated behind a feature flag.
|
// This change is gated behind a feature flag.
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ export enum EnvVar {
|
|||||||
/** Whether the CodeQL Action has already warned the user about low disk space. */
|
/** Whether the CodeQL Action has already warned the user about low disk space. */
|
||||||
HAS_WARNED_ABOUT_DISK_SPACE = "CODEQL_ACTION_HAS_WARNED_ABOUT_DISK_SPACE",
|
HAS_WARNED_ABOUT_DISK_SPACE = "CODEQL_ACTION_HAS_WARNED_ABOUT_DISK_SPACE",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a step in this job has already set up CodeQL. Steps that run afterwards may be holding
|
||||||
|
* a path into the toolcache, so we must not delete anything from it.
|
||||||
|
*/
|
||||||
|
HAS_SET_UP_CODEQL = "CODEQL_ACTION_HAS_SET_UP_CODEQL",
|
||||||
|
|
||||||
/** Whether the `setup-codeql` action has been run. */
|
/** Whether the `setup-codeql` action has been run. */
|
||||||
SETUP_CODEQL_ACTION_HAS_RUN = "CODEQL_ACTION_SETUP_CODEQL_HAS_RUN",
|
SETUP_CODEQL_ACTION_HAS_RUN = "CODEQL_ACTION_SETUP_CODEQL_HAS_RUN",
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ export enum Feature {
|
|||||||
AllowMergeConfigFiles = "allow_merge_config_files",
|
AllowMergeConfigFiles = "allow_merge_config_files",
|
||||||
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
||||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||||
|
/**
|
||||||
|
* Controls whether we delete CodeQL bundles that we are not going to use from the toolcache
|
||||||
|
* before downloading a different bundle, in order to reclaim disk space.
|
||||||
|
*/
|
||||||
|
CleanupToolcacheBundles = "cleanup_toolcache_bundles",
|
||||||
CleanupTrapCaches = "cleanup_trap_caches",
|
CleanupTrapCaches = "cleanup_trap_caches",
|
||||||
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
||||||
ConfigFileRepositoryProperty = "config_file_repository_property",
|
ConfigFileRepositoryProperty = "config_file_repository_property",
|
||||||
@@ -211,6 +216,11 @@ export const featureConfig = {
|
|||||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||||
minimumVersion: undefined,
|
minimumVersion: undefined,
|
||||||
},
|
},
|
||||||
|
[Feature.CleanupToolcacheBundles]: {
|
||||||
|
defaultValue: false,
|
||||||
|
envVar: "CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES",
|
||||||
|
minimumVersion: undefined,
|
||||||
|
},
|
||||||
[Feature.CleanupTrapCaches]: {
|
[Feature.CleanupTrapCaches]: {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import * as github from "@actions/github";
|
import * as github from "@actions/github";
|
||||||
@@ -7,7 +9,8 @@ import * as sinon from "sinon";
|
|||||||
|
|
||||||
import * as actionsUtil from "./actions-util";
|
import * as actionsUtil from "./actions-util";
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
import { EnvVar } from "./environment";
|
import * as diagnostics from "./diagnostics";
|
||||||
|
import { ActionsEnvVars, EnvVar, ReadOnlyEnv } from "./environment";
|
||||||
import { Feature } from "./feature-flags";
|
import { Feature } from "./feature-flags";
|
||||||
import { getRunnerLogger } from "./logging";
|
import { getRunnerLogger } from "./logging";
|
||||||
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
|
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
|
||||||
@@ -27,6 +30,7 @@ import {
|
|||||||
setupActionsVars,
|
setupActionsVars,
|
||||||
setupTests,
|
setupTests,
|
||||||
} from "./testing-utils";
|
} from "./testing-utils";
|
||||||
|
import * as toolsDownload from "./tools-download";
|
||||||
import {
|
import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
GitHubVariant,
|
GitHubVariant,
|
||||||
@@ -939,3 +943,616 @@ test.serial(
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** The CLI version that the toolcache cleanup tests download. */
|
||||||
|
const CLEANUP_CLI_VERSION = "2.21.0";
|
||||||
|
/** The bundle version that the toolcache cleanup tests download. */
|
||||||
|
const CLEANUP_BUNDLE_VERSION = "20240101";
|
||||||
|
/** A version of the CodeQL tools that is already in the toolcache but that we are not going to use. */
|
||||||
|
const CLEANUP_STALE_VERSION = "2.20.0";
|
||||||
|
|
||||||
|
/** Creates a directory in the toolcache that looks like a tool that `tool-cache` has cached. */
|
||||||
|
function createToolcacheEntry(
|
||||||
|
toolcacheRoot: string,
|
||||||
|
tool: string,
|
||||||
|
version: string,
|
||||||
|
): string {
|
||||||
|
const versionDirectory = path.join(toolcacheRoot, tool, version);
|
||||||
|
const archDirectory = path.join(versionDirectory, os.arch());
|
||||||
|
fs.mkdirSync(archDirectory, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(archDirectory, "contents"), "x".repeat(1024));
|
||||||
|
fs.writeFileSync(`${archDirectory}.complete`, "");
|
||||||
|
return versionDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stubs out the download and the diagnostic sink, then downloads the CodeQL tools.
|
||||||
|
*
|
||||||
|
* @returns the extraction directory and the toolcache cleanup diagnostic, if emitted.
|
||||||
|
*/
|
||||||
|
async function runDownloadCodeQL(
|
||||||
|
toolcacheRoot: string,
|
||||||
|
features: Feature[],
|
||||||
|
bundleVersion: string | undefined,
|
||||||
|
): Promise<{
|
||||||
|
codeqlFolder: string;
|
||||||
|
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
|
||||||
|
}> {
|
||||||
|
sinon
|
||||||
|
.stub(toolsDownload, "downloadAndExtract")
|
||||||
|
.callsFake(async (_url, _compressionMethod, dest) => {
|
||||||
|
// The real implementation creates the destination directory, which matters here because the
|
||||||
|
// cleanup deletes it first and `writeToolcacheMarkerFile` writes into its parent afterwards.
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
return { totalDurationMs: 1 };
|
||||||
|
});
|
||||||
|
const addDiagnostic = sinon.stub(diagnostics, "addNoLanguageDiagnostic");
|
||||||
|
|
||||||
|
const { codeqlFolder } = await setupCodeql.downloadCodeQL(
|
||||||
|
"https://example.com/codeql-bundle.tar.gz",
|
||||||
|
"gzip",
|
||||||
|
bundleVersion,
|
||||||
|
CLEANUP_CLI_VERSION,
|
||||||
|
SAMPLE_DOTCOM_API_DETAILS,
|
||||||
|
undefined, // tarVersion
|
||||||
|
toolcacheRoot, // tempDir
|
||||||
|
createFeatures(features),
|
||||||
|
getRunnerLogger(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
const diagnostic = addDiagnostic
|
||||||
|
.getCalls()
|
||||||
|
.map((call) => call.args[1])
|
||||||
|
.find((d) => d.source?.id === "codeql-action/toolcache-bundle-cleanup");
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeqlFolder,
|
||||||
|
cleanupDiagnostic: diagnostic?.attributes as
|
||||||
|
| toolsDownload.ToolcacheCleanupResult
|
||||||
|
| undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a toolcache containing the version of the CodeQL tools that we are about to download, a
|
||||||
|
* different version of the CodeQL tools, and an unrelated tool, then downloads the CodeQL tools.
|
||||||
|
*/
|
||||||
|
async function testToolcacheCleanup(
|
||||||
|
t: ExecutionContext<unknown>,
|
||||||
|
{
|
||||||
|
features,
|
||||||
|
runnerEnvironment,
|
||||||
|
setUp,
|
||||||
|
}: {
|
||||||
|
features: Feature[];
|
||||||
|
runnerEnvironment: string | undefined;
|
||||||
|
setUp?: () => void;
|
||||||
|
},
|
||||||
|
check: (context: {
|
||||||
|
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
|
||||||
|
destinationDirectory: string;
|
||||||
|
staleDirectory: string;
|
||||||
|
}) => void,
|
||||||
|
) {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
if (runnerEnvironment === undefined) {
|
||||||
|
delete process.env[ActionsEnvVars.RUNNER_ENVIRONMENT];
|
||||||
|
} else {
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnvironment;
|
||||||
|
}
|
||||||
|
setUp?.();
|
||||||
|
|
||||||
|
// The extraction of the bundle would normally create this directory.
|
||||||
|
const destinationDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_CLI_VERSION,
|
||||||
|
);
|
||||||
|
const staleDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
const otherToolDirectory = createToolcacheEntry(tmpDir, "Node", "20.0.0");
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
features,
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
fs.existsSync(otherToolDirectory),
|
||||||
|
"Should never delete other tools from the toolcache.",
|
||||||
|
);
|
||||||
|
|
||||||
|
check({ cleanupDiagnostic, destinationDirectory, staleDirectory });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not clean up the toolcache when the feature flag is disabled",
|
||||||
|
async (t) => {
|
||||||
|
await testToolcacheCleanup(
|
||||||
|
t,
|
||||||
|
{ features: [], runnerEnvironment: "github-hosted" },
|
||||||
|
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.true(fs.existsSync(destinationDirectory));
|
||||||
|
t.is(cleanupDiagnostic, undefined);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not clean up the toolcache when the runner is not GitHub-hosted",
|
||||||
|
async (t) => {
|
||||||
|
await testToolcacheCleanup(
|
||||||
|
t,
|
||||||
|
{
|
||||||
|
features: [Feature.CleanupToolcacheBundles],
|
||||||
|
runnerEnvironment: "self-hosted",
|
||||||
|
},
|
||||||
|
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.true(fs.existsSync(destinationDirectory));
|
||||||
|
t.is(cleanupDiagnostic, undefined);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not clean up the toolcache when the runner environment is unknown",
|
||||||
|
async (t) => {
|
||||||
|
// A runner that doesn't report its environment must be treated as not GitHub-hosted, since its
|
||||||
|
// toolcache may well outlive the job.
|
||||||
|
await testToolcacheCleanup(
|
||||||
|
t,
|
||||||
|
{
|
||||||
|
features: [Feature.CleanupToolcacheBundles],
|
||||||
|
runnerEnvironment: undefined,
|
||||||
|
},
|
||||||
|
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.true(fs.existsSync(destinationDirectory));
|
||||||
|
t.is(cleanupDiagnostic, undefined);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL deletes other CodeQL bundles from the toolcache when enabled on a GitHub-hosted runner",
|
||||||
|
async (t) => {
|
||||||
|
await testToolcacheCleanup(
|
||||||
|
t,
|
||||||
|
{
|
||||||
|
features: [Feature.CleanupToolcacheBundles],
|
||||||
|
runnerEnvironment: "github-hosted",
|
||||||
|
},
|
||||||
|
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||||
|
t.false(
|
||||||
|
fs.existsSync(staleDirectory),
|
||||||
|
"Should delete the version directory, including the `tool-cache` marker file it contains.",
|
||||||
|
);
|
||||||
|
t.false(
|
||||||
|
fs.existsSync(path.join(destinationDirectory, os.arch(), "contents")),
|
||||||
|
"Should also delete a partial entry for the version we are about to download, rather " +
|
||||||
|
"than extracting over it.",
|
||||||
|
);
|
||||||
|
t.deepEqual(cleanupDiagnostic, {
|
||||||
|
deletedVersions: [CLEANUP_STALE_VERSION, CLEANUP_CLI_VERSION].sort(),
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL reports no deleted versions when the toolcache has no CodeQL bundles",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
// A toolcache with other tools in it, but no CodeQL.
|
||||||
|
const otherToolDirectory = createToolcacheEntry(tmpDir, "Node", "20.0.0");
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(fs.existsSync(otherToolDirectory));
|
||||||
|
t.deepEqual(cleanupDiagnostic, { deletedVersions: [], failed: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL continues when deleting a CodeQL bundle from the toolcache fails",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
createToolcacheEntry(tmpDir, "CodeQL", CLEANUP_CLI_VERSION);
|
||||||
|
const staleDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
const rmStub = sinon
|
||||||
|
.stub(fs.promises, "rm")
|
||||||
|
.rejects(new Error("EACCES: permission denied"));
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore before `withTmpDir` cleans up after itself.
|
||||||
|
rmStub.restore();
|
||||||
|
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.deepEqual(
|
||||||
|
cleanupDiagnostic,
|
||||||
|
{ deletedVersions: [], failed: true },
|
||||||
|
"Should not report versions that we failed to delete.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"deleteToolcacheBundles reports a failure when the toolcache location is unknown",
|
||||||
|
async (t) => {
|
||||||
|
const messages: LoggedMessage[] = [];
|
||||||
|
|
||||||
|
const result = await toolsDownload.deleteToolcacheBundles({
|
||||||
|
env: new ReadOnlyEnv({}),
|
||||||
|
logger: getRecordingLogger(messages),
|
||||||
|
});
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
result,
|
||||||
|
{ deletedVersions: [], failed: true },
|
||||||
|
"Should report a failure rather than throwing, so the download can continue.",
|
||||||
|
);
|
||||||
|
checkExpectedLogMessages(t, messages, [
|
||||||
|
"Unable to determine toolcache directory: RUNNER_TOOL_CACHE environment variable must be set",
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"deleteToolcacheBundles uses the supplied environment rather than process.env",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const ambientRoot = path.join(tmpDir, "ambient");
|
||||||
|
const injectedRoot = path.join(tmpDir, "injected");
|
||||||
|
setupActionsVars(tmpDir, ambientRoot);
|
||||||
|
|
||||||
|
const ambientVersion = createToolcacheEntry(
|
||||||
|
ambientRoot,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
const injectedVersion = createToolcacheEntry(
|
||||||
|
injectedRoot,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
const fileEntry = path.join(injectedRoot, "CodeQL", "not-a-directory");
|
||||||
|
fs.writeFileSync(fileEntry, "keep");
|
||||||
|
|
||||||
|
const result = await toolsDownload.deleteToolcacheBundles({
|
||||||
|
env: new ReadOnlyEnv({
|
||||||
|
[ActionsEnvVars.RUNNER_TOOL_CACHE]: injectedRoot,
|
||||||
|
}),
|
||||||
|
logger: getRunnerLogger(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
t.deepEqual(result, {
|
||||||
|
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
t.false(fs.existsSync(injectedVersion));
|
||||||
|
t.true(fs.existsSync(ambientVersion));
|
||||||
|
t.is(fs.readFileSync(fileEntry, "utf8"), "keep");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"deleteToolcacheBundles reports a failure when the toolcache directory cannot be read",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const versionDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
const messages: LoggedMessage[] = [];
|
||||||
|
const readdir = sinon
|
||||||
|
.stub(fs.promises, "readdir")
|
||||||
|
.rejects(new Error("permission denied"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await toolsDownload.deleteToolcacheBundles({
|
||||||
|
env: new ReadOnlyEnv({
|
||||||
|
[ActionsEnvVars.RUNNER_TOOL_CACHE]: tmpDir,
|
||||||
|
}),
|
||||||
|
logger: getRecordingLogger(messages),
|
||||||
|
});
|
||||||
|
|
||||||
|
t.deepEqual(result, { deletedVersions: [], failed: true });
|
||||||
|
t.true(fs.existsSync(versionDirectory));
|
||||||
|
checkExpectedLogMessages(t, messages, [
|
||||||
|
`Failed to clean up the CodeQL toolcache at '${path.join(tmpDir, "CodeQL")}': permission denied`,
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
readdir.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not follow a symlinked CodeQL toolcache directory",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const toolcacheRoot = path.join(tmpDir, "toolcache");
|
||||||
|
setupActionsVars(tmpDir, toolcacheRoot);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
// Somewhere the toolcache cleanup must never reach.
|
||||||
|
const outsideDirectory = path.join(tmpDir, "outside");
|
||||||
|
createToolcacheEntry(outsideDirectory, "CodeQL", CLEANUP_STALE_VERSION);
|
||||||
|
createToolcacheEntry(outsideDirectory, "CodeQL", CLEANUP_CLI_VERSION);
|
||||||
|
|
||||||
|
fs.mkdirSync(toolcacheRoot, { recursive: true });
|
||||||
|
fs.symlinkSync(
|
||||||
|
path.join(outsideDirectory, "CodeQL"),
|
||||||
|
path.join(toolcacheRoot, "CodeQL"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
toolcacheRoot,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
fs.existsSync(
|
||||||
|
path.join(outsideDirectory, "CodeQL", CLEANUP_STALE_VERSION),
|
||||||
|
),
|
||||||
|
"Should not delete anything through a symlinked CodeQL directory.",
|
||||||
|
);
|
||||||
|
t.deepEqual(cleanupDiagnostic, { deletedVersions: [], failed: true });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not clean up the toolcache once a step has already set up CodeQL",
|
||||||
|
async (t) => {
|
||||||
|
// `.github/workflows/codeql.yml` sets up CodeQL twice and then runs both returned paths. If the
|
||||||
|
// second setup downloads, it must not delete the bundle the first one handed out.
|
||||||
|
await testToolcacheCleanup(
|
||||||
|
t,
|
||||||
|
{
|
||||||
|
features: [Feature.CleanupToolcacheBundles],
|
||||||
|
runnerEnvironment: "github-hosted",
|
||||||
|
setUp: () => {
|
||||||
|
process.env[EnvVar.HAS_SET_UP_CODEQL] = "true";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.true(fs.existsSync(destinationDirectory));
|
||||||
|
t.is(cleanupDiagnostic, undefined);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"setupCodeQLBundle records that this job has set up CodeQL",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
|
||||||
|
|
||||||
|
sinon.stub(setupCodeql, "downloadCodeQL").resolves({
|
||||||
|
codeqlFolder: "codeql",
|
||||||
|
statusReport: { totalDurationMs: 1 },
|
||||||
|
toolsVersion: LINKED_CLI_VERSION.cliVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
await setupCodeql.setupCodeQLBundle(
|
||||||
|
"linked",
|
||||||
|
SAMPLE_DOTCOM_API_DETAILS,
|
||||||
|
tmpDir,
|
||||||
|
GitHubVariant.DOTCOM,
|
||||||
|
SAMPLE_DEFAULT_CLI_VERSION,
|
||||||
|
undefined, // rawLanguages
|
||||||
|
false, // useOverlayAwareDefaultCliVersion
|
||||||
|
createFeatures([]),
|
||||||
|
getRunnerLogger(true),
|
||||||
|
);
|
||||||
|
|
||||||
|
t.is(
|
||||||
|
process.env[EnvVar.HAS_SET_UP_CODEQL],
|
||||||
|
"true",
|
||||||
|
"A later step must be able to tell that the toolcache is in use.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL cleans up the toolcache even when the download will not be cached",
|
||||||
|
async (t) => {
|
||||||
|
// A `tools` URL we can't derive a bundle version from is extracted to a temporary directory
|
||||||
|
// rather than the toolcache, but the toolcache is on the same filesystem, so emptying it still
|
||||||
|
// frees up space for the analysis.
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
const staleDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { codeqlFolder, cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
undefined, // bundleVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
t.is(path.dirname(codeqlFolder), tmpDir);
|
||||||
|
t.not(codeqlFolder, path.join(tmpDir, "CodeQL"));
|
||||||
|
t.true(fs.existsSync(codeqlFolder));
|
||||||
|
t.false(fs.existsSync(`${codeqlFolder}.complete`));
|
||||||
|
t.false(fs.existsSync(staleDirectory));
|
||||||
|
t.deepEqual(cleanupDiagnostic, {
|
||||||
|
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL reports a failure when the toolcache cannot be inspected",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
createToolcacheEntry(tmpDir, "CodeQL", CLEANUP_STALE_VERSION);
|
||||||
|
|
||||||
|
const lstatStub = sinon.stub(fs.promises, "lstat").rejects(
|
||||||
|
Object.assign(new Error("permission denied"), {
|
||||||
|
code: "EACCES",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
lstatStub.restore();
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
cleanupDiagnostic,
|
||||||
|
{ deletedVersions: [], failed: true },
|
||||||
|
"An error other than the toolcache being absent must not be reported as success.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not clean up a toolcache on a different filesystem to the workspace",
|
||||||
|
async (t) => {
|
||||||
|
// Some runner images keep the toolcache on a different volume to the workspace, in which case
|
||||||
|
// deleting the tools frees up disk space that the analysis cannot use.
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
const staleDirectory = createToolcacheEntry(
|
||||||
|
tmpDir,
|
||||||
|
"CodeQL",
|
||||||
|
CLEANUP_STALE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
sinon
|
||||||
|
.stub(toolsDownload, "isToolcacheOnWorkspaceFilesystem")
|
||||||
|
.returns(false);
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
tmpDir,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(fs.existsSync(staleDirectory));
|
||||||
|
t.is(cleanupDiagnostic, undefined);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"isToolcacheOnWorkspaceFilesystem assumes a different filesystem when it cannot tell",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const logger = getRunnerLogger(true);
|
||||||
|
|
||||||
|
setupActionsVars(tmpDir, tmpDir);
|
||||||
|
t.true(toolsDownload.isToolcacheOnWorkspaceFilesystem(logger));
|
||||||
|
|
||||||
|
// If we can't tell, we assume the toolcache is not somewhere we can reclaim space from.
|
||||||
|
process.env[ActionsEnvVars.RUNNER_TOOL_CACHE] = path.join(
|
||||||
|
tmpDir,
|
||||||
|
"does-not-exist",
|
||||||
|
);
|
||||||
|
t.false(toolsDownload.isToolcacheOnWorkspaceFilesystem(logger));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test.serial(
|
||||||
|
"downloadCodeQL does not delete through a symlinked version directory",
|
||||||
|
async (t) => {
|
||||||
|
await withTmpDir(async (tmpDir) => {
|
||||||
|
const toolcacheRoot = path.join(tmpDir, "toolcache");
|
||||||
|
setupActionsVars(tmpDir, toolcacheRoot);
|
||||||
|
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||||
|
|
||||||
|
createToolcacheEntry(toolcacheRoot, "CodeQL", CLEANUP_STALE_VERSION);
|
||||||
|
|
||||||
|
// Somewhere outside the toolcache that a version directory points at.
|
||||||
|
const outsideDirectory = path.join(tmpDir, "outside");
|
||||||
|
fs.mkdirSync(outsideDirectory, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(outsideDirectory, "contents"), "x");
|
||||||
|
fs.symlinkSync(
|
||||||
|
outsideDirectory,
|
||||||
|
path.join(toolcacheRoot, "CodeQL", "9.9.9"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||||
|
toolcacheRoot,
|
||||||
|
[Feature.CleanupToolcacheBundles],
|
||||||
|
CLEANUP_BUNDLE_VERSION,
|
||||||
|
);
|
||||||
|
|
||||||
|
t.true(
|
||||||
|
fs.existsSync(path.join(outsideDirectory, "contents")),
|
||||||
|
"Should not delete anything through a symlinked version directory.",
|
||||||
|
);
|
||||||
|
t.true(
|
||||||
|
fs
|
||||||
|
.lstatSync(path.join(toolcacheRoot, "CodeQL", "9.9.9"))
|
||||||
|
.isSymbolicLink(),
|
||||||
|
);
|
||||||
|
t.deepEqual(cleanupDiagnostic, {
|
||||||
|
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import * as fs from "fs";
|
|||||||
import { OutgoingHttpHeaders } from "http";
|
import { OutgoingHttpHeaders } from "http";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
import * as toolcache from "@actions/tool-cache";
|
import * as toolcache from "@actions/tool-cache";
|
||||||
import { default as deepEqual } from "fast-deep-equal";
|
import { default as deepEqual } from "fast-deep-equal";
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
import { v4 as uuidV4 } from "uuid";
|
import { v4 as uuidV4 } from "uuid";
|
||||||
|
|
||||||
|
import { ActionState } from "./action-common";
|
||||||
import {
|
import {
|
||||||
isAnalyzingPullRequest,
|
isAnalyzingPullRequest,
|
||||||
isDynamicWorkflow,
|
isDynamicWorkflow,
|
||||||
|
isGitHubHostedRunner,
|
||||||
isRunningLocalAction,
|
isRunningLocalAction,
|
||||||
} from "./actions-util";
|
} from "./actions-util";
|
||||||
import * as api from "./api-client";
|
import * as api from "./api-client";
|
||||||
@@ -19,6 +22,7 @@ import {
|
|||||||
makeDiagnostic,
|
makeDiagnostic,
|
||||||
makeTelemetryDiagnostic,
|
makeTelemetryDiagnostic,
|
||||||
} from "./diagnostics";
|
} from "./diagnostics";
|
||||||
|
import { EnvVar, getEnv } from "./environment";
|
||||||
import {
|
import {
|
||||||
CODEQL_VERSION_ZSTD_BUNDLE,
|
CODEQL_VERSION_ZSTD_BUNDLE,
|
||||||
CodeQLDefaultVersionInfo,
|
CodeQLDefaultVersionInfo,
|
||||||
@@ -30,8 +34,10 @@ import { Logger } from "./logging";
|
|||||||
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
||||||
import * as tar from "./tar";
|
import * as tar from "./tar";
|
||||||
import {
|
import {
|
||||||
|
deleteToolcacheBundles,
|
||||||
downloadAndExtract,
|
downloadAndExtract,
|
||||||
getToolcacheDirectory,
|
getToolcacheDirectory,
|
||||||
|
isToolcacheOnWorkspaceFilesystem,
|
||||||
ToolsDownloadStatusReport,
|
ToolsDownloadStatusReport,
|
||||||
writeToolcacheMarkerFile,
|
writeToolcacheMarkerFile,
|
||||||
} from "./tools-download";
|
} from "./tools-download";
|
||||||
@@ -784,6 +790,7 @@ export const downloadCodeQL = async function (
|
|||||||
apiDetails: api.GitHubApiDetails,
|
apiDetails: api.GitHubApiDetails,
|
||||||
tarVersion: tar.TarVersion | undefined,
|
tarVersion: tar.TarVersion | undefined,
|
||||||
tempDir: string,
|
tempDir: string,
|
||||||
|
features: FeatureEnablement,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
codeqlFolder: string;
|
codeqlFolder: string;
|
||||||
@@ -817,6 +824,8 @@ export const downloadCodeQL = async function (
|
|||||||
const extractedBundlePath =
|
const extractedBundlePath =
|
||||||
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
|
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
|
||||||
|
|
||||||
|
await tryDeleteToolcacheBundles({ env: getEnv(), features, logger });
|
||||||
|
|
||||||
const statusReport = await downloadAndExtract(
|
const statusReport = await downloadAndExtract(
|
||||||
codeqlURL,
|
codeqlURL,
|
||||||
compressionMethod,
|
compressionMethod,
|
||||||
@@ -869,6 +878,48 @@ function getToolcacheDestinationInfo(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reclaims disk space by deleting the CodeQL tools from the toolcache, if enabled.
|
||||||
|
*
|
||||||
|
* On GitHub-hosted runners the toolcache shares a filesystem with the workspace, so tools left in
|
||||||
|
* the toolcache take up space that the analysis could use instead. This holds wherever we extract
|
||||||
|
* the tools we are obtaining, since the toolcache is on that filesystem either way.
|
||||||
|
*/
|
||||||
|
async function tryDeleteToolcacheBundles({
|
||||||
|
env,
|
||||||
|
features,
|
||||||
|
logger,
|
||||||
|
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>): Promise<void> {
|
||||||
|
// A step that has already set up CodeQL may hand out a path into the toolcache that a later step
|
||||||
|
// runs, so only the first step to set it up can know that nothing else relies on the toolcache.
|
||||||
|
if (env.getOptional(EnvVar.HAS_SET_UP_CODEQL) !== undefined) {
|
||||||
|
logger.debug(
|
||||||
|
"Not deleting the CodeQL tools from the toolcache since a previous step in this job has " +
|
||||||
|
"already set up CodeQL.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isGitHubHostedRunner() ||
|
||||||
|
!isToolcacheOnWorkspaceFilesystem(logger) ||
|
||||||
|
!(await features.getValue(Feature.CleanupToolcacheBundles))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deleteToolcacheBundles({ env, logger });
|
||||||
|
|
||||||
|
addNoLanguageDiagnostic(
|
||||||
|
undefined,
|
||||||
|
makeTelemetryDiagnostic(
|
||||||
|
"codeql-action/toolcache-bundle-cleanup",
|
||||||
|
"Toolcache CodeQL bundle cleanup",
|
||||||
|
{ ...result },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCodeQLURLVersion(url: string): string {
|
export function getCodeQLURLVersion(url: string): string {
|
||||||
const match = url.match(/\/codeql-bundle-(.*)\//);
|
const match = url.match(/\/codeql-bundle-(.*)\//);
|
||||||
if (match === null || match.length < 2) {
|
if (match === null || match.length < 2) {
|
||||||
@@ -978,6 +1029,7 @@ export async function setupCodeQLBundle(
|
|||||||
apiDetails,
|
apiDetails,
|
||||||
zstdAvailability.version,
|
zstdAvailability.version,
|
||||||
tempDir,
|
tempDir,
|
||||||
|
features,
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
toolsVersion = result.toolsVersion;
|
toolsVersion = result.toolsVersion;
|
||||||
@@ -989,6 +1041,11 @@ export async function setupCodeQLBundle(
|
|||||||
default:
|
default:
|
||||||
util.assertNever(source);
|
util.assertNever(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record that this job now has a copy of the CodeQL tools, so that a later step doesn't delete
|
||||||
|
// the toolcache out from under the path we are about to return.
|
||||||
|
core.exportVariable(EnvVar.HAS_SET_UP_CODEQL, "true");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
codeqlFolder,
|
codeqlFolder,
|
||||||
toolsDownloadStatusReport,
|
toolsDownloadStatusReport,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import * as toolcache from "@actions/tool-cache";
|
|||||||
import { https } from "follow-redirects";
|
import { https } from "follow-redirects";
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
|
|
||||||
|
import { ActionState } from "./action-common";
|
||||||
|
import { ActionsEnvVars, getEnv, ReadOnlyEnv } from "./environment";
|
||||||
import { formatDuration, Logger } from "./logging";
|
import { formatDuration, Logger } from "./logging";
|
||||||
import * as tar from "./tar";
|
import * as tar from "./tar";
|
||||||
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
|
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
|
||||||
@@ -197,16 +199,154 @@ async function downloadAndExtractZstdWithStreaming(
|
|||||||
await tar.extractTarZst(response, dest, tarVersion, logger);
|
await tar.extractTarZst(response, dest, tarVersion, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets the path to the toolcache directory that holds all versions of the CodeQL tools. */
|
||||||
|
function getToolcacheToolDirectory(env: ReadOnlyEnv): string {
|
||||||
|
return path.join(
|
||||||
|
env.getRequired(ActionsEnvVars.RUNNER_TOOL_CACHE),
|
||||||
|
TOOLCACHE_TOOL_NAME,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the name of the toolcache directory that holds the given version of the CodeQL tools. */
|
||||||
|
function getToolcacheVersionDirectoryName(version: string): string {
|
||||||
|
return semver.clean(version) || version;
|
||||||
|
}
|
||||||
|
|
||||||
/** Gets the path to the toolcache directory for the specified version of the CodeQL tools. */
|
/** Gets the path to the toolcache directory for the specified version of the CodeQL tools. */
|
||||||
export function getToolcacheDirectory(version: string): string {
|
export function getToolcacheDirectory(version: string): string {
|
||||||
return path.join(
|
return path.join(
|
||||||
getRequiredEnvParam("RUNNER_TOOL_CACHE"),
|
getToolcacheToolDirectory(getEnv()),
|
||||||
TOOLCACHE_TOOL_NAME,
|
getToolcacheVersionDirectoryName(version),
|
||||||
semver.clean(version) || version,
|
|
||||||
os.arch() || "",
|
os.arch() || "",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the toolcache is on the same filesystem as the workspace, and so whether deleting the
|
||||||
|
* tools frees up disk space that the analysis can use.
|
||||||
|
*
|
||||||
|
* These are separate volumes on some runner images. Windows runners, for example, keep the
|
||||||
|
* toolcache on `C:` while the workspace is on `D:`.
|
||||||
|
*/
|
||||||
|
export function isToolcacheOnWorkspaceFilesystem(logger: Logger): boolean {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
fs.statSync(getRequiredEnvParam("RUNNER_TOOL_CACHE")).dev ===
|
||||||
|
fs.statSync(getRequiredEnvParam("GITHUB_WORKSPACE")).dev
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(
|
||||||
|
`Could not determine whether the toolcache is on the same filesystem as the workspace: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The outcome of trying to reclaim disk space by deleting the CodeQL tools from the toolcache. */
|
||||||
|
export interface ToolcacheCleanupResult {
|
||||||
|
/** The versions of the CodeQL tools that were deleted. */
|
||||||
|
deletedVersions: string[];
|
||||||
|
/**
|
||||||
|
* Whether we hit an error while trying to delete the tools. Distinguishes a toolcache that had
|
||||||
|
* nothing to reclaim from one we failed to clean up.
|
||||||
|
*/
|
||||||
|
failed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes every version of the CodeQL tools from the toolcache.
|
||||||
|
*
|
||||||
|
* Only safe to call when we are about to download the tools, since that means we did not resolve
|
||||||
|
* them from the toolcache and so nothing in there is in use by this job.
|
||||||
|
*
|
||||||
|
* This only ever touches the CodeQL directory of the toolcache. Cleanup errors are logged and
|
||||||
|
* returned as `failed: true` rather than thrown.
|
||||||
|
*
|
||||||
|
* @returns the versions that were deleted, and whether we hit an error while trying.
|
||||||
|
*/
|
||||||
|
export async function deleteToolcacheBundles({
|
||||||
|
env,
|
||||||
|
logger,
|
||||||
|
}: ActionState<["Logger", "ReadOnlyEnv"]>): Promise<ToolcacheCleanupResult> {
|
||||||
|
let toolDirectory: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
toolDirectory = getToolcacheToolDirectory(env);
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(
|
||||||
|
`Unable to determine toolcache directory: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
return { deletedVersions: [], failed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Refuse to follow a symlinked CodeQL directory, so that we can only ever delete paths that are
|
||||||
|
// really inside the toolcache.
|
||||||
|
if ((await fs.promises.lstat(toolDirectory)).isSymbolicLink()) {
|
||||||
|
logger.info(
|
||||||
|
`Not deleting the CodeQL tools from the toolcache since '${toolDirectory}' is a symlink.`,
|
||||||
|
);
|
||||||
|
return { deletedVersions: [], failed: true };
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.code === "ENOENT") {
|
||||||
|
logger.debug(
|
||||||
|
`There are no CodeQL tools at '${toolDirectory}' to delete from the toolcache.`,
|
||||||
|
);
|
||||||
|
return { deletedVersions: [], failed: false };
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`Failed to inspect the CodeQL tools at '${toolDirectory}': ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
return { deletedVersions: [], failed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(toolDirectory, {
|
||||||
|
withFileTypes: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedVersions: string[] = [];
|
||||||
|
let failed = false;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
// `isDirectory` is false for a symlink, so we never delete a version directory that is
|
||||||
|
// really somewhere else.
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
logger.debug(
|
||||||
|
`Not deleting '${entry.name}' from the CodeQL toolcache since it is not a directory.`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionDirectory = path.join(toolDirectory, entry.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(versionDirectory, {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
deletedVersions.push(entry.name);
|
||||||
|
logger.info(
|
||||||
|
`Deleted the CodeQL tools at '${versionDirectory}' from the toolcache to free up disk space.`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
failed = true;
|
||||||
|
logger.info(
|
||||||
|
`Failed to delete the CodeQL tools at '${versionDirectory}' from the toolcache: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deletedVersions: deletedVersions.sort(), failed };
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(
|
||||||
|
`Failed to clean up the CodeQL toolcache at '${toolDirectory}': ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
return { deletedVersions: [], failed: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function writeToolcacheMarkerFile(
|
export function writeToolcacheMarkerFile(
|
||||||
extractedPath: string,
|
extractedPath: string,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
|||||||
@@ -842,9 +842,13 @@ export async function checkForTimeout() {
|
|||||||
* directory with the name hostedtoolcache which is present on
|
* directory with the name hostedtoolcache which is present on
|
||||||
* GitHub-hosted runners.
|
* GitHub-hosted runners.
|
||||||
*
|
*
|
||||||
* @returns true iff the runner is hosted by GitHub
|
* Since this is a heuristic over how the runner happens to be named and laid out, it also matches
|
||||||
|
* self-hosted runners that are configured to resemble hosted ones. Prefer
|
||||||
|
* `isGitHubHostedRunner` when you need the answer the Actions service reports.
|
||||||
|
*
|
||||||
|
* @returns true iff the runner looks like it is hosted by GitHub
|
||||||
*/
|
*/
|
||||||
export function isHostedRunner() {
|
export function looksLikeHostedRunner() {
|
||||||
return (
|
return (
|
||||||
// Name of the runner on hosted Windows runners
|
// Name of the runner on hosted Windows runners
|
||||||
process.env["RUNNER_NAME"]?.includes("Hosted Agent") ||
|
process.env["RUNNER_NAME"]?.includes("Hosted Agent") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user