mirror of
https://github.com/github/codeql-action.git
synced 2026-10-13 06:11:23 +00:00
Compare commits
27 Commits
default-se
...
mbg/macos2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9e004d42a | ||
|
|
1bab076d1f | ||
|
|
99284ab495 | ||
|
|
62c624b6d1 | ||
|
|
eb924ecb54 | ||
|
|
8ec9f29c0e | ||
|
|
8a7fad3ea4 | ||
|
|
511ac68353 | ||
|
|
8f7891e3ee | ||
|
|
7db9c3094b | ||
|
|
eaca28258d | ||
|
|
f8e112ae90 | ||
|
|
9eeab46818 | ||
|
|
e0fedcf877 | ||
|
|
bbeb45dd7a | ||
|
|
50b827040e | ||
|
|
23835d3cbe | ||
|
|
ac8654a8f2 | ||
|
|
ecfc96e0a9 | ||
|
|
409be01136 | ||
|
|
f4a15a13fd | ||
|
|
4fdbb71b54 | ||
|
|
5136039854 | ||
|
|
19445ddfed | ||
|
|
317f264e1f | ||
|
|
309f6af89b | ||
|
|
cf8f7b9787 |
86
.github/workflows/__diagnostics-without-db.yml
generated
vendored
Normal file
86
.github/workflows/__diagnostics-without-db.yml
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
# Warning: This file is generated automatically, and should not be modified.
|
||||
# Instead, please modify the template in the pr-checks directory and run:
|
||||
# pr-checks/sync.sh
|
||||
# to regenerate this file.
|
||||
|
||||
name: PR Check - Diagnostic export without database
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GO111MODULE: auto
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/v*
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
workflow_call:
|
||||
inputs: {}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: diagnostics-without-db-${{github.ref}}
|
||||
jobs:
|
||||
diagnostics-without-db:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
version: linked
|
||||
name: Diagnostic export without database
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
# We deliberately need to make this step fail before the database is initialised, but after
|
||||
# some diagnostic has been produced. The simplest way to do that right now is to force the
|
||||
# use of a nightly release and then fail because we are attempting to analyse Swift on
|
||||
# a non-macOS runner.
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
id: prepare-test
|
||||
uses: ./.github/actions/prepare-test
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Initialise failing analysis
|
||||
id: init
|
||||
uses: ./../action/init
|
||||
continue-on-error: true
|
||||
env:
|
||||
CODEQL_ACTION_FORCE_NIGHTLY: true
|
||||
with:
|
||||
languages: swift
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
|
||||
- name: Check diagnostics appear on disk
|
||||
env:
|
||||
SARIF_PATH: '${{ runner.temp }}/codeql-action-temp-diagnostics/'
|
||||
run: |
|
||||
cd ../action
|
||||
npx tsx ./pr-checks/diagnostics-without-db.ts
|
||||
env:
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
1504
lib/entry-points.js
generated
1504
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
27
pr-checks/checks/diagnostics-without-db.yml
Normal file
27
pr-checks/checks/diagnostics-without-db.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: "Diagnostic export without database"
|
||||
description: "Tests that temporary diagnostics are correctly included in the failed SARIF."
|
||||
versions:
|
||||
# Overriden by the FF below.
|
||||
- linked
|
||||
installNode: true
|
||||
steps:
|
||||
# We deliberately need to make this step fail before the database is initialised, but after
|
||||
# some diagnostic has been produced. The simplest way to do that right now is to force the
|
||||
# use of a nightly release and then fail because we are attempting to analyse Swift on
|
||||
# a non-macOS runner.
|
||||
- name: Initialise failing analysis
|
||||
id: init
|
||||
uses: ./../action/init
|
||||
continue-on-error: true
|
||||
env:
|
||||
CODEQL_ACTION_FORCE_NIGHTLY: true
|
||||
with:
|
||||
languages: swift
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
|
||||
- name: Check diagnostics appear on disk
|
||||
env:
|
||||
SARIF_PATH: "${{ runner.temp }}/codeql-action-temp-diagnostics/"
|
||||
run: |
|
||||
cd ../action
|
||||
npx tsx ./pr-checks/diagnostics-without-db.ts
|
||||
48
pr-checks/diagnostics-without-db.ts
Normal file
48
pr-checks/diagnostics-without-db.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import type * as diagnostics from "../src/diagnostics";
|
||||
|
||||
const DIAGNOSTIC_ID = "codeql-action/forced-nightly-cli";
|
||||
const sarifDirectory = process.env["SARIF_PATH"];
|
||||
|
||||
if (sarifDirectory === undefined) {
|
||||
throw new Error("SARIF_PATH is undefined.");
|
||||
}
|
||||
|
||||
const paths = fs
|
||||
.readdirSync(sarifDirectory, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.filter((p) => p.endsWith(".json"));
|
||||
|
||||
core.info(`Found ${paths.length} paths in '${sarifDirectory}'.`);
|
||||
core.info(`Searching for a diagnostic with id '${DIAGNOSTIC_ID}'...`);
|
||||
|
||||
let found = false;
|
||||
for (const diagnosticPath of paths) {
|
||||
if (path.extname(diagnosticPath) !== ".json") {
|
||||
core.info(`Skipping ${diagnosticPath}.`);
|
||||
continue;
|
||||
}
|
||||
core.info(`Found ${diagnosticPath}.`);
|
||||
|
||||
const contents = JSON.parse(
|
||||
fs.readFileSync(path.resolve(sarifDirectory, diagnosticPath), "utf-8"),
|
||||
) as diagnostics.DiagnosticMessage;
|
||||
|
||||
if (contents.source.id === DIAGNOSTIC_ID) {
|
||||
core.info(
|
||||
`Found '${DIAGNOSTIC_ID}' in '${diagnosticPath}'. Skipping remaining files.`,
|
||||
);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw new Error(`Didn't find '${DIAGNOSTIC_ID}' diagnostic.`);
|
||||
}
|
||||
@@ -19,6 +19,24 @@ import { getErrorMessage } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
for (const [key, displayName] of Object.entries(
|
||||
statusReport.ACTION_DISPLAY_NAMES,
|
||||
)) {
|
||||
test(`getDisplayActionName - '${displayName}' instead of '${key}'`, (t) => {
|
||||
t.is(statusReport.getDisplayActionName(key as ActionName), displayName);
|
||||
});
|
||||
}
|
||||
|
||||
test("getDisplayActionName - returns action name", (t) => {
|
||||
for (const actionName of Object.values(ActionName)) {
|
||||
if (statusReport.ACTION_DISPLAY_NAMES[actionName]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
t.is(statusReport.getDisplayActionName(actionName), actionName);
|
||||
}
|
||||
});
|
||||
|
||||
interface RunInActionsTestOpts {
|
||||
runFn?: () => Promise<any>;
|
||||
expectedErrorMessage?: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as os from "os";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ActionsEnv, getActionsEnv } from "./actions-util";
|
||||
@@ -13,6 +15,8 @@ import {
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage, wrapError } from "./util";
|
||||
|
||||
export type { Logger } from "./logging";
|
||||
|
||||
/** Base state that is available to an Action on startup. */
|
||||
export interface BaseState {
|
||||
/** The name of the Action. */
|
||||
@@ -23,6 +27,8 @@ export interface BaseState {
|
||||
platform: NodeJS.Platform;
|
||||
/** The architecture of the host. */
|
||||
arch: NodeJS.Architecture;
|
||||
/** The version of the operating system. */
|
||||
osRelease: string;
|
||||
}
|
||||
|
||||
/** Describes different state features that an Action may have. */
|
||||
@@ -173,6 +179,7 @@ export async function runInActions(action: Action) {
|
||||
startedAt,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
osRelease: os.release(),
|
||||
logger,
|
||||
env,
|
||||
actions: actionsEnv,
|
||||
|
||||
@@ -7,12 +7,13 @@ import {
|
||||
isAnalyzingPullRequest,
|
||||
isDefaultSetup,
|
||||
isDynamicWorkflow,
|
||||
printDebugLogs,
|
||||
} from "./actions-util";
|
||||
import { computeAutomationID } from "./api-client";
|
||||
import { EnvVar } from "./environment";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { initializeEnvironment } from "./util";
|
||||
import { initializeEnvironment, withTmpDir } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
@@ -100,6 +101,19 @@ test("computeAutomationID()", async (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("printDebugLogs - doesn't throw when config doesn't have needed properties", async (t) => {
|
||||
await t.notThrowsAsync(printDebugLogs({}));
|
||||
await t.notThrowsAsync(printDebugLogs({ languages: [] }));
|
||||
await t.notThrowsAsync(printDebugLogs({ dbLocation: "nowhere" }));
|
||||
});
|
||||
|
||||
test("printDebugLogs - doesn't throw when language db doesn't exist", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
await t.notThrowsAsync(
|
||||
printDebugLogs({ dbLocation: tmpDir, languages: ["javascript"] }),
|
||||
);
|
||||
}));
|
||||
|
||||
test.serial("getPullRequestBranches() with pull request context", (t) => {
|
||||
withMockedContext(
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
|
||||
import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
|
||||
import type { Config } from "./config-utils";
|
||||
import { hasConfigKey, type Config } from "./config/action-config";
|
||||
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
@@ -72,7 +72,7 @@ export const getOptionalInput = function (name: string): string | undefined {
|
||||
* directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the
|
||||
* value of `RUNNER_TEMP` otherwise.
|
||||
*/
|
||||
export function getTemporaryDirectory(env: Env = getEnv()): string {
|
||||
export function getTemporaryDirectory(env: ReadOnlyEnv = getEnv()): string {
|
||||
return (
|
||||
env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP)
|
||||
);
|
||||
@@ -131,7 +131,14 @@ export function getWorkflowEvent(env: Env = getEnv()): any {
|
||||
}
|
||||
}
|
||||
|
||||
export async function printDebugLogs(config: Config) {
|
||||
export async function printDebugLogs(config: Partial<Config>) {
|
||||
if (
|
||||
!hasConfigKey(config, "languages") ||
|
||||
!hasConfigKey(config, "dbLocation")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const language of config.languages) {
|
||||
const databaseDirectory = getCodeQLDatabasePath(config, language);
|
||||
const logsDirectory = path.join(databaseDirectory, "log");
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { setupCppAutobuild } from "./autobuild";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import type { ActionConfig } from "./config/action-config";
|
||||
import { defaultSuites } from "./config/db-config";
|
||||
import * as configUtils from "./config-utils";
|
||||
import {
|
||||
@@ -171,7 +172,7 @@ async function shouldExtractLanguage(
|
||||
}
|
||||
|
||||
export function dbIsFinalized(
|
||||
config: configUtils.Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
logger: Logger,
|
||||
) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import { ReadOnlyEnv } from "../environment";
|
||||
import * as json from "../json";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
@@ -51,7 +51,7 @@ export function resetCachedCodeQlVersion(): void {
|
||||
* Returns the path to the temporary file that backs the
|
||||
* on-disk cache of CLI responses between workflow steps.
|
||||
*/
|
||||
export function getCommandCacheFilePath(env: Env): string {
|
||||
export function getCommandCacheFilePath(env: ReadOnlyEnv): string {
|
||||
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ import * as api from "./api-client";
|
||||
import * as outputCache from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import { CliError, wrapCliConfigurationError } from "./cli-errors";
|
||||
import type { ActionConfig } from "./config/action-config";
|
||||
import { appendExtraQueryExclusions, type Config } from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar, getEnv, exportEnvVar } from "./environment";
|
||||
import { EnvVar, getEnv, exportEnvVar, Env, ReadOnlyEnv } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -197,7 +198,8 @@ export interface CodeQL {
|
||||
diagnosticsExport(
|
||||
sarifFile: string,
|
||||
automationDetailsId: string | undefined,
|
||||
config: Config,
|
||||
config: ActionConfig<"tempDir">,
|
||||
diagnosticDir: string | undefined,
|
||||
): Promise<void>;
|
||||
/** Get the location of an extractor for the specified language. */
|
||||
resolveExtractor(language: Language): Promise<string>;
|
||||
@@ -493,8 +495,9 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||
export async function getCodeQLForTesting(
|
||||
cmd = "codeql-for-testing",
|
||||
logger: Logger = getRunnerLogger(true),
|
||||
env: Env = getEnv(),
|
||||
): Promise<CodeQL> {
|
||||
return getCodeQLForCmd(logger, cmd, false);
|
||||
return getCodeQLForCmd(logger, cmd, false, env);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,13 +512,14 @@ async function getCodeQLForCmd(
|
||||
logger: Logger,
|
||||
cmd: string,
|
||||
checkVersion: boolean,
|
||||
env: Env = getEnv(),
|
||||
): Promise<CodeQL> {
|
||||
const codeql: CodeQL = {
|
||||
getPath() {
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
|
||||
const cacheFilePath = outputCache.getCommandCacheFilePath(env);
|
||||
let result = outputCache.getCachedCodeQlVersion(
|
||||
logger,
|
||||
cacheFilePath,
|
||||
@@ -641,7 +645,7 @@ async function getCodeQLForCmd(
|
||||
}
|
||||
},
|
||||
async runAutobuild(config: Config, language: Language) {
|
||||
applyAutobuildAzurePipelinesTimeoutFix();
|
||||
applyAutobuildAzurePipelinesTimeoutFix(env);
|
||||
|
||||
const autobuildCmd = path.join(
|
||||
await this.resolveExtractor(language),
|
||||
@@ -651,8 +655,11 @@ async function getCodeQLForCmd(
|
||||
|
||||
// Bump the verbosity of the autobuild command if we're in debug mode
|
||||
if (config.debugMode) {
|
||||
process.env[EnvVar.CLI_VERBOSITY] =
|
||||
process.env[EnvVar.CLI_VERBOSITY] || EXTRACTION_DEBUG_MODE_VERBOSITY;
|
||||
env.set(
|
||||
EnvVar.CLI_VERBOSITY,
|
||||
env.getOptional(EnvVar.CLI_VERBOSITY) ??
|
||||
EXTRACTION_DEBUG_MODE_VERBOSITY,
|
||||
);
|
||||
}
|
||||
|
||||
// On macOS, System Integrity Protection (SIP) typically interferes with
|
||||
@@ -684,7 +691,7 @@ async function getCodeQLForCmd(
|
||||
},
|
||||
async extractUsingBuildMode(config: Config, language: Language) {
|
||||
if (config.buildMode === BuildMode.Autobuild) {
|
||||
applyAutobuildAzurePipelinesTimeoutFix();
|
||||
applyAutobuildAzurePipelinesTimeoutFix(env);
|
||||
}
|
||||
try {
|
||||
await runCli(cmd, [
|
||||
@@ -816,7 +823,7 @@ async function getCodeQLForCmd(
|
||||
"--sarif-group-rules-by-pack",
|
||||
"--sarif-include-query-help=always",
|
||||
"--sublanguage-file-coverage",
|
||||
...(await getJobRunUuidSarifOptions()),
|
||||
...(await getJobRunUuidSarifOptions(env)),
|
||||
...getExtraOptionsFromEnv(["database", "interpret-results"]),
|
||||
];
|
||||
if (sarifRunPropertyFlag !== undefined) {
|
||||
@@ -913,7 +920,8 @@ async function getCodeQLForCmd(
|
||||
async diagnosticsExport(
|
||||
sarifFile: string,
|
||||
automationDetailsId: string | undefined,
|
||||
config: Config,
|
||||
config: ActionConfig<"tempDir">,
|
||||
diagnosticDir: string | undefined,
|
||||
): Promise<void> {
|
||||
const args = [
|
||||
"diagnostics",
|
||||
@@ -923,6 +931,9 @@ async function getCodeQLForCmd(
|
||||
`--sarif-codescanning-config=${getGeneratedCodeScanningConfigPath(
|
||||
config,
|
||||
)}`,
|
||||
...(diagnosticDir !== undefined
|
||||
? [`--diagnostic-dir=${diagnosticDir}`]
|
||||
: []),
|
||||
...getExtraOptionsFromEnv(["diagnostics", "export"]),
|
||||
];
|
||||
if (automationDetailsId !== undefined) {
|
||||
@@ -1036,7 +1047,7 @@ async function getCodeQLForCmd(
|
||||
);
|
||||
} else if (
|
||||
checkVersion &&
|
||||
process.env[EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING] !== "true" &&
|
||||
env.getOptional(EnvVar.SUPPRESS_DEPRECATED_SOON_WARNING) !== "true" &&
|
||||
!(await util.codeQlVersionAtLeast(codeql, CODEQL_NEXT_MINIMUM_VERSION))
|
||||
) {
|
||||
const result = await codeql.getVersion();
|
||||
@@ -1237,7 +1248,9 @@ export async function getTrapCachingExtractorConfigArgsForLang(
|
||||
*
|
||||
* This will not exist if the configuration is being parsed in the Action.
|
||||
*/
|
||||
function getGeneratedCodeScanningConfigPath(config: Config): string {
|
||||
function getGeneratedCodeScanningConfigPath(
|
||||
config: ActionConfig<"tempDir">,
|
||||
): string {
|
||||
return path.resolve(config.tempDir, "user-config.yaml");
|
||||
}
|
||||
|
||||
@@ -1256,17 +1269,20 @@ function getExtractionVerbosityArguments(
|
||||
* Without the fix, long build processes will timeout when pulling down Java packages
|
||||
* https://developercommunity.visualstudio.com/content/problem/292284/maven-hosted-agent-connection-timeout.html
|
||||
*/
|
||||
function applyAutobuildAzurePipelinesTimeoutFix() {
|
||||
const javaToolOptions = process.env["JAVA_TOOL_OPTIONS"] || "";
|
||||
process.env["JAVA_TOOL_OPTIONS"] = [
|
||||
...javaToolOptions.split(/\s+/),
|
||||
"-Dhttp.keepAlive=false",
|
||||
"-Dmaven.wagon.http.pool=false",
|
||||
].join(" ");
|
||||
function applyAutobuildAzurePipelinesTimeoutFix(env: Env) {
|
||||
const javaToolOptions = env.getOptional("JAVA_TOOL_OPTIONS") ?? "";
|
||||
env.set(
|
||||
"JAVA_TOOL_OPTIONS",
|
||||
[
|
||||
...javaToolOptions.split(/\s+/),
|
||||
"-Dhttp.keepAlive=false",
|
||||
"-Dmaven.wagon.http.pool=false",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
async function getJobRunUuidSarifOptions() {
|
||||
const jobRunUuid = process.env[EnvVar.JOB_RUN_UUID];
|
||||
async function getJobRunUuidSarifOptions(env: ReadOnlyEnv) {
|
||||
const jobRunUuid = env.getOptional(EnvVar.JOB_RUN_UUID);
|
||||
|
||||
return jobRunUuid ? [`--sarif-run-property=jobRunUuid=${jobRunUuid}`] : [];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
import * as api from "./api-client";
|
||||
import { getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config/action-config";
|
||||
import {
|
||||
type AbortedInitConfig,
|
||||
type ActionConfig,
|
||||
type Config,
|
||||
} from "./config/action-config";
|
||||
import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
@@ -71,6 +75,7 @@ import {
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { shouldSkipOverlayAnalysis } from "./overlay/status";
|
||||
import { RepositoryNwo } from "./repository";
|
||||
import { ActionName } from "./status-report";
|
||||
import { ToolsFeature } from "./tools-features";
|
||||
import { downloadTrapCaches } from "./trap-caching";
|
||||
import {
|
||||
@@ -1379,31 +1384,84 @@ export function getPathToParsedConfigFile(tempDir: string): string {
|
||||
return path.join(tempDir, "config");
|
||||
}
|
||||
|
||||
function getPathToFailedConfigFile(tempDir: string): string {
|
||||
return path.join(tempDir, "failed-config");
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the given config to the path returned from getPathToParsedConfigFile.
|
||||
* Writes {@link config} to {@link configFilePath}.
|
||||
*
|
||||
* This function is not intended to be called, other than by
|
||||
* {@link saveConfig} and {@link saveFailedConfig} below.
|
||||
*/
|
||||
export async function saveConfig(config: Config, logger: Logger) {
|
||||
function saveConfigFile(
|
||||
logger: Logger,
|
||||
configFilePath: string,
|
||||
config: Partial<Config>,
|
||||
) {
|
||||
const configString = JSON.stringify(config);
|
||||
const configFile = getPathToParsedConfigFile(config.tempDir);
|
||||
fs.mkdirSync(path.dirname(configFile), { recursive: true });
|
||||
fs.writeFileSync(configFile, configString, "utf8");
|
||||
fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
|
||||
fs.writeFileSync(configFilePath, configString, "utf8");
|
||||
logger.debug("Saved config:");
|
||||
logger.debug(configString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config that has been saved to the given temp dir.
|
||||
* If the config could not be found then returns undefined.
|
||||
* Store the given config to the path returned from getPathToParsedConfigFile.
|
||||
*/
|
||||
export async function getConfig(
|
||||
tempDir: string,
|
||||
export async function saveConfig(config: Config, logger: Logger) {
|
||||
const configFile = getPathToParsedConfigFile(config.tempDir);
|
||||
saveConfigFile(logger, configFile, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store parts of {@link config} as a failed configuration file.
|
||||
*
|
||||
* The file will only contain parts of {@link config} that are relevant to
|
||||
* the `init-post` step in the workflow.
|
||||
*/
|
||||
export function saveAbortedInitConfig(
|
||||
action: ActionState<["Base", "Logger"]>,
|
||||
config: Config,
|
||||
) {
|
||||
// Should only be called by init.
|
||||
if (action.name !== ActionName.Init) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy of the input `Config` that only contains the keys we want.
|
||||
const failedConfig: AbortedInitConfig = {
|
||||
version: config.version,
|
||||
tempDir: config.tempDir,
|
||||
codeQLCmd: config.codeQLCmd,
|
||||
debugMode: config.debugMode,
|
||||
gitHubVersion: config.gitHubVersion,
|
||||
languages: config.languages,
|
||||
analysisKinds: config.analysisKinds,
|
||||
};
|
||||
|
||||
action.logger.debug(`Saving config for failed init step.`);
|
||||
saveConfigFile(
|
||||
action.logger,
|
||||
getPathToFailedConfigFile(config.tempDir),
|
||||
failedConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read a {@link Config} from {@link configFilePath}.
|
||||
*
|
||||
* This function is not intended to be called, other than by
|
||||
* {@link getConfig} and {@link getAbortedInitConfig} below.
|
||||
*/
|
||||
function readConfigFile(
|
||||
logger: Logger,
|
||||
): Promise<Config | undefined> {
|
||||
const configFile = getPathToParsedConfigFile(tempDir);
|
||||
if (!fs.existsSync(configFile)) {
|
||||
configFilePath: string,
|
||||
): Config | undefined {
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
const configString = fs.readFileSync(configFile, "utf8");
|
||||
const configString = fs.readFileSync(configFilePath, "utf8");
|
||||
logger.debug("Loaded config:");
|
||||
logger.debug(configString);
|
||||
|
||||
@@ -1428,6 +1486,36 @@ export async function getConfig(
|
||||
return config as Config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config that has been saved to the given temp dir.
|
||||
* If the config could not be found then returns undefined.
|
||||
*/
|
||||
export async function getConfig(
|
||||
tempDir: string,
|
||||
logger: Logger,
|
||||
): Promise<Config | undefined> {
|
||||
const configFile = getPathToParsedConfigFile(tempDir);
|
||||
return readConfigFile(logger, configFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a {@link AbortedInitConfig} from a failed `init` step if it exists.
|
||||
*
|
||||
* The result will only contain parts of a {@link Config} that are relevant to
|
||||
* the `init-post` step in the workflow.
|
||||
*/
|
||||
export function getAbortedInitConfig(
|
||||
action: ActionState<["Base", "Logger"]>,
|
||||
tempDir: string,
|
||||
): AbortedInitConfig | undefined {
|
||||
// Only intended to be called by the `init-post` action.
|
||||
if (action.name !== ActionName.InitPost) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return readConfigFile(action.logger, getPathToFailedConfigFile(tempDir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a `qlconfig.yml` file from the `registries` input.
|
||||
* This file is used by the CodeQL CLI to list the registries to use for each
|
||||
@@ -1618,21 +1706,27 @@ export function appendExtraQueryExclusions(
|
||||
/**
|
||||
* Returns `true` if Code Scanning analysis is enabled, or `false` if not.
|
||||
*/
|
||||
export function isCodeScanningEnabled(config: Config): boolean {
|
||||
export function isCodeScanningEnabled(
|
||||
config: ActionConfig<"analysisKinds">,
|
||||
): boolean {
|
||||
return config.analysisKinds.includes(AnalysisKind.CodeScanning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if Code Quality analysis is enabled, or `false` if not.
|
||||
*/
|
||||
export function isCodeQualityEnabled(config: Config): boolean {
|
||||
export function isCodeQualityEnabled(
|
||||
config: ActionConfig<"analysisKinds">,
|
||||
): boolean {
|
||||
return config.analysisKinds.includes(AnalysisKind.CodeQuality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if Code Scanning Risk Assessment analysis is enabled, or `false` if not.
|
||||
*/
|
||||
export function isRiskAssessmentEnabled(config: Config): boolean {
|
||||
export function isRiskAssessmentEnabled(
|
||||
config: ActionConfig<"analysisKinds">,
|
||||
): boolean {
|
||||
return config.analysisKinds.includes(AnalysisKind.RiskAssessment);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,3 +126,38 @@ export interface Config {
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
|
||||
/** Properties available in `Config`. */
|
||||
export type ActionConfigProperties = keyof Config;
|
||||
|
||||
/**
|
||||
* The CodeQL Action configuration state that is persisted
|
||||
* between steps of the CodeQL Action in a CodeQL workflow.
|
||||
*
|
||||
* By default, this is equivalent to `Config`. By explicitly
|
||||
* providing required property names for `T` this can be restricted
|
||||
* to requiring only those. For example, in `ActionConfig<"codeqlCmd">`
|
||||
* all properties except `codeqlCmd` are optional.
|
||||
*/
|
||||
export type ActionConfig<
|
||||
T extends ActionConfigProperties = ActionConfigProperties,
|
||||
> = Partial<Config> & Pick<Config, T>;
|
||||
|
||||
/** Helper function to check whether `config` has `key` to narrow the type of `config`. */
|
||||
export function hasConfigKey<Key extends ActionConfigProperties>(
|
||||
config: Partial<Config>,
|
||||
key: Key,
|
||||
): config is ActionConfig<Key> {
|
||||
return config[key] !== undefined;
|
||||
}
|
||||
|
||||
/** A minimal configuration that we can make available even when `init` aborts. */
|
||||
export type AbortedInitConfig = ActionConfig<
|
||||
| "version"
|
||||
| "tempDir"
|
||||
| "gitHubVersion"
|
||||
| "debugMode"
|
||||
| "languages"
|
||||
| "analysisKinds"
|
||||
| "codeQLCmd"
|
||||
>;
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
LoggedMessage,
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
setupActionsVars,
|
||||
} from "../testing-utils";
|
||||
import { ConfigurationError, prettyPrintPack } from "../util";
|
||||
import { ConfigurationError, prettyPrintPack, withTmpDir } from "../util";
|
||||
|
||||
import * as dbConfig from "./db-config";
|
||||
|
||||
@@ -562,69 +563,84 @@ test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplie
|
||||
t.deepEqual(result, configFile);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
test.serial(
|
||||
"mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
borg: [],
|
||||
org: {
|
||||
unknown: "foo",
|
||||
"model-packs": [],
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
borg: [],
|
||||
org: {
|
||||
unknown: "foo",
|
||||
"model-packs": [],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
"paths-ignore": ["other-path"],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
"paths-ignore": ["other-path"],
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [] } },
|
||||
});
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [] } },
|
||||
});
|
||||
|
||||
const expectedUnrecognisedKeys = [
|
||||
".default-setup.org.unknown",
|
||||
".default-setup.borg",
|
||||
".paths-ignore",
|
||||
].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
|
||||
]);
|
||||
});
|
||||
const expectedUnrecognisedKeys = [
|
||||
".default-setup.org.unknown",
|
||||
".default-setup.borg",
|
||||
".paths-ignore",
|
||||
].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {};
|
||||
test.serial(
|
||||
"mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": [123],
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": [123],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [123] } },
|
||||
});
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [123] } },
|
||||
});
|
||||
|
||||
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
|
||||
]);
|
||||
});
|
||||
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(
|
||||
", ",
|
||||
);
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/** Parses `contents` as a configuration without validating it. */
|
||||
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getOptionalInput, getTemporaryDirectory } from "./actions-util";
|
||||
import { dbIsFinalized } from "./analyze";
|
||||
import { scanArtifactsForTokens } from "./artifact-scanner";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import type { ActionConfig } from "./config/action-config";
|
||||
import { EnvVar, exportEnvVar } from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
@@ -89,7 +89,7 @@ export async function uploadCombinedSarifArtifacts(
|
||||
* @return The path to that debug artifact, or undefined if an error occurs.
|
||||
*/
|
||||
function tryPrepareSarifDebugArtifact(
|
||||
config: Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
logger: Logger,
|
||||
): string | undefined {
|
||||
@@ -131,7 +131,7 @@ function tryPrepareSarifDebugArtifact(
|
||||
*/
|
||||
async function tryBundleDatabase(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
logger: Logger,
|
||||
): Promise<string | undefined> {
|
||||
@@ -157,6 +157,11 @@ async function tryBundleDatabase(
|
||||
}
|
||||
}
|
||||
|
||||
/** The minimal configuration needed for `tryUploadAllAvailableDebugArtifacts`. */
|
||||
export type UploadDebugArtifactsConfig = ActionConfig<
|
||||
"languages" | "gitHubVersion" | "dbLocation" | "debugArtifactName"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Attempt to upload all available debug artifacts.
|
||||
*
|
||||
@@ -164,7 +169,7 @@ async function tryBundleDatabase(
|
||||
*/
|
||||
export async function tryUploadAllAvailableDebugArtifacts(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: UploadDebugArtifactsConfig,
|
||||
logger: Logger,
|
||||
) {
|
||||
const filesToUpload: string[] = [];
|
||||
@@ -353,7 +358,7 @@ export async function getArtifactUploaderClient(
|
||||
* all files in the database folder and return the path.
|
||||
*/
|
||||
async function createPartialDatabaseBundle(
|
||||
config: Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
): Promise<string> {
|
||||
const databasePath = getCodeQLDatabasePath(config, language);
|
||||
@@ -394,7 +399,7 @@ async function createPartialDatabaseBundle(
|
||||
*/
|
||||
async function createDatabaseBundleCli(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
): Promise<string> {
|
||||
const databaseBundlePath = await bundleDb(
|
||||
|
||||
111
src/diagnostics.test.ts
Normal file
111
src/diagnostics.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import test, { type ExecutionContext } from "ava";
|
||||
|
||||
import * as diagnostics from "./diagnostics";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import {
|
||||
checkExpectedLogMessages,
|
||||
createTestConfig,
|
||||
initAllState,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
import { withTmpDir } from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const id = "codeql-action/test-diagnostic";
|
||||
const name = "Test title";
|
||||
|
||||
test("makeDiagnostic - adds expected properties", (t) => {
|
||||
const diagnostic = diagnostics.makeDiagnostic(id, name, {});
|
||||
|
||||
t.truthy(diagnostic.timestamp);
|
||||
t.is(diagnostic.source.id, id);
|
||||
t.is(diagnostic.source.name, name);
|
||||
});
|
||||
|
||||
/**
|
||||
* Diagnostic files that are written to disk don't have deterministic names.
|
||||
* This function recursively finds all `.json` files at {@link rootPath},
|
||||
* asserts that there is exactly one, and that it matches {@link expected}.
|
||||
*
|
||||
* @param rootPath The path to start searching from.
|
||||
*/
|
||||
function assertSingleDiagnosticIn(
|
||||
t: ExecutionContext<unknown>,
|
||||
rootPath: string,
|
||||
expected: diagnostics.DiagnosticMessage,
|
||||
) {
|
||||
// Find all `.json` files in `rootPath` and check that there is exactly one.
|
||||
const filePaths = fs
|
||||
.readdirSync(rootPath, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.filter((p) => p.endsWith(".json"));
|
||||
|
||||
t.is(filePaths.length, 1);
|
||||
|
||||
// Check that the written diagnostic matches the one we added.
|
||||
const contents = fs.readFileSync(
|
||||
path.resolve(rootPath, filePaths[0]),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const readDiagnostic = JSON.parse(contents) as diagnostics.DiagnosticMessage;
|
||||
t.deepEqual(readDiagnostic, expected);
|
||||
}
|
||||
|
||||
test("addDiagnostic writes temporary diagnostics and flushDiagnostics moves them to the database", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const tempDiagDir = path.resolve(tmpDir, "temp");
|
||||
const testDbDir = path.resolve(tmpDir, "test", "database");
|
||||
const diagnostic = diagnostics.makeDiagnostic(id, name, {});
|
||||
|
||||
// Write the diagnostic. The path provided for `dbLocation` doesn't exist,
|
||||
// so the diagnostic will be written to a temporary location.
|
||||
t.notThrows(() => {
|
||||
diagnostics.addDiagnostic(
|
||||
createTestConfig({ tempDir: tempDiagDir, dbLocation: testDbDir }),
|
||||
BuiltInLanguage.actions,
|
||||
diagnostic,
|
||||
logger,
|
||||
);
|
||||
});
|
||||
|
||||
// Check that `tempDiagDir` was created.
|
||||
t.true(fs.existsSync(tempDiagDir));
|
||||
|
||||
// Check that the diagnostic exists under `tempDiagDir`.
|
||||
assertSingleDiagnosticIn(t, tempDiagDir, diagnostic);
|
||||
|
||||
// Flush the diagnostics. This will cause the diagnostic we wrote previously to
|
||||
// be moved to a location under `dbLocation`.
|
||||
t.notThrows(() => {
|
||||
diagnostics.flushDiagnostics(
|
||||
initAllState({ logger }),
|
||||
createTestConfig({
|
||||
tempDir: tempDiagDir,
|
||||
dbLocation: testDbDir,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// Check that `testDbDir` was created.
|
||||
t.true(fs.existsSync(testDbDir));
|
||||
|
||||
// Check that the diagnostic exists under `testDbDir`.
|
||||
assertSingleDiagnosticIn(t, testDbDir, diagnostic);
|
||||
|
||||
// Check that we logged that the database directory doesn't exist yet
|
||||
// and that we moved one diagnostic.
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`but the database at ${testDbDir}/actions does not exist yet`,
|
||||
"Moving 1 diagnostic(s) to their respective databases.",
|
||||
]);
|
||||
}));
|
||||
@@ -1,10 +1,12 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import type { ActionState } from "./action-common";
|
||||
import { getTemporaryDirectory } from "./actions-util";
|
||||
import type { Config } from "./config-utils";
|
||||
import { Language } from "./languages";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { getCodeQLDatabasePath } from "./util";
|
||||
import { getActionsLogger, type Logger } from "./logging";
|
||||
import { getCodeQLDatabasePath, getErrorMessage } from "./util";
|
||||
|
||||
/**
|
||||
* Known tags for diagnostics. There is currently only "internal-error",
|
||||
@@ -84,22 +86,16 @@ export type DiagnosticMessage = DiagnosticMessageOptions & {
|
||||
source: DiagnosticSource;
|
||||
};
|
||||
|
||||
/** Represents a diagnostic message that has not yet been written to the database. */
|
||||
interface UnwrittenDiagnostic {
|
||||
/** The diagnostic message that has not yet been written. */
|
||||
diagnostic: DiagnosticMessage;
|
||||
/** The language the diagnostic is for. */
|
||||
language: Language;
|
||||
/** Represents a diagnostic message that has not yet been saved to the database. */
|
||||
interface TemporaryDiagnostic {
|
||||
/** The path to which the diagnostic has temporarily been written to. */
|
||||
path: string;
|
||||
/** The language the diagnostic is for, if any. */
|
||||
language?: Language;
|
||||
}
|
||||
|
||||
/** A list of diagnostics which have not yet been written to disk. */
|
||||
let unwrittenDiagnostics: UnwrittenDiagnostic[] = [];
|
||||
|
||||
/**
|
||||
* A list of diagnostics which have not yet been written to disk,
|
||||
* and where the language does not matter.
|
||||
*/
|
||||
let unwrittenDefaultLanguageDiagnostics: DiagnosticMessage[] = [];
|
||||
/** A list of diagnostics which have not yet been saved to the database. */
|
||||
let temporaryDiagnostics: TemporaryDiagnostic[] = [];
|
||||
|
||||
/**
|
||||
* Counter used to generate a unique suffix for each diagnostic filename, so that
|
||||
@@ -128,6 +124,29 @@ export function makeDiagnostic(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a path relative to {@link tmpDir} where diagnostics for {@link language}
|
||||
* can temporarily be stored at before the database is initialised. If {@link language}
|
||||
* is `undefined`, then the common base path for all languages relative to {@link tmpDir}
|
||||
* is returned.
|
||||
*/
|
||||
export function getTempDiagnosticPath(tmpDir: string, language?: Language) {
|
||||
const root = path.join(tmpDir, "codeql-action-temp-diagnostics");
|
||||
return getDiagnosticsPath(root, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a path relative to {@link basePath} where diagnostics for {@link language}
|
||||
* should be stored at. If {@link language} is `undefined`, then {@link baseBath}
|
||||
* is returned instead.
|
||||
*/
|
||||
export function getDiagnosticsPath(basePath: string, language?: Language) {
|
||||
if (language !== undefined) {
|
||||
return path.resolve(basePath, language, "diagnostic", "codeql-action");
|
||||
}
|
||||
return basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given diagnostic to the database. If the database does not yet exist,
|
||||
* the diagnostic will be written to it once it has been created.
|
||||
@@ -140,22 +159,27 @@ export function addDiagnostic(
|
||||
config: Config,
|
||||
language: Language,
|
||||
diagnostic: DiagnosticMessage,
|
||||
logger: Logger = getActionsLogger(),
|
||||
) {
|
||||
const logger = getActionsLogger();
|
||||
const databasePath = language
|
||||
? getCodeQLDatabasePath(config, language)
|
||||
: config.dbLocation;
|
||||
|
||||
// Check that the database exists before writing to it. If the database does not yet exist,
|
||||
// store the diagnostic in memory and write it later.
|
||||
if (existsSync(databasePath)) {
|
||||
writeDiagnostic(config, language, diagnostic);
|
||||
if (fs.existsSync(databasePath)) {
|
||||
writeDiagnostic({ logger }, config, language, diagnostic);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Writing a diagnostic for ${language}, but the database at ${databasePath} does not exist yet.`,
|
||||
);
|
||||
|
||||
unwrittenDiagnostics.push({ diagnostic, language });
|
||||
// Write the diagnostic to a temporary location.
|
||||
const tempDiagnosticsPath = getTempDiagnosticPath(config.tempDir, language);
|
||||
const diagnosticPath = writeDiagnosticFile(tempDiagnosticsPath, diagnostic);
|
||||
|
||||
// Track the temporary file.
|
||||
temporaryDiagnostics.push({ path: diagnosticPath, language });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,10 +197,51 @@ export function addNoLanguageDiagnostic(
|
||||
diagnostic,
|
||||
);
|
||||
} else {
|
||||
unwrittenDefaultLanguageDiagnostics.push(diagnostic);
|
||||
const tempDiagnosticsPath = getTempDiagnosticPath(getTemporaryDirectory());
|
||||
const diagnosticPath = writeDiagnosticFile(tempDiagnosticsPath, diagnostic);
|
||||
|
||||
// Track the temporary file.
|
||||
temporaryDiagnostics.push({ path: diagnosticPath });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes {@link diagnostic} to a file in {@link diagnosticsPath}.
|
||||
*/
|
||||
function writeDiagnosticFile(
|
||||
diagnosticsPath: string,
|
||||
diagnostic: DiagnosticMessage,
|
||||
): string {
|
||||
// Create the directory if it doesn't exist yet.
|
||||
fs.mkdirSync(diagnosticsPath, { recursive: true });
|
||||
|
||||
// Include a monotonically increasing suffix to avoid filename collisions
|
||||
// between diagnostics produced within the same millisecond.
|
||||
const uniqueSuffix = (diagnosticCounter++).toString();
|
||||
// We should only need to remove colons, but to be defensive, only allow a restricted set of
|
||||
// characters.
|
||||
const sanitizedTimestamp = diagnostic.timestamp.replace(
|
||||
/[^a-zA-Z0-9.-]/g,
|
||||
"",
|
||||
);
|
||||
const jsonPath = path.resolve(
|
||||
diagnosticsPath,
|
||||
`codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(jsonPath, JSON.stringify(diagnostic));
|
||||
|
||||
return jsonPath;
|
||||
}
|
||||
|
||||
/** Gets the path where diagnostics for {@link language} should be stored in the database. */
|
||||
export function getDatabaseDiagnosticsPath(
|
||||
config: Config,
|
||||
language: Language | undefined,
|
||||
) {
|
||||
return getDiagnosticsPath(config.dbLocation, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given diagnostic to the database.
|
||||
*
|
||||
@@ -185,78 +250,74 @@ export function addNoLanguageDiagnostic(
|
||||
* @param diagnostic The diagnostic message to add to the database.
|
||||
*/
|
||||
function writeDiagnostic(
|
||||
action: ActionState<["Logger"]>,
|
||||
config: Config,
|
||||
language: Language | undefined,
|
||||
diagnostic: DiagnosticMessage,
|
||||
) {
|
||||
const logger = getActionsLogger();
|
||||
const databasePath = language
|
||||
? getCodeQLDatabasePath(config, language)
|
||||
: config.dbLocation;
|
||||
const diagnosticsPath = path.resolve(
|
||||
databasePath,
|
||||
"diagnostic",
|
||||
"codeql-action",
|
||||
);
|
||||
const diagnosticsPath = getDatabaseDiagnosticsPath(config, language);
|
||||
|
||||
try {
|
||||
// Create the directory if it doesn't exist yet.
|
||||
mkdirSync(diagnosticsPath, { recursive: true });
|
||||
|
||||
// Include a monotonically increasing suffix to avoid filename collisions
|
||||
// between diagnostics produced within the same millisecond.
|
||||
const uniqueSuffix = (diagnosticCounter++).toString();
|
||||
// We should only need to remove colons, but to be defensive, only allow a restricted set of
|
||||
// characters.
|
||||
const sanitizedTimestamp = diagnostic.timestamp.replace(
|
||||
/[^a-zA-Z0-9.-]/g,
|
||||
"",
|
||||
);
|
||||
const jsonPath = path.resolve(
|
||||
diagnosticsPath,
|
||||
`codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json`,
|
||||
);
|
||||
|
||||
writeFileSync(jsonPath, JSON.stringify(diagnostic));
|
||||
writeDiagnosticFile(diagnosticsPath, diagnostic);
|
||||
} catch (err) {
|
||||
logger.warning(`Unable to write diagnostic message to database: ${err}`);
|
||||
logger.debug(JSON.stringify(diagnostic));
|
||||
action.logger.warning(
|
||||
`Unable to write diagnostic message to database: ${err}`,
|
||||
);
|
||||
action.logger.debug(JSON.stringify(diagnostic));
|
||||
}
|
||||
}
|
||||
|
||||
/** Report if there are unwritten diagnostics and write them to the log. */
|
||||
export function logUnwrittenDiagnostics() {
|
||||
const logger = getActionsLogger();
|
||||
const num = unwrittenDiagnostics.length;
|
||||
/** Report if there are temporary diagnostics and write them to the log. */
|
||||
export function logTemporaryDiagnostics(action: ActionState<["Logger"]>) {
|
||||
const num = temporaryDiagnostics.length;
|
||||
|
||||
if (num > 0) {
|
||||
logger.warning(
|
||||
action.logger.warning(
|
||||
`${num} diagnostic(s) could not be written to the database and will not appear on the Tool Status Page.`,
|
||||
);
|
||||
|
||||
for (const unwritten of unwrittenDiagnostics) {
|
||||
logger.debug(JSON.stringify(unwritten.diagnostic));
|
||||
for (const temporary of temporaryDiagnostics) {
|
||||
action.logger.debug(fs.readFileSync(temporary.path, "utf-8"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes all unwritten diagnostics to disk. */
|
||||
export function flushDiagnostics(config: Config) {
|
||||
const logger = getActionsLogger();
|
||||
/** Relocates all temporary diagnostics to the respective databases. */
|
||||
export function flushDiagnostics(
|
||||
action: ActionState<["Logger"]>,
|
||||
config: Config,
|
||||
) {
|
||||
const diagnosticsCount = temporaryDiagnostics.length;
|
||||
action.logger.debug(
|
||||
`Moving ${diagnosticsCount} diagnostic(s) to their respective databases.`,
|
||||
);
|
||||
|
||||
const diagnosticsCount =
|
||||
unwrittenDiagnostics.length + unwrittenDefaultLanguageDiagnostics.length;
|
||||
logger.debug(`Writing ${diagnosticsCount} diagnostic(s) to database.`);
|
||||
const failedFlushDiagnostics: TemporaryDiagnostic[] = [];
|
||||
for (const temporary of temporaryDiagnostics) {
|
||||
// If `temporary.language` is `undefined`, then we didn't have a `config` at the time that
|
||||
// `addNoLanguageDiagnostic` was called. In that case, we arbitrarily choose the first
|
||||
// configured language here like we would have done in `addNoLanguageDiagnostic`.
|
||||
const directory = getDatabaseDiagnosticsPath(
|
||||
config,
|
||||
temporary.language ?? config.languages[0],
|
||||
);
|
||||
const filename = path.basename(temporary.path);
|
||||
const destination = path.join(directory, filename);
|
||||
|
||||
for (const unwritten of unwrittenDiagnostics) {
|
||||
writeDiagnostic(config, unwritten.language, unwritten.diagnostic);
|
||||
}
|
||||
for (const unwritten of unwrittenDefaultLanguageDiagnostics) {
|
||||
addNoLanguageDiagnostic(config, unwritten);
|
||||
try {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
fs.renameSync(temporary.path, destination);
|
||||
} catch (err) {
|
||||
action.logger.warning(
|
||||
`Failed to rename '${temporary.path}' to '${destination}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
|
||||
failedFlushDiagnostics.push(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the unwritten diagnostics arrays.
|
||||
unwrittenDiagnostics = [];
|
||||
unwrittenDefaultLanguageDiagnostics = [];
|
||||
// Reset the temporary diagnostics arrays.
|
||||
temporaryDiagnostics = failedFlushDiagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -170,6 +170,8 @@ export enum Feature {
|
||||
*/
|
||||
PerLanguageBundles = "per_language_bundles_v2",
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Whether we should fail early if we detect that traced Swift analysis is unsupported. */
|
||||
SwiftSkipUnsupportedTracedAnalysis = "swift_skip_unsupported_traced_analysis",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
/** Note that this currently only disables baseline file coverage information. */
|
||||
@@ -466,6 +468,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_START_PROXY_USE_FEATURES_RELEASE",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.SwiftSkipUnsupportedTracedAnalysis]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_SWIFT_SKIP_UNSUPPORTED_TRACED_ANALYSIS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.ToolsRepositoryProperty]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_TOOLS_REPOSITORY_PROPERTY",
|
||||
|
||||
@@ -7,6 +7,11 @@ import * as actionsUtil from "./actions-util";
|
||||
import { CodeScanning, RiskAssessment } from "./analyses";
|
||||
import { getApiClient, getGitHubVersion } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
import {
|
||||
hasConfigKey,
|
||||
type AbortedInitConfig,
|
||||
type ActionConfig,
|
||||
} from "./config/action-config";
|
||||
import {
|
||||
Config,
|
||||
isCodeScanningEnabled,
|
||||
@@ -16,8 +21,10 @@ import {
|
||||
getArtifactSuffix,
|
||||
getArtifactUploaderClient,
|
||||
sanitizeArtifactName,
|
||||
type UploadDebugArtifactsConfig,
|
||||
} from "./debug-artifacts";
|
||||
import * as dependencyCaching from "./dependency-caching";
|
||||
import { getTempDiagnosticPath } from "./diagnostics";
|
||||
import { EnvVar, ReadOnlyEnv } from "./environment";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
@@ -86,6 +93,11 @@ interface FailedSarifInfo {
|
||||
checkoutPath: string;
|
||||
}
|
||||
|
||||
/** The minimal Action configuration required to prepare and upload a failed SARIF. */
|
||||
export type FailedSarifConfig = ActionConfig<
|
||||
"codeQLCmd" | "tempDir" | "analysisKinds" | "languages"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Tries to prepare a SARIF file that contains information about a failed analysis.
|
||||
*
|
||||
@@ -94,7 +106,7 @@ interface FailedSarifInfo {
|
||||
async function prepareFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: Config,
|
||||
config: FailedSarifConfig,
|
||||
): Promise<Result<FailedSarifInfo, UploadFailedSarifResult>> {
|
||||
if (!config.codeQLCmd) {
|
||||
return new Failure({
|
||||
@@ -160,7 +172,7 @@ async function prepareFailedSarif(
|
||||
async function generateFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: Config,
|
||||
config: ActionConfig<"codeQLCmd" | "tempDir">,
|
||||
category: string | undefined,
|
||||
checkoutPath: string,
|
||||
sarifFile?: string,
|
||||
@@ -178,7 +190,19 @@ async function generateFailedSarif(
|
||||
databasePath === undefined ||
|
||||
!(await features.getValue(Feature.ExportDiagnosticsEnabled, codeql))
|
||||
) {
|
||||
await codeql.diagnosticsExport(sarifFile, category, config);
|
||||
// If we don't have a database path, then we may have diagnostics that were written to a
|
||||
// temporary location. Get the path that they would have been written to and check whether
|
||||
// it exists. If so, pass it on to the CLI. The CLI will not complain if we give it a
|
||||
// path that doesn't exist, so we are just being extra defensive here.
|
||||
let diagnosticDir: string | undefined = getTempDiagnosticPath(
|
||||
config.tempDir,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(diagnosticDir)) {
|
||||
diagnosticDir = undefined;
|
||||
}
|
||||
|
||||
await codeql.diagnosticsExport(sarifFile, category, config, diagnosticDir);
|
||||
} else {
|
||||
// We call 'database export-diagnostics' to find any per-database diagnostics.
|
||||
await codeql.databaseExportDiagnostics(databasePath, sarifFile, category);
|
||||
@@ -192,7 +216,7 @@ async function generateFailedSarif(
|
||||
* category for the workflow.
|
||||
*/
|
||||
async function maybeUploadFailedSarif(
|
||||
config: Config,
|
||||
config: FailedSarifConfig,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
@@ -227,7 +251,7 @@ async function maybeUploadFailedSarif(
|
||||
|
||||
/** Uploads a failed SARIF file as workflow artifact, if it can be generated. */
|
||||
async function maybeUploadFailedSarifArtifact(
|
||||
config: Config,
|
||||
config: FailedSarifConfig,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
): Promise<UploadFailedSarifResult> {
|
||||
@@ -267,7 +291,7 @@ async function maybeUploadFailedSarifArtifact(
|
||||
* @returns The results of uploading the SARIF file for the failure.
|
||||
*/
|
||||
export async function tryUploadSarifIfRunFailed(
|
||||
config: Config,
|
||||
config: FailedSarifConfig,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
@@ -324,12 +348,12 @@ export async function tryUploadSarifIfRunFailed(
|
||||
export async function uploadFailureInfo(
|
||||
uploadAllAvailableDebugArtifacts: (
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: UploadDebugArtifactsConfig,
|
||||
logger: Logger,
|
||||
) => Promise<void>,
|
||||
printDebugLogs: (config: Config) => Promise<void>,
|
||||
printDebugLogs: (config: Partial<Config>) => Promise<void>,
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: Config | AbortedInitConfig,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
@@ -376,7 +400,7 @@ export async function uploadFailureInfo(
|
||||
}
|
||||
|
||||
// Upload appropriate Actions artifacts for debugging
|
||||
if (config.debugMode) {
|
||||
if (config.debugMode && hasConfigKey(config, "dbLocation")) {
|
||||
logger.info(
|
||||
"Debug mode is on. Uploading available database bundles and logs as Actions debugging artifacts...",
|
||||
);
|
||||
@@ -384,7 +408,7 @@ export async function uploadFailureInfo(
|
||||
await printDebugLogs(config);
|
||||
}
|
||||
|
||||
if (actionsUtil.isSelfHostedRunner()) {
|
||||
if (actionsUtil.isSelfHostedRunner() && config.dbLocation) {
|
||||
try {
|
||||
fs.rmSync(config.dbLocation, {
|
||||
recursive: true,
|
||||
@@ -447,7 +471,7 @@ function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
|
||||
*/
|
||||
async function recordOverlayStatus(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
config: Partial<Config>,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
env: ReadOnlyEnv,
|
||||
@@ -456,7 +480,8 @@ async function recordOverlayStatus(
|
||||
if (
|
||||
config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase ||
|
||||
env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" ||
|
||||
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
|
||||
!(await features.getValue(Feature.OverlayAnalysisStatusSave)) ||
|
||||
config.languages === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { runInActions, type Action, type ActionState } from "./action-common";
|
||||
import {
|
||||
restoreInputs,
|
||||
getOptionalInput,
|
||||
@@ -15,22 +16,25 @@ import {
|
||||
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { getCodeQL } from "./codeql";
|
||||
import { type Config, getConfig } from "./config-utils";
|
||||
import {
|
||||
hasConfigKey,
|
||||
type AbortedInitConfig,
|
||||
type Config,
|
||||
} from "./config/action-config";
|
||||
import { getAbortedInitConfig, getConfig } from "./config-utils";
|
||||
import * as debugArtifacts from "./debug-artifacts";
|
||||
import {
|
||||
DependencyCachingUsageReport,
|
||||
getDependencyCacheUsage,
|
||||
} from "./dependency-caching";
|
||||
import { EnvVar, getEnv, exportEnvVar } from "./environment";
|
||||
import { EnvVar, type ReadOnlyEnv } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import * as initActionPostHelper from "./init-action-post-helper";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import {
|
||||
StatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
ActionName,
|
||||
@@ -45,16 +49,17 @@ interface InitPostStatusReport
|
||||
initActionPostHelper.JobStatusReport,
|
||||
initActionPostHelper.DependencyCachingUsageReport {}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run(action: ActionState<["Base", "Logger", "Env"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
const logger = action.logger;
|
||||
|
||||
const logger = getActionsLogger();
|
||||
let config: Config | undefined;
|
||||
let config: Config | AbortedInitConfig | undefined;
|
||||
let uploadFailedSarifResult:
|
||||
| initActionPostHelper.UploadFailedSarifResult
|
||||
| undefined;
|
||||
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
|
||||
|
||||
try {
|
||||
// Read the job status before restoring inputs, since it is provided by the Actions runtime
|
||||
// environment for this step and would otherwise be overwritten by the value that the `init`
|
||||
@@ -64,7 +69,15 @@ async function run(startedAt: Date) {
|
||||
// Restore inputs from `init` Action.
|
||||
restoreInputs(logger);
|
||||
|
||||
config = await getConfig(getTemporaryDirectory(), logger);
|
||||
const tmpDir = getTemporaryDirectory();
|
||||
config = await getConfig(tmpDir, logger);
|
||||
|
||||
// If we weren't able to read the normal configuration file, the `init` step failed before it could write it.
|
||||
// Try to load the "failed" configuration file instead which may have been written by the `init` step
|
||||
// with a minimal configuration for this post-action.
|
||||
if (config === undefined) {
|
||||
config = getAbortedInitConfig(action, tmpDir);
|
||||
}
|
||||
|
||||
const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
|
||||
checkGitHubVersionInRange(gitHubVersion, logger);
|
||||
@@ -77,7 +90,7 @@ async function run(startedAt: Date) {
|
||||
logger,
|
||||
);
|
||||
|
||||
if (config === undefined) {
|
||||
if (config?.codeQLCmd === undefined) {
|
||||
logger.warning(
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
|
||||
);
|
||||
@@ -92,7 +105,7 @@ async function run(startedAt: Date) {
|
||||
repositoryNwo,
|
||||
features,
|
||||
jobStatus,
|
||||
getEnv(),
|
||||
action.env,
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -102,6 +115,7 @@ async function run(startedAt: Date) {
|
||||
// and where caching may not be enabled.
|
||||
if (
|
||||
(await gitUtils.isAnalyzingDefaultBranch()) &&
|
||||
hasConfigKey(config, "dependencyCachingEnabled") &&
|
||||
config.dependencyCachingEnabled !== CachingKind.None
|
||||
) {
|
||||
dependencyCachingUsage = await getDependencyCacheUsage(logger);
|
||||
@@ -112,9 +126,9 @@ async function run(startedAt: Date) {
|
||||
core.setFailed(error.message);
|
||||
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
ActionName.InitPost,
|
||||
action.name,
|
||||
getActionsStatus(error),
|
||||
startedAt,
|
||||
action.startedAt,
|
||||
config,
|
||||
await checkDiskUsage(logger),
|
||||
logger,
|
||||
@@ -126,13 +140,13 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const jobStatus = getFinalJobStatus(config);
|
||||
const jobStatus = getFinalJobStatus(action, config);
|
||||
logger.info(`CodeQL job status was ${getJobStatusDisplayName(jobStatus)}.`);
|
||||
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
ActionName.InitPost,
|
||||
action.name,
|
||||
"success",
|
||||
startedAt,
|
||||
action.startedAt,
|
||||
config,
|
||||
await checkDiskUsage(logger),
|
||||
logger,
|
||||
@@ -157,16 +171,21 @@ async function run(startedAt: Date) {
|
||||
* Otherwise, we determine the job status based on whether the analyze step
|
||||
* completed successfully and whether we have a valid CodeQL config.
|
||||
*/
|
||||
function getFinalJobStatus(config: Config | undefined): JobStatus {
|
||||
const existingJobStatus = getJobStatusFromEnvironment();
|
||||
function getFinalJobStatus(
|
||||
action: ActionState<["Env"]>,
|
||||
config: Partial<Config> | undefined,
|
||||
): JobStatus {
|
||||
const existingJobStatus = getJobStatusFromEnvironment(action.env);
|
||||
if (existingJobStatus !== undefined) {
|
||||
return existingJobStatus;
|
||||
}
|
||||
|
||||
let jobStatus: JobStatus;
|
||||
|
||||
if (process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true") {
|
||||
exportEnvVar(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
|
||||
if (
|
||||
action.env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true"
|
||||
) {
|
||||
action.env.export(EnvVar.JOB_STATUS, JobStatus.SuccessStatus);
|
||||
jobStatus = JobStatus.SuccessStatus;
|
||||
} else if (config !== undefined) {
|
||||
// - We have computed a CodeQL config
|
||||
@@ -191,7 +210,7 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
|
||||
|
||||
// This shouldn't be necessary, but in the odd case that we run more than one
|
||||
// `init` post step, ensure the job status is consistent between them.
|
||||
exportEnvVar(EnvVar.JOB_STATUS, jobStatus);
|
||||
action.env.export(EnvVar.JOB_STATUS, jobStatus);
|
||||
return jobStatus;
|
||||
}
|
||||
|
||||
@@ -200,8 +219,8 @@ function getFinalJobStatus(config: Config | undefined): JobStatus {
|
||||
*
|
||||
* If the job status is invalid, return `UnknownStatus`.
|
||||
*/
|
||||
function getJobStatusFromEnvironment(): JobStatus | undefined {
|
||||
const jobStatusFromEnvironment = process.env[EnvVar.JOB_STATUS];
|
||||
function getJobStatusFromEnvironment(env: ReadOnlyEnv): JobStatus | undefined {
|
||||
const jobStatusFromEnvironment = env.getOptional(EnvVar.JOB_STATUS);
|
||||
|
||||
if (jobStatusFromEnvironment !== undefined) {
|
||||
// Validate the job status from the environment. If it is invalid, return unknown.
|
||||
@@ -216,18 +235,11 @@ function getJobStatusFromEnvironment(): JobStatus | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const initPost: Action = {
|
||||
name: ActionName.InitPost,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`init post action failed: ${wrapError(error).message}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.InitPost,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
return await runInActions(initPost);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
addDiagnostic,
|
||||
addNoLanguageDiagnostic,
|
||||
flushDiagnostics,
|
||||
logUnwrittenDiagnostics,
|
||||
logTemporaryDiagnostics,
|
||||
makeDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "./diagnostics";
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
runDatabaseInitCluster,
|
||||
} from "./init";
|
||||
import { JavaEnvVars, BuiltInLanguage } from "./languages";
|
||||
import { isSwiftCompatible } from "./languages/swift";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import {
|
||||
downloadOverlayBaseDatabaseFromCache,
|
||||
@@ -418,14 +419,7 @@ async function run(
|
||||
logger,
|
||||
});
|
||||
|
||||
if (
|
||||
config.languages.includes(BuiltInLanguage.swift) &&
|
||||
process.platform !== "darwin"
|
||||
) {
|
||||
throw new ConfigurationError(
|
||||
`Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`,
|
||||
);
|
||||
}
|
||||
await isSwiftCompatible(actionStateWithFeatures, config, codeql);
|
||||
|
||||
if (repositoryPropertiesResult.isFailure()) {
|
||||
addNoLanguageDiagnostic(
|
||||
@@ -457,6 +451,18 @@ async function run(
|
||||
|
||||
await checkInstallPython311(config.languages, codeql);
|
||||
} catch (unwrappedError) {
|
||||
// If we have initialised the Action state, then try to save a subset of it for use by the `init-post` action
|
||||
// so that it can try to create and upload additional diagnostic information.
|
||||
if (config !== undefined) {
|
||||
try {
|
||||
configUtils.saveAbortedInitConfig(actionState, config);
|
||||
} catch (configSaveError) {
|
||||
logger.warning(
|
||||
`Unable to save configuration for '${ActionName.InitPost}' step: ${getErrorMessage(configSaveError)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const error = wrapError(unwrappedError);
|
||||
core.setFailed(error.message);
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
@@ -472,6 +478,7 @@ async function run(
|
||||
if (statusReportBase !== undefined) {
|
||||
await sendStatusReport(statusReportBase);
|
||||
}
|
||||
logTemporaryDiagnostics(actionState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -754,9 +761,9 @@ async function run(
|
||||
);
|
||||
}
|
||||
|
||||
// Write diagnostics to the database that we previously stored in memory because the database
|
||||
// did not exist until now.
|
||||
flushDiagnostics(config);
|
||||
// Move diagnostics to the database that we previously kept in a temporary location
|
||||
// because the database did not exist until now
|
||||
flushDiagnostics(actionState, config);
|
||||
|
||||
// We save the config here instead of at the end of `initConfig` because we
|
||||
// may have updated the config returned from `initConfig`, e.g. to revert to
|
||||
@@ -785,7 +792,7 @@ async function run(
|
||||
);
|
||||
return;
|
||||
} finally {
|
||||
logUnwrittenDiagnostics();
|
||||
logTemporaryDiagnostics(actionState);
|
||||
}
|
||||
await sendCompletedStatusReport(
|
||||
startedAt,
|
||||
|
||||
578
src/languages/swift.test.ts
Normal file
578
src/languages/swift.test.ts
Normal file
@@ -0,0 +1,578 @@
|
||||
import fs from "fs";
|
||||
|
||||
import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { getCodeQLForTesting } from "../codeql";
|
||||
import * as diagnostics from "../diagnostics";
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import { Feature } from "../feature-flags";
|
||||
import {
|
||||
checkExpectedLogMessages,
|
||||
checkUnexpectedLogMessages,
|
||||
createFeatures,
|
||||
createTestConfig,
|
||||
getTestEnv,
|
||||
initAllState,
|
||||
makeVersionInfo,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "../testing-utils";
|
||||
import { ToolsFeature } from "../tools-features";
|
||||
import { withTmpDir } from "../util";
|
||||
|
||||
import {
|
||||
isSwiftCompatible,
|
||||
XCODE_SELECT_LINK_PATH,
|
||||
xcodeVersion,
|
||||
} from "./swift";
|
||||
|
||||
import { BuiltInLanguage } from ".";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial("xcodeVersion returns undefined if symlink doesn't exist", (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon.stub(fs, "statSync").throws(new Error("ENOENT"));
|
||||
|
||||
t.is(xcodeVersion(logger), undefined);
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Unable to determine Xcode version: ENOENT",
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial("xcodeVersion returns undefined if file is not a symlink", (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => false } as fs.Stats);
|
||||
|
||||
t.is(xcodeVersion(logger), undefined);
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"exists, but is not a symbolic link",
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"xcodeVersion returns undefined if resolving the symlink returns nothing",
|
||||
(t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon.stub(fs, "readlinkSync").returns("");
|
||||
|
||||
t.is(xcodeVersion(logger), undefined);
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"unexpectedly returned nothing",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"xcodeVersion returns undefined if resolved path doesn't include pattern",
|
||||
(t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode.app/Contents/Developer");
|
||||
|
||||
t.is(xcodeVersion(logger), undefined);
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"does not contain expected pattern",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("xcodeVersion returns undefined if match can't be parsed", (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode_00.0.app/Contents/Developer");
|
||||
|
||||
t.is(xcodeVersion(logger), undefined);
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Couldn't parse '00.0' as a semantic version.",
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial("xcodeVersion returns version from resolved path", (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode_16.4.app/Contents/Developer");
|
||||
|
||||
const result = xcodeVersion(logger);
|
||||
|
||||
if (t.truthy(result)) {
|
||||
t.is(result.major, 16);
|
||||
t.is(result.minor, 4);
|
||||
}
|
||||
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
});
|
||||
|
||||
test("isSwiftCompatible doesn't throw for non-Swift languages", async (t) => {
|
||||
for (const language of Object.values(BuiltInLanguage)) {
|
||||
if (language === BuiltInLanguage.swift) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const codeql = await getCodeQLForTesting();
|
||||
await t.notThrowsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState(),
|
||||
createTestConfig({ languages: [language] }),
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("isSwiftCompatible doesn't throw for Swift if CLI supports swiftSupportsAllPlatforms", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
|
||||
const supportsFeature = sinon
|
||||
.stub(codeql, "supportsFeature")
|
||||
.withArgs(ToolsFeature.SwiftSupportsAllPlatforms)
|
||||
.resolves(true);
|
||||
|
||||
await t.notThrowsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState({ platform: "darwin", env, logger }),
|
||||
createTestConfig({ languages: [BuiltInLanguage.swift] }),
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
|
||||
t.is(supportsFeature.callCount, 1);
|
||||
t.deepEqual(supportsFeature.args[0], [
|
||||
ToolsFeature.SwiftSupportsAllPlatforms,
|
||||
]);
|
||||
}));
|
||||
|
||||
test("isSwiftCompatible doesn't throw for Swift on darwin", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
await t.notThrowsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState({ platform: "darwin", env, logger }),
|
||||
createTestConfig({
|
||||
tempDir: tmpDir,
|
||||
languages: [BuiltInLanguage.swift],
|
||||
}),
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
const nonDarwinPlatforms: NodeJS.Platform[] = ["linux", "win32"];
|
||||
for (const nonDarwinPlatform of nonDarwinPlatforms) {
|
||||
test(`isSwiftCompatible throws for Swift on ${nonDarwinPlatform}`, async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
await t.throwsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState({ platform: nonDarwinPlatform, env, logger }),
|
||||
createTestConfig({ languages: [BuiltInLanguage.swift] }),
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
test("isSwiftCompatible warns if version string is not a semver", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const codeql = await getCodeQLForTesting("codeql-for-testing", logger, env);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
await isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "unexpected",
|
||||
env,
|
||||
}),
|
||||
createTestConfig({ languages: [BuiltInLanguage.swift] }),
|
||||
codeql,
|
||||
);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Unable to determine version of macOS, got: unexpected",
|
||||
]);
|
||||
}));
|
||||
|
||||
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
|
||||
test.serial(
|
||||
"isSwiftCompatible logs and adds diagnostic if macOS version is unsupported",
|
||||
async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
|
||||
|
||||
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
|
||||
await isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "27.0.0",
|
||||
env,
|
||||
}),
|
||||
config,
|
||||
codeql,
|
||||
);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Traced Swift analysis is not supported on macOS 27",
|
||||
]);
|
||||
|
||||
t.is(addDiagnostic.callCount, 1);
|
||||
t.like(addDiagnostic.args[0], [
|
||||
config,
|
||||
BuiltInLanguage.swift,
|
||||
{
|
||||
attributes: {
|
||||
languages: [BuiltInLanguage.swift],
|
||||
macOSVersion: "27.0.0",
|
||||
},
|
||||
severity: "warning",
|
||||
source: {
|
||||
id: "codeql-action/unsupported-traced-swift-analysis-macos",
|
||||
name: "Traced Swift analysis is not supported on this version of macOS",
|
||||
},
|
||||
visibility: {
|
||||
cliSummaryTable: true,
|
||||
statusPage: true,
|
||||
telemetry: true,
|
||||
},
|
||||
} satisfies Partial<diagnostics.DiagnosticMessage>,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
|
||||
test.serial(
|
||||
"isSwiftCompatible throws if macOS version is unsupported and FF is enabled",
|
||||
async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const features = createFeatures([
|
||||
Feature.SwiftSkipUnsupportedTracedAnalysis,
|
||||
]);
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
|
||||
|
||||
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
|
||||
await t.throwsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "27.0.0",
|
||||
env,
|
||||
features,
|
||||
}),
|
||||
config,
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
|
||||
t.is(addDiagnostic.callCount, 1);
|
||||
t.like(addDiagnostic.args[0], [
|
||||
config,
|
||||
BuiltInLanguage.swift,
|
||||
{
|
||||
attributes: {
|
||||
languages: [BuiltInLanguage.swift],
|
||||
macOSVersion: "27.0.0",
|
||||
},
|
||||
severity: "error",
|
||||
source: {
|
||||
id: "codeql-action/unsupported-traced-swift-analysis-macos",
|
||||
name: "Traced Swift analysis is not supported on this version of macOS",
|
||||
},
|
||||
visibility: {
|
||||
cliSummaryTable: true,
|
||||
statusPage: true,
|
||||
telemetry: true,
|
||||
},
|
||||
} satisfies Partial<diagnostics.DiagnosticMessage>,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
|
||||
test.serial(
|
||||
"isSwiftCompatible doesn't add a diagnostic if Xcode version is supported",
|
||||
async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode_26.0.app/Contents/Developer");
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
|
||||
|
||||
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
|
||||
await isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "26.0.0",
|
||||
env,
|
||||
}),
|
||||
config,
|
||||
codeql,
|
||||
);
|
||||
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkUnexpectedLogMessages(t, logger.messages, [
|
||||
"Traced Swift analysis is not supported on Xcode 27",
|
||||
]);
|
||||
|
||||
t.is(addDiagnostic.callCount, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
|
||||
test.serial(
|
||||
"isSwiftCompatible logs and adds diagnostic if Xcode version is unsupported",
|
||||
async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode_27.0.app/Contents/Developer");
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
|
||||
|
||||
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
|
||||
await isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "26.0.0",
|
||||
env,
|
||||
}),
|
||||
config,
|
||||
codeql,
|
||||
);
|
||||
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Traced Swift analysis is not supported on Xcode 27",
|
||||
]);
|
||||
|
||||
t.is(addDiagnostic.callCount, 1);
|
||||
t.like(addDiagnostic.args[0], [
|
||||
config,
|
||||
BuiltInLanguage.swift,
|
||||
{
|
||||
attributes: {
|
||||
languages: [BuiltInLanguage.swift],
|
||||
xcodeVersion: "27.0.0",
|
||||
},
|
||||
severity: "warning",
|
||||
source: {
|
||||
id: "codeql-action/unsupported-traced-swift-analysis-xcode",
|
||||
name: "Traced Swift analysis is not supported on this version of Xcode",
|
||||
},
|
||||
visibility: {
|
||||
cliSummaryTable: true,
|
||||
statusPage: true,
|
||||
telemetry: true,
|
||||
},
|
||||
} satisfies Partial<diagnostics.DiagnosticMessage>,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
// `addDiagnostic` changes global state and we must stub it, so this test must be serial.
|
||||
test.serial(
|
||||
"isSwiftCompatible throws if Xcode version is unsupported and FF is enabled",
|
||||
async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tmpDir);
|
||||
|
||||
const statSync = sinon
|
||||
.stub(fs, "statSync")
|
||||
.returns({ isSymbolicLink: () => true } as fs.Stats);
|
||||
const readlinkSync = sinon
|
||||
.stub(fs, "readlinkSync")
|
||||
.returns("/Applications/Xcode_27.0.app/Contents/Developer");
|
||||
|
||||
const features = createFeatures([
|
||||
Feature.SwiftSkipUnsupportedTracedAnalysis,
|
||||
]);
|
||||
|
||||
const codeql = await getCodeQLForTesting(
|
||||
"codeql-for-testing",
|
||||
logger,
|
||||
env,
|
||||
);
|
||||
sinon.stub(codeql, "getVersion").resolves(makeVersionInfo("2.27.0"));
|
||||
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addDiagnostic");
|
||||
|
||||
const config = createTestConfig({ languages: [BuiltInLanguage.swift] });
|
||||
await t.throwsAsync(
|
||||
isSwiftCompatible(
|
||||
initAllState({
|
||||
logger,
|
||||
platform: "darwin",
|
||||
osRelease: "26.0.0",
|
||||
env,
|
||||
features,
|
||||
}),
|
||||
config,
|
||||
codeql,
|
||||
),
|
||||
);
|
||||
|
||||
t.is(statSync.callCount, 1);
|
||||
t.deepEqual(statSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
t.is(readlinkSync.callCount, 1);
|
||||
t.deepEqual(readlinkSync.args[0], [XCODE_SELECT_LINK_PATH]);
|
||||
|
||||
t.is(addDiagnostic.callCount, 1);
|
||||
t.like(addDiagnostic.args[0], [
|
||||
config,
|
||||
BuiltInLanguage.swift,
|
||||
{
|
||||
attributes: {
|
||||
languages: [BuiltInLanguage.swift],
|
||||
xcodeVersion: "27.0.0",
|
||||
},
|
||||
severity: "error",
|
||||
source: {
|
||||
id: "codeql-action/unsupported-traced-swift-analysis-xcode",
|
||||
name: "Traced Swift analysis is not supported on this version of Xcode",
|
||||
},
|
||||
visibility: {
|
||||
cliSummaryTable: true,
|
||||
statusPage: true,
|
||||
telemetry: true,
|
||||
},
|
||||
} satisfies Partial<diagnostics.DiagnosticMessage>,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
216
src/languages/swift.ts
Normal file
216
src/languages/swift.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import * as semver from "semver";
|
||||
|
||||
import { ActionState, Logger } from "../action-common";
|
||||
import { CodeQL } from "../codeql";
|
||||
import { Config } from "../config-utils";
|
||||
import { addDiagnostic, makeDiagnostic } from "../diagnostics";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { macOSVersion } from "../platform";
|
||||
import { ToolsFeature } from "../tools-features";
|
||||
import { ConfigurationError, getErrorMessage } from "../util";
|
||||
|
||||
import { BuiltInLanguage } from ".";
|
||||
|
||||
/** The static path we check for a symbolic link to the (dynamic) Xcode location. */
|
||||
export const XCODE_SELECT_LINK_PATH = "/private/var/db/xcode_select_link";
|
||||
|
||||
/** The pattern we expect to find in the Xcode path. */
|
||||
export const XCODE_APP_FILENAME_PATTERN = new RegExp(
|
||||
/(?<filename>Xcode_(?<majorMinor>\d+.\d+).app)/,
|
||||
);
|
||||
|
||||
/** macOS 27 and above do not support traced extraction for Swift. */
|
||||
export const SWIFT_TRACED_UNSUPPORTED_MACOS = 27;
|
||||
|
||||
/** Xcode 27 and above do not support traced extraction for Swift. */
|
||||
export const SWIFT_TRACED_UNSUPPORTED_XCODE = 27;
|
||||
|
||||
/**
|
||||
* Tries to determine the version of Xcode that is installed.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @returns The Xcode version or `undefined` if it couldn't be determined.
|
||||
*/
|
||||
export function xcodeVersion(logger: Logger): semver.SemVer | undefined {
|
||||
try {
|
||||
// Stat the expected symbolic link to check that it exists and is a symbolic link.
|
||||
// The `readlinkSync` call below returns an empty string in either case and so
|
||||
// this check allows us to distinguish between the two cases.
|
||||
const stats = fs.statSync(XCODE_SELECT_LINK_PATH);
|
||||
|
||||
if (!stats.isSymbolicLink()) {
|
||||
logger.warning(
|
||||
`${XCODE_SELECT_LINK_PATH} exists, but is not a symbolic link.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Read what the symbolic link points to.
|
||||
const xcodePath = fs.readlinkSync(XCODE_SELECT_LINK_PATH);
|
||||
|
||||
if (xcodePath === "") {
|
||||
logger.warning(
|
||||
`Resolving ${XCODE_SELECT_LINK_PATH} unexpectedly returned nothing.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try to extract the version from the path.
|
||||
const matchResult = xcodePath.match(XCODE_APP_FILENAME_PATTERN);
|
||||
|
||||
if (matchResult?.groups === undefined) {
|
||||
logger.warning(
|
||||
`Xcode path '${xcodePath}' does not contain expected pattern.`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const majorMinor = matchResult.groups["majorMinor"];
|
||||
const version = semver.coerce(majorMinor);
|
||||
|
||||
if (version === null) {
|
||||
logger.warning(`Couldn't parse '${majorMinor}' as a semantic version.`);
|
||||
return undefined;
|
||||
}
|
||||
return version;
|
||||
} catch (err) {
|
||||
logger.warning(
|
||||
`Unable to determine Xcode version: ${getErrorMessage(err)}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diagnostic indicating that `version` of `product` is unsupported for traced Swift analysis.
|
||||
* Depending on `skipUnsupportedTracedAnalysis`, this function then either throws a {@link ConfigurationError}
|
||||
* or logs the problem as a warning.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param config The CodeQL Action configuration.
|
||||
* @param skipUnsupportedTracedAnalysis Whether this is a fatal error.
|
||||
* @param product The product that the version is unsupported of.
|
||||
* @param version The unsupported version.
|
||||
*/
|
||||
function handleUnsupportedVersion(
|
||||
logger: Logger,
|
||||
config: Config,
|
||||
skipUnsupportedTracedAnalysis: boolean,
|
||||
product: "macOS" | "Xcode",
|
||||
version: semver.SemVer,
|
||||
) {
|
||||
const baseMessage = [
|
||||
`Traced Swift analysis is not supported on ${product} ${SWIFT_TRACED_UNSUPPORTED_MACOS} or above.`,
|
||||
`Configure your analysis to run on macOS ${SWIFT_TRACED_UNSUPPORTED_MACOS - 1} or below`,
|
||||
`and XCode ${SWIFT_TRACED_UNSUPPORTED_XCODE - 1} or below.`,
|
||||
].join(" ");
|
||||
|
||||
const attributeName = product === "macOS" ? "macOSVersion" : "xcodeVersion";
|
||||
|
||||
// Create a diagnostic that will show up on the TSP.
|
||||
addDiagnostic(
|
||||
config,
|
||||
BuiltInLanguage.swift,
|
||||
makeDiagnostic(
|
||||
`codeql-action/unsupported-traced-swift-analysis-${product.toLowerCase()}`,
|
||||
`Traced Swift analysis is not supported on this version of ${product}`,
|
||||
{
|
||||
attributes: {
|
||||
languages: config.languages,
|
||||
[attributeName]: version.toString(),
|
||||
},
|
||||
markdownMessage: baseMessage,
|
||||
severity: skipUnsupportedTracedAnalysis ? "error" : "warning",
|
||||
visibility: {
|
||||
cliSummaryTable: true,
|
||||
statusPage: true,
|
||||
telemetry: true,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Throw an error to abort the analysis if the FF is enabled or log the message.
|
||||
if (skipUnsupportedTracedAnalysis) {
|
||||
// ConfigurationErrors are converted to the "aborted" status by the exception handler
|
||||
// in `init-action.ts` that guards the call to `isSwiftCompatible`.
|
||||
throw new ConfigurationError(baseMessage);
|
||||
} else {
|
||||
// This will also show up as a workflow annotation.
|
||||
logger.warning(baseMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether we can run a Swift analysis on the current runner.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param config The Action configuration.
|
||||
*
|
||||
* @throws {ConfigurationError} If Swift analysis is not possible on the current runner.
|
||||
* @returns True if we can run a Swift analysis.
|
||||
*/
|
||||
export async function isSwiftCompatible(
|
||||
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
|
||||
config: Config,
|
||||
codeql: CodeQL,
|
||||
) {
|
||||
// The checks are not relevant if we are not trying to analyse Swift.
|
||||
if (!config.languages.includes(BuiltInLanguage.swift)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip the checks if the `swiftSupportsAllPlatforms` feature is supported by the CLI.
|
||||
// This is a forward-looking measure that allows a future CLI update to disable these
|
||||
// platform checks in the Action when they shouldn't be enforced anymore.
|
||||
if (await codeql.supportsFeature(ToolsFeature.SwiftSupportsAllPlatforms)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get the macOS version.
|
||||
const version = macOSVersion(action);
|
||||
|
||||
// If `version` is undefined, then we are not on macOS.
|
||||
if (version === undefined) {
|
||||
throw new ConfigurationError(
|
||||
`Swift analysis is only supported on macOS runner images. Please migrate to a macOS runner.`,
|
||||
);
|
||||
}
|
||||
|
||||
const skipUnsupportedTracedAnalysis = await action.features.getValue(
|
||||
Feature.SwiftSkipUnsupportedTracedAnalysis,
|
||||
);
|
||||
if (typeof version === "string") {
|
||||
// If we got a string, we are on macOS but couldn't parse the version string.
|
||||
action.logger.warning(
|
||||
`Unable to determine version of macOS, got: ${version}`,
|
||||
);
|
||||
} else if (version.major >= SWIFT_TRACED_UNSUPPORTED_MACOS) {
|
||||
handleUnsupportedVersion(
|
||||
action.logger,
|
||||
config,
|
||||
skipUnsupportedTracedAnalysis,
|
||||
"macOS",
|
||||
version,
|
||||
);
|
||||
}
|
||||
|
||||
// Determining whether the Xcode version is supported only makes sense on macOS, so we only do it
|
||||
// after determining that we are running on macOS.
|
||||
const xcodeVer = xcodeVersion(action.logger);
|
||||
|
||||
if (
|
||||
xcodeVer !== undefined &&
|
||||
xcodeVer.major >= SWIFT_TRACED_UNSUPPORTED_XCODE
|
||||
) {
|
||||
handleUnsupportedVersion(
|
||||
action.logger,
|
||||
config,
|
||||
skipUnsupportedTracedAnalysis,
|
||||
"Xcode",
|
||||
xcodeVer,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import test from "ava";
|
||||
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
import { BundlePlatform, getBundlePlatform, macOSVersion } from "./platform";
|
||||
import { initAllState } from "./testing-utils";
|
||||
|
||||
for (const [platform, arch, expected] of [
|
||||
["linux", "x64", BundlePlatform.Linux64],
|
||||
@@ -16,3 +17,40 @@ for (const [platform, arch, expected] of [
|
||||
t.is(getBundlePlatform(platform, arch), expected);
|
||||
});
|
||||
}
|
||||
|
||||
const platforms: NodeJS.Platform[] = ["linux", "win32", "freebsd"];
|
||||
for (const platform of platforms) {
|
||||
test(`macOSVersion returns undefined on ${platform}`, (t) => {
|
||||
t.is(macOSVersion(initAllState({ platform })), undefined);
|
||||
});
|
||||
}
|
||||
|
||||
test("macOSVersion returns raw string if semver parsing fails", (t) => {
|
||||
const invalidSemVer = "sealOS-2026";
|
||||
t.is(
|
||||
macOSVersion(
|
||||
initAllState({ platform: "darwin", osRelease: invalidSemVer }),
|
||||
),
|
||||
invalidSemVer,
|
||||
);
|
||||
});
|
||||
|
||||
test("macOSVersion returns semver if parsing succeeds", (t) => {
|
||||
const validSemVer = "27.0.1";
|
||||
|
||||
const version = macOSVersion(
|
||||
initAllState({ platform: "darwin", osRelease: validSemVer }),
|
||||
);
|
||||
|
||||
// Check that `version` is not undefined and narrow the type; throws if undefined.
|
||||
if (t.truthy(version)) {
|
||||
// Check that it's also not just a string.
|
||||
t.not(typeof version, "string");
|
||||
|
||||
// Should be an object with the expected properties.
|
||||
t.is(typeof version, "object");
|
||||
t.is(version["major"], 27);
|
||||
t.is(version["minor"], 0);
|
||||
t.is(version["patch"], 1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import type { ActionState } from "./action-common";
|
||||
|
||||
/** Platform identifiers used in CodeQL bundle asset names. */
|
||||
export enum BundlePlatform {
|
||||
Linux64 = "linux64",
|
||||
@@ -24,3 +28,28 @@ export function getBundlePlatform(
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine the version of macOS.
|
||||
*
|
||||
* @returns
|
||||
* The version as either a semantic version object, the raw version string
|
||||
* if it is not a semantic version, or `undefined` if we are not on macOS.
|
||||
*/
|
||||
export function macOSVersion(
|
||||
action: ActionState<["Base"]>,
|
||||
): semver.SemVer | string | undefined {
|
||||
// Skip if we are not running on macOS.
|
||||
if (action.platform !== "darwin") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try to parse the OS version string.
|
||||
const version = semver.parse(action.osRelease);
|
||||
|
||||
if (version === null) {
|
||||
return action.osRelease;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
@@ -1647,16 +1647,20 @@ test.serial(
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(result, [
|
||||
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
|
||||
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
|
||||
]);
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const result =
|
||||
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersion]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(result, [
|
||||
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
|
||||
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1773,21 +1777,25 @@ test.serial(
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersionDryRun]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(
|
||||
result,
|
||||
[],
|
||||
"Dry-run should return an empty list so the caller falls back.",
|
||||
);
|
||||
t.assert(
|
||||
listStub.calledOnce,
|
||||
"Dry-run should still list Actions caches to populate the diagnostic.",
|
||||
);
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const result =
|
||||
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([Feature.OverlayAnalysisMatchCodeqlVersionDryRun]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(
|
||||
result,
|
||||
[],
|
||||
"Dry-run should return an empty list so the caller falls back.",
|
||||
);
|
||||
t.assert(
|
||||
listStub.calledOnce,
|
||||
"Dry-run should still list Actions caches to populate the diagnostic.",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1801,18 +1809,22 @@ test.serial(
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([
|
||||
Feature.OverlayAnalysisMatchCodeqlVersion,
|
||||
Feature.OverlayAnalysisMatchCodeqlVersionDryRun,
|
||||
]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(result, [
|
||||
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
|
||||
]);
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const result =
|
||||
await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
createFeatures([
|
||||
Feature.OverlayAnalysisMatchCodeqlVersion,
|
||||
Feature.OverlayAnalysisMatchCodeqlVersionDryRun,
|
||||
]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
t.deepEqual(result, [
|
||||
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -61,13 +61,21 @@ export enum ActionName {
|
||||
UploadSarif = "upload-sarif",
|
||||
}
|
||||
|
||||
/** A partial mapping from `ActionName` to names for actions whose display name is not the `ActionName` value. */
|
||||
export const ACTION_DISPLAY_NAMES: Partial<Record<ActionName, string>> = {
|
||||
[ActionName.Analyze]: "analyze",
|
||||
[ActionName.InitPost]: "init post",
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps an `ActionName` to its display name. Usually that is the same, except
|
||||
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
|
||||
* Maps an {@link ActionName} to its display name. Usually that is the same, except
|
||||
* for actions which have a different display name in {@link ACTION_DISPLAY_NAMES}.
|
||||
*/
|
||||
export function getDisplayActionName(actionName: ActionName): string {
|
||||
if (actionName === ActionName.Analyze) {
|
||||
return "analyze";
|
||||
const displayName = ACTION_DISPLAY_NAMES[actionName];
|
||||
|
||||
if (displayName !== undefined) {
|
||||
return displayName;
|
||||
}
|
||||
return actionName;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TextDecoder } from "node:util";
|
||||
import * as os from "os";
|
||||
import path from "path";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
@@ -34,6 +35,7 @@ import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { getBundlePlatform } from "./platform";
|
||||
import { ActionName } from "./status-report";
|
||||
import { ToolsFeature } from "./tools-features";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
@@ -232,6 +234,7 @@ export function initAllState(
|
||||
startedAt: new Date(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
osRelease: os.release(),
|
||||
logger: new RecordingLogger(),
|
||||
env,
|
||||
actions: getTestActionsEnv(env),
|
||||
@@ -870,7 +873,7 @@ export function mockLanguagesInRepo(languages: string[]) {
|
||||
*/
|
||||
export const makeVersionInfo = (
|
||||
version: string,
|
||||
features?: { [name: string]: boolean },
|
||||
features?: { [key in ToolsFeature]?: boolean },
|
||||
overlayVersion?: number,
|
||||
): VersionInfo => ({
|
||||
version,
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum ToolsFeature {
|
||||
BundleSupportsOverlay = "bundleSupportsOverlay",
|
||||
IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries",
|
||||
SuppressesMissingFileBaselineWarning = "suppressesMissingFileBaselineWarning",
|
||||
SwiftSupportsAllPlatforms = "swiftSupportsAllPlatforms",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
12
src/util.ts
12
src/util.ts
@@ -12,6 +12,7 @@ import * as semver from "semver";
|
||||
|
||||
import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL } from "./codeql";
|
||||
import type { ActionConfig } from "./config/action-config";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import {
|
||||
@@ -445,7 +446,10 @@ export function getThreadsFlag(
|
||||
/**
|
||||
* Get the path where the CodeQL database for the given language lives.
|
||||
*/
|
||||
export function getCodeQLDatabasePath(config: Config, language: Language) {
|
||||
export function getCodeQLDatabasePath(
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
) {
|
||||
return path.resolve(config.dbLocation, language);
|
||||
}
|
||||
|
||||
@@ -639,7 +643,9 @@ export async function codeQlVersionAtLeast(
|
||||
return semver.gte((await codeql.getVersion()).version, requiredVersion);
|
||||
}
|
||||
|
||||
export function getBaseDatabaseOidsFilePath(config: Config): string {
|
||||
export function getBaseDatabaseOidsFilePath(
|
||||
config: ActionConfig<"dbLocation">,
|
||||
): string {
|
||||
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
|
||||
}
|
||||
|
||||
@@ -651,7 +657,7 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
|
||||
* given `includeDiagnostics` value.
|
||||
*/
|
||||
export async function bundleDb(
|
||||
config: Config,
|
||||
config: ActionConfig<"dbLocation">,
|
||||
language: Language,
|
||||
codeql: CodeQL,
|
||||
dbName: string,
|
||||
|
||||
Reference in New Issue
Block a user