mirror of
https://github.com/github/codeql-action.git
synced 2026-11-05 20:49:49 +00:00
Merge branch 'main' into mbg/pr-dequeue-notify
This commit is contained in:
143
.github/workflows/__diagnostics-without-db.yml
generated
vendored
Normal file
143
.github/workflows/__diagnostics-without-db.yml
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
# 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:
|
||||
#
|
||||
# - Produce a diagnostic before the db is initialised by forcing the use of a nightly release
|
||||
# with the `CODEQL_ACTION_FORCE_NIGHTLY` feature flag.
|
||||
# - Cause `init` to abort 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@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.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: Analyze
|
||||
uses: ./analyze
|
||||
# In a real workflow, this step wouldn't run. Since we used `continue-on-error`
|
||||
# above, we manually disable it with an `if` condition. Having this step is required
|
||||
# for the init post-action because it checks the workflow definition for e.g. the
|
||||
# `category` input. Without this step, that part of the init post-action would fail.
|
||||
if: false
|
||||
with:
|
||||
category: '/test-codeql-version:${{ matrix.version }}'
|
||||
|
||||
- name: Check diagnostics appear on disk
|
||||
env:
|
||||
DIAGNOSTICS_DIR: '${{ runner.temp }}/codeql-action-temp-diagnostics/'
|
||||
run: |
|
||||
cd ../action
|
||||
npx tsx ./pr-checks/diagnostics-without-db.ts
|
||||
env:
|
||||
CODEQL_ACTION_SKIP_GENERATE_FAILED_SARIF: false
|
||||
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
|
||||
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
diagnostics-without-db-validation:
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
needs:
|
||||
- diagnostics-without-db
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.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: Download workflow artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: test-mode-sarif-*
|
||||
path: sarif
|
||||
|
||||
- name: List artifact contents
|
||||
run: ls -laR ./sarif
|
||||
|
||||
- name: Check diagnostics appear in SARIF
|
||||
env:
|
||||
SARIF_PATH: "./sarif/codeql-failed-run.sarif"
|
||||
DIAGNOSTIC_ID: "codeql-action/forced-nightly-cli"
|
||||
DIAGNOSTIC_NAME: "A nightly release of CodeQL was used"
|
||||
EXPECTED_LOCATIONS: "0"
|
||||
EXPECTED_MESSAGE: |
|
||||
GitHub configured this analysis to use a nightly release of CodeQL to allow you to preview changes from an upcoming release.
|
||||
run: npx tsx ./pr-checks/check-diagnostics.ts
|
||||
env:
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
6
.github/workflows/__export-file-baseline-information.yml
generated
vendored
6
.github/workflows/__export-file-baseline-information.yml
generated
vendored
@@ -91,6 +91,11 @@ jobs:
|
||||
with:
|
||||
languages: javascript
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
# To balance speed and coverage, analyze only JavaScript but use the combined bundle so
|
||||
# baseline information is reported for every language in the multi-language repository. A
|
||||
# custom configuration prevents per-language bundles from being used.
|
||||
config: |
|
||||
paths-ignore: []
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
@@ -124,5 +129,4 @@ jobs:
|
||||
env:
|
||||
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
|
||||
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
|
||||
39
.github/workflows/__per-language-bundle-validation.yml
generated
vendored
39
.github/workflows/__per-language-bundle-validation.yml
generated
vendored
@@ -36,40 +36,40 @@ jobs:
|
||||
include:
|
||||
- language: actions
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
expected-extractors: actions javascript
|
||||
- language: cpp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: manual
|
||||
build-command: gcc -o main main.c
|
||||
- language: csharp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: none
|
||||
- language: go
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: autobuild
|
||||
- language: java
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: none
|
||||
- language: javascript
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: python
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: ruby
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: rust
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: swift
|
||||
os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: autobuild
|
||||
name: Per-language bundles
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
@@ -81,6 +81,15 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.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
|
||||
@@ -88,6 +97,10 @@ jobs:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Remove CodeQL from toolcache
|
||||
run: |
|
||||
cd ./../action
|
||||
npx tsx ./pr-checks/toolcache.ts clear
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
with:
|
||||
@@ -128,9 +141,8 @@ jobs:
|
||||
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
|
||||
run: |
|
||||
# A bundle that is missing most of its extractors must never be left in the toolcache,
|
||||
# where a later job analyzing a different language could pick it up. The runner image
|
||||
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
|
||||
# rather than whether the toolcache contains CodeQL at all.
|
||||
# where a later job analyzing a different language could pick it up. Per-language bundles
|
||||
# must instead be extracted under the runner's temporary directory.
|
||||
echo "CodeQL is at $CODEQL_PATH"
|
||||
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
|
||||
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
|
||||
@@ -160,5 +172,4 @@ jobs:
|
||||
fi
|
||||
echo "Created a ${LANGUAGE} database at ${database}."
|
||||
env:
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
|
||||
4
.github/workflows/__submit-sarif-failure.yml
generated
vendored
4
.github/workflows/__submit-sarif-failure.yml
generated
vendored
@@ -71,7 +71,9 @@ jobs:
|
||||
run: exit 1
|
||||
- uses: ./analyze
|
||||
# In a real workflow, this step wouldn't run. Since we used `continue-on-error`
|
||||
# above, we manually disable it with an `if` condition.
|
||||
# above, we manually disable it with an `if` condition. Having this step is required
|
||||
# for the init post-action because it checks the workflow definition for e.g. the
|
||||
# `category` input. Without this step, that part of the init post-action would fail.
|
||||
if: false
|
||||
with:
|
||||
category: '/test-codeql-version:${{ matrix.version }}'
|
||||
|
||||
1012
lib/entry-points.js
generated
1012
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,32 @@ import * as core from "@actions/core";
|
||||
|
||||
import * as sarif from "../src/sarif";
|
||||
|
||||
function checkStatusPageNotification(n) {
|
||||
const expectedMessage = "Plaintext message";
|
||||
if (n.message.text !== expectedMessage) {
|
||||
core.setFailed(
|
||||
`Expected the status page diagnostic to have the message '${expectedMessage}', but found '${n.message.text}'.`,
|
||||
);
|
||||
const DIAGNOSTIC_ID =
|
||||
process.env["DIAGNOSTIC_ID"] ?? "lang/diagnostics/example";
|
||||
const DIAGNOSTIC_NAME = process.env["DIAGNOSTIC_NAME"] ?? "Diagnostic name";
|
||||
const EXPECTED_MESSAGE = process.env["EXPECTED_MESSAGE"] ?? "Plaintext message";
|
||||
const EXPECTED_LOCATIONS = Number.parseInt(
|
||||
process.env["EXPECTED_LOCATIONS"] ?? "1",
|
||||
);
|
||||
|
||||
function checkStatusPageNotification(n: sarif.Notification) {
|
||||
const expectedMessage = EXPECTED_MESSAGE.trim();
|
||||
if (!n.message.text?.includes(expectedMessage)) {
|
||||
if (n.message.text !== undefined) {
|
||||
core.setFailed(
|
||||
`Expected the status page diagnostic to contain the message '${expectedMessage}', but found '${n.message.text.replaceAll("\n", "\\n")}'.`,
|
||||
);
|
||||
} else {
|
||||
core.setFailed(
|
||||
`Expected the status page diagnostic to contain the message '${expectedMessage}', but found nothing.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (n.locations.length !== 1) {
|
||||
|
||||
const locationCount = n.locations?.length ?? 0;
|
||||
if (locationCount !== EXPECTED_LOCATIONS) {
|
||||
core.setFailed(
|
||||
`Expected the status page diagnostic to have exactly 1 location, but found ${n.locations.length}.`,
|
||||
`Expected the status page diagnostic to have exactly ${EXPECTED_LOCATIONS} location(s), but found ${locationCount}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -40,8 +56,7 @@ if (toolExecutionNotifications === undefined) {
|
||||
|
||||
const statusPageNotifications = toolExecutionNotifications.filter(
|
||||
(n) =>
|
||||
n.descriptor?.id === "lang/diagnostics/example" &&
|
||||
n.properties?.visibility?.statusPage,
|
||||
n.descriptor?.id === DIAGNOSTIC_ID && n.properties?.visibility?.statusPage,
|
||||
);
|
||||
if (statusPageNotifications.length !== 1) {
|
||||
core.setFailed(
|
||||
@@ -61,9 +76,9 @@ if (notifications === undefined) {
|
||||
|
||||
const diagnosticNotification = notifications.filter(
|
||||
(n) =>
|
||||
n.id === "lang/diagnostics/example" &&
|
||||
n.name === "lang/diagnostics/example" &&
|
||||
n.fullDescription?.text === "Diagnostic name",
|
||||
n.id === DIAGNOSTIC_ID &&
|
||||
n.name === DIAGNOSTIC_ID &&
|
||||
n.fullDescription?.text === DIAGNOSTIC_NAME,
|
||||
);
|
||||
if (diagnosticNotification.length !== 1) {
|
||||
core.setFailed(
|
||||
|
||||
72
pr-checks/checks/diagnostics-without-db.yml
Normal file
72
pr-checks/checks/diagnostics-without-db.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
name: "Diagnostic export without database"
|
||||
description: "Tests that temporary diagnostics are correctly included in the failed SARIF."
|
||||
versions:
|
||||
# Overridden by the FF in the `init` step below.
|
||||
- linked
|
||||
installNode: true
|
||||
|
||||
env:
|
||||
# Allow the failed SARIF file to be generated.
|
||||
CODEQL_ACTION_SKIP_GENERATE_FAILED_SARIF: false
|
||||
# But don't actually upload it.
|
||||
CODEQL_ACTION_SKIP_SARIF_UPLOAD: true
|
||||
# Mark telemetry for this workflow so it can be treated separately.
|
||||
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
|
||||
|
||||
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:
|
||||
#
|
||||
# - Produce a diagnostic before the db is initialised by forcing the use of a nightly release
|
||||
# with the `CODEQL_ACTION_FORCE_NIGHTLY` feature flag.
|
||||
# - Cause `init` to abort 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: Analyze
|
||||
uses: ./analyze
|
||||
# In a real workflow, this step wouldn't run. Since we used `continue-on-error`
|
||||
# above, we manually disable it with an `if` condition. Having this step is required
|
||||
# for the init post-action because it checks the workflow definition for e.g. the
|
||||
# `category` input. Without this step, that part of the init post-action would fail.
|
||||
if: false
|
||||
with:
|
||||
category: "/test-codeql-version:${{ matrix.version }}"
|
||||
|
||||
- name: Check diagnostics appear on disk
|
||||
env:
|
||||
DIAGNOSTICS_DIR: "${{ runner.temp }}/codeql-action-temp-diagnostics/"
|
||||
run: |
|
||||
cd ../action
|
||||
npx tsx ./pr-checks/diagnostics-without-db.ts
|
||||
validationJobs:
|
||||
diagnostics-without-db-validation:
|
||||
checkout: true
|
||||
installNode: true
|
||||
steps:
|
||||
- name: Download workflow artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: test-mode-sarif-*
|
||||
path: sarif
|
||||
|
||||
- name: List artifact contents
|
||||
run: ls -laR ./sarif
|
||||
|
||||
- name: Check diagnostics appear in SARIF
|
||||
env:
|
||||
SARIF_PATH: "./sarif/codeql-failed-run.sarif"
|
||||
DIAGNOSTIC_ID: "codeql-action/forced-nightly-cli"
|
||||
DIAGNOSTIC_NAME: "A nightly release of CodeQL was used"
|
||||
EXPECTED_LOCATIONS: "0"
|
||||
EXPECTED_MESSAGE: |
|
||||
GitHub configured this analysis to use a nightly release of CodeQL to allow you to preview changes from an upcoming release.
|
||||
run: npx tsx ./pr-checks/check-diagnostics.ts
|
||||
@@ -11,16 +11,17 @@ installDotNet: true
|
||||
env:
|
||||
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
|
||||
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
|
||||
# To balance speed and coverage, we analyze only a single language (JavaScript), but use the
|
||||
# combined bundle so we can test that baseline information is reported for each language in the
|
||||
# multi-language source directory.
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
with:
|
||||
languages: javascript
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
# To balance speed and coverage, analyze only JavaScript but use the combined bundle so
|
||||
# baseline information is reported for every language in the multi-language repository. A
|
||||
# custom configuration prevents per-language bundles from being used.
|
||||
config: |
|
||||
paths-ignore: []
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
name: Per-language bundles
|
||||
description: Validates extraction and analysis using each per-language CodeQL bundle.
|
||||
# TODO: Use a released bundle once releases include per-language bundles.
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
# Actions also needs the JavaScript extractor.
|
||||
expected-extractors: actions javascript
|
||||
- language: cpp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: manual
|
||||
build-command: gcc -o main main.c
|
||||
- language: csharp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: none
|
||||
- language: go
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: autobuild
|
||||
- language: java
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: none
|
||||
- language: javascript
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: python
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: ruby
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: rust
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
- language: swift
|
||||
os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
version: linked
|
||||
build-mode: autobuild
|
||||
env:
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
|
||||
installNode: true
|
||||
steps:
|
||||
- name: Remove CodeQL from toolcache
|
||||
run: |
|
||||
cd ./../action
|
||||
npx tsx ./pr-checks/toolcache.ts clear
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
with:
|
||||
@@ -84,9 +86,8 @@ steps:
|
||||
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
|
||||
run: |
|
||||
# A bundle that is missing most of its extractors must never be left in the toolcache,
|
||||
# where a later job analyzing a different language could pick it up. The runner image
|
||||
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
|
||||
# rather than whether the toolcache contains CodeQL at all.
|
||||
# where a later job analyzing a different language could pick it up. Per-language bundles
|
||||
# must instead be extracted under the runner's temporary directory.
|
||||
echo "CodeQL is at $CODEQL_PATH"
|
||||
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
|
||||
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
|
||||
|
||||
@@ -35,7 +35,9 @@ steps:
|
||||
run: exit 1
|
||||
- uses: ./analyze
|
||||
# In a real workflow, this step wouldn't run. Since we used `continue-on-error`
|
||||
# above, we manually disable it with an `if` condition.
|
||||
# above, we manually disable it with an `if` condition. Having this step is required
|
||||
# for the init post-action because it checks the workflow definition for e.g. the
|
||||
# `category` input. Without this step, that part of the init post-action would fail.
|
||||
if: false
|
||||
with:
|
||||
category: "/test-codeql-version:${{ matrix.version }}"
|
||||
|
||||
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 diagnosticDirectory = process.env["DIAGNOSTICS_DIR"];
|
||||
|
||||
if (diagnosticDirectory === undefined) {
|
||||
throw new Error("DIAGNOSTICS_DIR is undefined.");
|
||||
}
|
||||
|
||||
const paths = fs
|
||||
.readdirSync(diagnosticDirectory, {
|
||||
recursive: true,
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.filter((p) => p.endsWith(".json"));
|
||||
|
||||
core.info(`Found ${paths.length} paths in '${diagnosticDirectory}'.`);
|
||||
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(diagnosticDirectory, 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.`);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ interface Specification extends JobSpecification {
|
||||
services?: any;
|
||||
|
||||
/** Additional jobs to run after the main PR check job. */
|
||||
validationJobs?: Record<string, JobSpecification>;
|
||||
validationJobs?: Record<string, ValidationJobSpecification>;
|
||||
|
||||
/** If set, this check is part of a named collection that gets its own caller workflow. */
|
||||
collection?: string;
|
||||
@@ -122,6 +122,10 @@ interface JobSpecification {
|
||||
installYq?: boolean;
|
||||
}
|
||||
|
||||
type ValidationJobSpecification = JobSpecification & {
|
||||
checkout?: boolean;
|
||||
};
|
||||
|
||||
/** Describes language/framework-specific steps and inputs. */
|
||||
interface LanguageSetup {
|
||||
specProperty: keyof JobSpecification;
|
||||
@@ -462,6 +466,20 @@ function generateJobMatrix(
|
||||
return matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the `actions/checkout` step.
|
||||
*/
|
||||
function getCheckoutStep(): Step {
|
||||
return {
|
||||
name: "Check out repository",
|
||||
uses: pinnedUses(
|
||||
"actions/checkout",
|
||||
"3d3c42e5aac5ba805825da76410c181273ba90b1",
|
||||
"v7.0.1",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves setup steps and additional input definitions based on specific languages or frameworks
|
||||
* that are requested by the `checkSpecification`.
|
||||
@@ -531,14 +549,7 @@ function generateJob(
|
||||
|
||||
// Construct the workflow steps needed for this check.
|
||||
const steps: Step[] = [
|
||||
{
|
||||
name: "Check out repository",
|
||||
uses: pinnedUses(
|
||||
"actions/checkout",
|
||||
"3d3c42e5aac5ba805825da76410c181273ba90b1",
|
||||
"v7.0.1",
|
||||
),
|
||||
},
|
||||
getCheckoutStep(),
|
||||
...setupInfo.steps,
|
||||
{
|
||||
name: "Prepare test",
|
||||
@@ -609,13 +620,16 @@ function generateJob(
|
||||
/** Generates a validation job. */
|
||||
function generateValidationJob(
|
||||
specDocument: yaml.Document,
|
||||
jobSpecification: JobSpecification,
|
||||
jobSpecification: ValidationJobSpecification,
|
||||
checkName: string,
|
||||
name: string,
|
||||
) {
|
||||
// Determine which languages or frameworks have to be installed.
|
||||
const { inputs, steps } = getSetupSteps(jobSpecification);
|
||||
|
||||
// Construct a checkout step if requested.
|
||||
const checkoutStep = jobSpecification.checkout ? [getCheckoutStep()] : [];
|
||||
|
||||
// Extract the sequence of steps from the YAML document to persist as much formatting as possible.
|
||||
const specSteps = specDocument.getIn([
|
||||
"validationJobs",
|
||||
@@ -624,7 +638,7 @@ function generateValidationJob(
|
||||
]) as yaml.YAMLSeq;
|
||||
|
||||
// Add the generated steps in front of the ones from the specification.
|
||||
specSteps.items.unshift(...steps);
|
||||
specSteps.items.unshift(...checkoutStep, ...steps);
|
||||
|
||||
const validationJob: Record<string, any> = {
|
||||
name: jobSpecification.name,
|
||||
@@ -635,7 +649,7 @@ function generateValidationJob(
|
||||
"security-events": "read",
|
||||
},
|
||||
"timeout-minutes": 5,
|
||||
"runs-on": "ubuntu-slim",
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: specSteps,
|
||||
};
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ export interface CodeQL {
|
||||
sarifFile: string,
|
||||
automationDetailsId: string | undefined,
|
||||
config: ActionConfig<"tempDir">,
|
||||
diagnosticDir: string | undefined,
|
||||
): Promise<void>;
|
||||
/** Get the location of an extractor for the specified language. */
|
||||
resolveExtractor(language: Language): Promise<string>;
|
||||
@@ -903,15 +904,43 @@ export async function getCodeQLForCmd(
|
||||
sarifFile: string,
|
||||
automationDetailsId: string | undefined,
|
||||
config: ActionConfig<"tempDir">,
|
||||
diagnosticDir: string | undefined,
|
||||
): Promise<void> {
|
||||
// We may be exporting diagnostics for a failed SARIF, in which case the configuration
|
||||
// file may not have been written to disk and there may not be a database. The CLI
|
||||
// will produce a SARIF for diagnostics from `diagnosticDir` without it. Therefore,
|
||||
// we check whether `configPath` exists and only provide it to the CLI if it does.
|
||||
// Although not needed, the CLI will produce an error if it
|
||||
// is provided with an argument to `--sarif-codescanning-config` that does not exist.
|
||||
let configPath: string | undefined =
|
||||
getGeneratedCodeScanningConfigPath(config);
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
// Warn if there is no configuration file, even though `config` claims that `init`
|
||||
// was successful. This shouldn't be happening.
|
||||
if (config.successfullyInitialised) {
|
||||
logger.warning(
|
||||
[
|
||||
`Expected a configuration file at '${configPath}', which does not exist.`,
|
||||
"Continuing with diagnostics export anyway, without a configuration file.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
configPath = undefined;
|
||||
}
|
||||
|
||||
const args = [
|
||||
"diagnostics",
|
||||
"export",
|
||||
"--format=sarif-latest",
|
||||
`--output=${sarifFile}`,
|
||||
`--sarif-codescanning-config=${getGeneratedCodeScanningConfigPath(
|
||||
config,
|
||||
)}`,
|
||||
...(configPath !== undefined
|
||||
? [`--sarif-codescanning-config=${configPath}`]
|
||||
: []),
|
||||
...(diagnosticDir !== undefined
|
||||
? [`--diagnostic-dir=${diagnosticDir}`]
|
||||
: []),
|
||||
...getExtraOptionsFromEnv(["diagnostics", "export"]),
|
||||
];
|
||||
if (automationDetailsId !== undefined) {
|
||||
|
||||
@@ -465,6 +465,8 @@ export async function initActionState(
|
||||
overlayModeSetExplicitly: false,
|
||||
repositoryProperties,
|
||||
enableFileCoverageInformation,
|
||||
// We don't set this to `true` until `init` is successful.
|
||||
successfullyInitialised: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,9 @@ export interface Config {
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
|
||||
/** Whether the analysis was successfully initialised using the configuration. */
|
||||
successfullyInitialised: boolean;
|
||||
}
|
||||
|
||||
/** Properties available in `Config`. */
|
||||
|
||||
@@ -8,8 +8,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";
|
||||
|
||||
@@ -561,69 +562,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 {
|
||||
|
||||
165
src/diagnostics.test.ts
Normal file
165
src/diagnostics.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
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);
|
||||
});
|
||||
|
||||
test("getLanguageDiagnosticsPath gets language-specific path", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
for (const language of Object.values(BuiltInLanguage)) {
|
||||
t.is(
|
||||
diagnostics.getLanguageDiagnosticsPath(tmpDir, language),
|
||||
path.resolve(
|
||||
tmpDir,
|
||||
language,
|
||||
...diagnostics.CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS,
|
||||
),
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
test("getTempLanguageDiagnosticsPath returns path without language", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
t.is(
|
||||
diagnostics.getTempLanguageDiagnosticsPath(tmpDir),
|
||||
path.resolve(
|
||||
tmpDir,
|
||||
diagnostics.TEMP_DIAGNOSTIC_DIR_NAME,
|
||||
...diagnostics.CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS,
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
test("getTempLanguageDiagnosticsPath returns language-specific path if a language is provided", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
t.is(
|
||||
diagnostics.getTempLanguageDiagnosticsPath(tmpDir, BuiltInLanguage.swift),
|
||||
path.resolve(
|
||||
tmpDir,
|
||||
diagnostics.TEMP_DIAGNOSTIC_DIR_NAME,
|
||||
BuiltInLanguage.swift,
|
||||
...diagnostics.CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS,
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
test("getDatabaseDiagnosticsPath returns language-specific path", async (t) =>
|
||||
withTmpDir(async (tmpDir) => {
|
||||
t.is(
|
||||
diagnostics.getDatabaseDiagnosticsPath(
|
||||
createTestConfig({ dbLocation: tmpDir }),
|
||||
BuiltInLanguage.swift,
|
||||
),
|
||||
path.resolve(
|
||||
tmpDir,
|
||||
BuiltInLanguage.swift,
|
||||
...diagnostics.CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS,
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
/**
|
||||
* 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 ${path.join(testDbDir, "actions")} does not exist yet`,
|
||||
"Moving 1 diagnostic(s) to their respective databases.",
|
||||
]);
|
||||
}));
|
||||
@@ -1,10 +1,24 @@
|
||||
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,
|
||||
isErrnoException,
|
||||
} from "./util";
|
||||
|
||||
/** The name of the directory we use to temporarily store diagnostics. */
|
||||
export const TEMP_DIAGNOSTIC_DIR_NAME = "codeql-action-temp-diagnostics";
|
||||
|
||||
export const CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS = [
|
||||
"diagnostic",
|
||||
"codeql-action",
|
||||
];
|
||||
|
||||
/**
|
||||
* Known tags for diagnostics. There is currently only "internal-error",
|
||||
@@ -84,22 +98,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 +136,51 @@ export function makeDiagnostic(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a path relative to {@link basePath} where diagnostics for {@link language} should be stored at.
|
||||
*/
|
||||
export function getLanguageDiagnosticsPath(
|
||||
basePath: string,
|
||||
language: Language,
|
||||
) {
|
||||
return path.resolve(
|
||||
basePath,
|
||||
language,
|
||||
...CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a path relative to {@link tmpDir} where diagnostics should temporarily be stored
|
||||
* until they can be moved to the database.
|
||||
*/
|
||||
export function getTempDiagnosticsRoot(tmpDir: string): string {
|
||||
return path.resolve(tmpDir, TEMP_DIAGNOSTIC_DIR_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a non-language-specific path is returned.
|
||||
*/
|
||||
export function getTempLanguageDiagnosticsPath(
|
||||
tmpDir: string,
|
||||
language?: Language,
|
||||
) {
|
||||
const root = getTempDiagnosticsRoot(tmpDir);
|
||||
|
||||
if (language !== undefined) {
|
||||
return getLanguageDiagnosticsPath(root, language);
|
||||
}
|
||||
|
||||
return path.resolve(root, ...CODEQL_ACTION_DIAGNOSTIC_SUBDIR_PARTS);
|
||||
}
|
||||
|
||||
/** Gets the path where diagnostics for {@link language} should be stored in the database. */
|
||||
export function getDatabaseDiagnosticsPath(config: Config, language: Language) {
|
||||
return getLanguageDiagnosticsPath(config.dbLocation, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +193,23 @@ 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);
|
||||
// store the diagnostic in a temporary location and move it to the database later.
|
||||
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.
|
||||
writeTemporaryDiagnosticFile(logger, config.tempDir, language, diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +217,7 @@ export function addDiagnostic(
|
||||
export function addNoLanguageDiagnostic(
|
||||
config: Config | undefined,
|
||||
diagnostic: DiagnosticMessage,
|
||||
logger: Logger = getActionsLogger(),
|
||||
) {
|
||||
if (config !== undefined) {
|
||||
addDiagnostic(
|
||||
@@ -173,10 +228,74 @@ export function addNoLanguageDiagnostic(
|
||||
diagnostic,
|
||||
);
|
||||
} else {
|
||||
unwrittenDefaultLanguageDiagnostics.push(diagnostic);
|
||||
logger.debug(
|
||||
`Writing a diagnostic not associated with any language, but we have no configuration to tell us where the database is yet.`,
|
||||
);
|
||||
|
||||
// Write the diagnostic to a temporary location.
|
||||
writeTemporaryDiagnosticFile(
|
||||
logger,
|
||||
getTemporaryDirectory(),
|
||||
undefined,
|
||||
diagnostic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes {@link diagnostic} for an optional {@link language} to a subdirectory of {@link tempDir}
|
||||
* and tracks it as a diagnostic that was written to a temporary location.
|
||||
*/
|
||||
function writeTemporaryDiagnosticFile(
|
||||
logger: Logger,
|
||||
tempDir: string,
|
||||
language: Language | undefined,
|
||||
diagnostic: DiagnosticMessage,
|
||||
) {
|
||||
const tempDiagnosticsPath = getTempLanguageDiagnosticsPath(tempDir, language);
|
||||
|
||||
try {
|
||||
// Write the diagnostic to a temporary location.
|
||||
const diagnosticPath = writeDiagnosticFile(tempDiagnosticsPath, diagnostic);
|
||||
|
||||
// Track the temporary file.
|
||||
temporaryDiagnostics.push({ path: diagnosticPath, language });
|
||||
} catch (err) {
|
||||
logger.warning(
|
||||
`Unable to write temporary diagnostic to '${tempDiagnosticsPath}': ${getErrorMessage(err)}\n\n${JSON.stringify(diagnostic)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given diagnostic to the database.
|
||||
*
|
||||
@@ -185,78 +304,94 @@ export function addNoLanguageDiagnostic(
|
||||
* @param diagnostic The diagnostic message to add to the database.
|
||||
*/
|
||||
function writeDiagnostic(
|
||||
action: ActionState<["Logger"]>,
|
||||
config: Config,
|
||||
language: Language | undefined,
|
||||
language: Language,
|
||||
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;
|
||||
if (num > 0) {
|
||||
logger.warning(
|
||||
`${num} diagnostic(s) could not be written to the database and will not appear on the Tool Status Page.`,
|
||||
/** 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 && action.logger.isDebug()) {
|
||||
action.logger.startGroup("Diagnostics in temporary location");
|
||||
action.logger.debug(
|
||||
`${num} diagnostic(s) were not moved to the database and remain in the temporary directory.`,
|
||||
);
|
||||
|
||||
for (const unwritten of unwrittenDiagnostics) {
|
||||
logger.debug(JSON.stringify(unwritten.diagnostic));
|
||||
for (const temporary of temporaryDiagnostics) {
|
||||
try {
|
||||
action.logger.debug(fs.readFileSync(temporary.path, "utf-8"));
|
||||
} catch (err) {
|
||||
action.logger.debug(
|
||||
`Failed to read diagnostics from temporary location at '${temporary.path}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
action.logger.endGroup();
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 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);
|
||||
|
||||
try {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
try {
|
||||
fs.renameSync(temporary.path, destination);
|
||||
} catch (renameErr: unknown) {
|
||||
// Rethrow the error if it isn't an EXDEV one.
|
||||
if (!isErrnoException(renameErr) || renameErr.code !== "EXDEV") {
|
||||
throw renameErr;
|
||||
}
|
||||
|
||||
// The error was an EXDEV one: try to copy the file and delete the temporary one.
|
||||
fs.copyFileSync(temporary.path, destination);
|
||||
fs.unlinkSync(temporary.path);
|
||||
}
|
||||
} catch (err) {
|
||||
action.logger.warning(
|
||||
`Failed to relocate '${temporary.path}' to '${destination}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
|
||||
failedFlushDiagnostics.push(temporary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes all unwritten diagnostics to disk. */
|
||||
export function flushDiagnostics(config: Config) {
|
||||
const logger = getActionsLogger();
|
||||
|
||||
const diagnosticsCount =
|
||||
unwrittenDiagnostics.length + unwrittenDefaultLanguageDiagnostics.length;
|
||||
logger.debug(`Writing ${diagnosticsCount} diagnostic(s) to database.`);
|
||||
|
||||
for (const unwritten of unwrittenDiagnostics) {
|
||||
writeDiagnostic(config, unwritten.language, unwritten.diagnostic);
|
||||
}
|
||||
for (const unwritten of unwrittenDefaultLanguageDiagnostics) {
|
||||
addNoLanguageDiagnostic(config, unwritten);
|
||||
}
|
||||
|
||||
// Reset the unwritten diagnostics arrays.
|
||||
unwrittenDiagnostics = [];
|
||||
unwrittenDefaultLanguageDiagnostics = [];
|
||||
// Reset the temporary diagnostics arrays.
|
||||
temporaryDiagnostics = failedFlushDiagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -143,11 +143,6 @@ export enum Feature {
|
||||
OverlayAnalysisStatusCheck = "overlay_analysis_status_check",
|
||||
/** Controls whether overlay build failures on the default branch are stored in the Actions cache. */
|
||||
OverlayAnalysisStatusSave = "overlay_analysis_status_save",
|
||||
/**
|
||||
* Controls whether we may download a bundle containing only the single language being analysed,
|
||||
* rather than the combined bundle that contains every language.
|
||||
*/
|
||||
PerLanguageBundles = "per_language_bundles_v2",
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
@@ -393,11 +388,6 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SKIP_RESOURCE_CHECKS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.PerLanguageBundles]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_PER_LANGUAGE_BUNDLES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.QaTelemetryEnabled]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_QA_TELEMETRY",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import * as sinon from "sinon";
|
||||
@@ -8,6 +11,7 @@ import * as apiClient from "./api-client";
|
||||
import * as codeql from "./codeql";
|
||||
import * as configUtils from "./config-utils";
|
||||
import * as debugArtifacts from "./debug-artifacts";
|
||||
import { getTempDiagnosticsRoot } from "./diagnostics";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import * as initActionPostHelper from "./init-action-post-helper";
|
||||
@@ -35,6 +39,47 @@ const NUM_BYTES_PER_GIB = 1024 * 1024 * 1024;
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial("generateFailedSarif picks up unwritten diagnostics", async (t) =>
|
||||
util.withTmpDir(async (tmpDir) => {
|
||||
const logger = new RecordingLogger();
|
||||
|
||||
const tempDiagDir = getTempDiagnosticsRoot(tmpDir);
|
||||
fs.mkdirSync(tempDiagDir, { recursive: true });
|
||||
|
||||
const codeqlObject = await getCodeQLForTesting();
|
||||
const diagnosticsExport = sinon
|
||||
.stub(codeqlObject, "diagnosticsExport")
|
||||
.resolves();
|
||||
|
||||
sinon.stub(codeql, "getCodeQL").resolves(codeqlObject);
|
||||
|
||||
const sarifFile = path.join(tmpDir, "failed.sarif");
|
||||
const result = await initActionPostHelper.generateFailedSarif(
|
||||
logger,
|
||||
createFeatures([Feature.ExportDiagnosticsEnabled]),
|
||||
createTestConfig({
|
||||
tempDir: tmpDir,
|
||||
// Explicitly undefined to ensure we get the "no database" path.
|
||||
dbLocation: undefined,
|
||||
}),
|
||||
"category",
|
||||
"",
|
||||
sarifFile,
|
||||
);
|
||||
|
||||
t.truthy(result);
|
||||
t.is(diagnosticsExport.callCount, 1);
|
||||
t.true(
|
||||
diagnosticsExport.calledOnceWith(
|
||||
sarifFile,
|
||||
"category",
|
||||
sinon.match.any,
|
||||
tempDiagDir,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
test.serial("init-post action with debug mode off", async (t) => {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type UploadDebugArtifactsConfig,
|
||||
} from "./debug-artifacts";
|
||||
import * as dependencyCaching from "./dependency-caching";
|
||||
import { getTempDiagnosticsRoot } from "./diagnostics";
|
||||
import { EnvVar, ReadOnlyEnv } from "./environment";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
@@ -177,7 +178,16 @@ async function prepareFailedSarif(
|
||||
}
|
||||
}
|
||||
|
||||
async function generateFailedSarif(
|
||||
/**
|
||||
* Generates a SARIF file for a failed run, i.e. an analysis where no SARIF was produced as a result of the analysis.
|
||||
*
|
||||
* Not intended to be called directly, other than by `prepareFailedSarif`.
|
||||
*
|
||||
* @param category The analysis category to include in the SARIF.
|
||||
* @param checkoutPath The path where the repository was checked out at.
|
||||
* @param sarifFile The SARIF file to produce.
|
||||
*/
|
||||
export async function generateFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: ActionConfig<"codeQLCmd" | "tempDir">,
|
||||
@@ -194,11 +204,26 @@ async function generateFailedSarif(
|
||||
}
|
||||
|
||||
// If there is no database or the feature flag is off, we run 'export diagnostics'
|
||||
if (
|
||||
databasePath === undefined ||
|
||||
!(await features.getValue(Feature.ExportDiagnosticsEnabled, codeql))
|
||||
) {
|
||||
await codeql.diagnosticsExport(sarifFile, category, config);
|
||||
const exportDiagnosticsEnabled = await features.getValue(
|
||||
Feature.ExportDiagnosticsEnabled,
|
||||
codeql,
|
||||
);
|
||||
if (databasePath === undefined || !exportDiagnosticsEnabled) {
|
||||
// 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.
|
||||
// We also don't pass the path to the CLI if `exportDiagnosticsEnabled` is not enabled, to preserve
|
||||
// its intended behaviour of controlling whether diagnostics are included in the SARIF or not.
|
||||
let diagnosticDir: string | undefined = getTempDiagnosticsRoot(
|
||||
config.tempDir,
|
||||
);
|
||||
|
||||
if (!exportDiagnosticsEnabled || !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);
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
addDiagnostic,
|
||||
addNoLanguageDiagnostic,
|
||||
flushDiagnostics,
|
||||
logUnwrittenDiagnostics,
|
||||
logTemporaryDiagnostics,
|
||||
makeDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "./diagnostics";
|
||||
@@ -484,6 +484,7 @@ async function run(
|
||||
if (statusReportBase !== undefined) {
|
||||
await sendStatusReport(statusReportBase);
|
||||
}
|
||||
logTemporaryDiagnostics(actionState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -766,14 +767,15 @@ 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
|
||||
// `OverlayDatabaseMode.None` if we failed to download an overlay-base
|
||||
// database.
|
||||
config.successfullyInitialised = true;
|
||||
await configUtils.saveConfig(config, logger);
|
||||
|
||||
core.setOutput("codeql-path", config.codeQLCmd);
|
||||
@@ -797,7 +799,7 @@ async function run(
|
||||
);
|
||||
return;
|
||||
} finally {
|
||||
logUnwrittenDiagnostics();
|
||||
logTemporaryDiagnostics(actionState);
|
||||
}
|
||||
await sendCompletedStatusReport(
|
||||
startedAt,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import test from "ava";
|
||||
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import {
|
||||
getOtherLanguagePacksReason,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
} from "./per-language-bundles";
|
||||
import { BundlePlatform } from "./platform";
|
||||
import {
|
||||
createFeatures,
|
||||
getRecordingLogger,
|
||||
getTestEnv,
|
||||
initAllState,
|
||||
@@ -40,7 +38,6 @@ async function checkEligibility(
|
||||
env: getTestEnv({
|
||||
[ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted",
|
||||
}),
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
...stateOverrides,
|
||||
}),
|
||||
{ ...ELIGIBLE_OPTIONS, ...overrides },
|
||||
@@ -145,27 +142,6 @@ test("getPerLanguageBundleLanguage requires a supported release version", async
|
||||
t.is(await checkEligibility({ cliVersion: "2.27.1" }), BuiltInLanguage.java);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires the feature flag", async (t) => {
|
||||
t.is(await checkEligibility({}, { features: createFeatures([]) }), undefined);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage explains a disabled feature before checking eligibility", async (t) => {
|
||||
const messages: LoggedMessage[] = [];
|
||||
const language = await getPerLanguageBundleLanguage(
|
||||
initAllState({
|
||||
features: createFeatures([]),
|
||||
logger: getRecordingLogger(messages, { logToConsole: false }),
|
||||
}),
|
||||
{ ...ELIGIBLE_OPTIONS, rawLanguages: undefined, cliVersion: undefined },
|
||||
);
|
||||
|
||||
t.is(language, undefined);
|
||||
t.deepEqual(
|
||||
messages.map((message) => message.message),
|
||||
["Not using a per-language CodeQL bundle since the feature is disabled."],
|
||||
);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage skips only the release version check for the latest nightly", async (t) => {
|
||||
const nightly = { isLatestNightly: true, cliVersion: undefined };
|
||||
t.is(await checkEligibility(nightly), BuiltInLanguage.java);
|
||||
@@ -181,10 +157,6 @@ test("getPerLanguageBundleLanguage skips only the release version check for the
|
||||
]) {
|
||||
t.is(await checkEligibility({ ...nightly, ...overrides }), undefined);
|
||||
}
|
||||
t.is(
|
||||
await checkEligibility(nightly, { features: createFeatures([]) }),
|
||||
undefined,
|
||||
);
|
||||
t.is(
|
||||
await checkEligibility(nightly, {
|
||||
env: getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
QuerySpec,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { RepositoryPropertyName } from "./feature-flags/properties";
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { BundlePlatform } from "./platform";
|
||||
@@ -139,14 +138,10 @@ export interface PerLanguageBundleOptions {
|
||||
}
|
||||
|
||||
/** Returns the eligible bundle language, or undefined for the combined bundle. */
|
||||
export async function getPerLanguageBundleLanguage(
|
||||
{
|
||||
env,
|
||||
features,
|
||||
logger,
|
||||
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
|
||||
export function getPerLanguageBundleLanguage(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv"]>,
|
||||
options: PerLanguageBundleOptions,
|
||||
): Promise<BuiltInLanguage | undefined> {
|
||||
): BuiltInLanguage | undefined {
|
||||
const {
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
@@ -158,14 +153,12 @@ export async function getPerLanguageBundleLanguage(
|
||||
} = options;
|
||||
|
||||
const explain = (reason: string) => {
|
||||
logger.debug(`Not using a per-language CodeQL bundle since ${reason}.`);
|
||||
action.logger.debug(
|
||||
`Not using a per-language CodeQL bundle since ${reason}.`,
|
||||
);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if (!(await features.getValue(Feature.PerLanguageBundles))) {
|
||||
return explain("the feature is disabled");
|
||||
}
|
||||
|
||||
// A defined reason means the CodeQL CLI may need packs for other languages, for example because
|
||||
// of the configured queries. That applies whichever languages were requested, so check it first
|
||||
// to avoid suggesting that requesting a single language would be enough.
|
||||
@@ -197,7 +190,7 @@ export async function getPerLanguageBundleLanguage(
|
||||
return explain(`we are running against ${variant}`);
|
||||
}
|
||||
|
||||
if (!isGitHubHostedRunner(env)) {
|
||||
if (!isGitHubHostedRunner(action.env)) {
|
||||
// Per-language installs stay out of the toolcache; self-hosted runners should retain
|
||||
// the reusable combined bundle instead.
|
||||
return explain("the job is not running on a GitHub-hosted runner");
|
||||
|
||||
@@ -439,7 +439,7 @@ test.serial(
|
||||
tmpDir,
|
||||
GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
["javascript"],
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
@@ -633,7 +633,7 @@ for (const toolsInput of ["nightly", "nightly-latest"]) {
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
true, // tarSupportsZstd
|
||||
createFeatures([Feature.PerLanguageBundles]),
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
@@ -664,24 +664,16 @@ test.serial(
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
for (const { languages, otherLanguagePacksReason, features } of [
|
||||
// The per-language feature is disabled.
|
||||
{
|
||||
languages: ["java"],
|
||||
otherLanguagePacksReason: undefined,
|
||||
features: createFeatures([]),
|
||||
},
|
||||
for (const { languages, otherLanguagePacksReason } of [
|
||||
// More than one language requires a combined bundle.
|
||||
{
|
||||
languages: ["java", "python"],
|
||||
otherLanguagePacksReason: undefined,
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
},
|
||||
// The configured queries may need library packs for other languages.
|
||||
{
|
||||
languages: ["java"],
|
||||
otherLanguagePacksReason: "an example reason",
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
},
|
||||
]) {
|
||||
const source = await setupCodeql.getCodeQLSource(
|
||||
@@ -693,7 +685,7 @@ test.serial(
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
true, // tarSupportsZstd
|
||||
features,
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
@@ -709,58 +701,48 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
for (const perLanguageBundles of [false, true]) {
|
||||
test.serial(
|
||||
`getCodeQLSource uses the latest ${perLanguageBundles ? "per-language" : "combined"} bundle for a forced nightly`,
|
||||
async (t) => {
|
||||
const expectedTag = "codeql-bundle-30260213";
|
||||
const baseURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}`;
|
||||
const latestNightlyRequest = stubHostedNightly(expectedTag);
|
||||
test.serial(
|
||||
"getCodeQLSource uses the latest per-language bundle for a forced nightly",
|
||||
async (t) => {
|
||||
const expectedTag = "codeql-bundle-30260213";
|
||||
const baseURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}`;
|
||||
const latestNightlyRequest = stubHostedNightly(expectedTag);
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" });
|
||||
const source = await setupCodeql.getCodeQLSource(
|
||||
undefined, // toolsInput: the nightly is selected by ForceNightly
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
["java"],
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
true, // tarSupportsZstd
|
||||
createFeatures(
|
||||
perLanguageBundles
|
||||
? [Feature.ForceNightly, Feature.PerLanguageBundles]
|
||||
: [Feature.ForceNightly],
|
||||
),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir, { GITHUB_EVENT_NAME: "dynamic" });
|
||||
const source = await setupCodeql.getCodeQLSource(
|
||||
undefined, // toolsInput: the nightly is selected by ForceNightly
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
["java"],
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
true, // tarSupportsZstd
|
||||
createFeatures([Feature.ForceNightly]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
t.is(source.sourceType, "download");
|
||||
t.true(latestNightlyRequest.calledOnce);
|
||||
if (source.sourceType === "download") {
|
||||
const combinedURL = `${baseURL}/codeql-bundle-linux64.tar.zst`;
|
||||
t.deepEqual(
|
||||
source.bundle,
|
||||
perLanguageBundles
|
||||
? {
|
||||
kind: "per-language",
|
||||
language: BuiltInLanguage.java,
|
||||
url: `${baseURL}/codeql-bundle-java-linux64.tar.zst`,
|
||||
combinedBundleURL: combinedURL,
|
||||
}
|
||||
: { kind: "combined", url: combinedURL },
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
t.is(source.sourceType, "download");
|
||||
t.true(latestNightlyRequest.calledOnce);
|
||||
if (source.sourceType === "download") {
|
||||
t.deepEqual(source.bundle, {
|
||||
kind: "per-language",
|
||||
language: BuiltInLanguage.java,
|
||||
url: `${baseURL}/codeql-bundle-java-linux64.tar.zst`,
|
||||
combinedBundleURL: `${baseURL}/codeql-bundle-linux64.tar.zst`,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
for (const date of ["20200101", "30260213"]) {
|
||||
for (const bundle of ["combined", "per-language"] as const) {
|
||||
test.serial(
|
||||
`getCodeQLSource preserves an explicit ${bundle} nightly URL for ${date}`,
|
||||
`getCodeQLSource preserves an explicit ${bundle} nightly URL for ${date}${
|
||||
bundle === "per-language" ? " on GHES" : ""
|
||||
}`,
|
||||
async (t) => {
|
||||
const latestNightlyRequest = stubHostedNightly(
|
||||
"codeql-bundle-30260213",
|
||||
@@ -770,7 +752,9 @@ for (const date of ["20200101", "30260213"]) {
|
||||
? "codeql-bundle-linux64.tar.zst"
|
||||
: "codeql-bundle-java-linux64.tar.zst";
|
||||
const url = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/codeql-bundle-${date}/${asset}`;
|
||||
const features = createFeatures([Feature.PerLanguageBundles]);
|
||||
const features = createFeatures([]);
|
||||
const variant =
|
||||
bundle === "per-language" ? GitHubVariant.GHES : GitHubVariant.DOTCOM;
|
||||
const logger = getRecordingLogger([], { logToConsole: false });
|
||||
const error = new HTTPError("Not Found", 404);
|
||||
const extractStub = sinon
|
||||
@@ -786,7 +770,7 @@ for (const date of ["20200101", "30260213"]) {
|
||||
undefined, // otherLanguagePacksReason
|
||||
false,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
variant,
|
||||
true,
|
||||
features,
|
||||
logger,
|
||||
@@ -808,7 +792,7 @@ for (const date of ["20200101", "30260213"]) {
|
||||
url,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
GitHubVariant.DOTCOM,
|
||||
variant,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
["java"],
|
||||
undefined, // otherLanguagePacksReason
|
||||
@@ -1262,7 +1246,7 @@ test.serial(
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
true, // tarSupportsZstd
|
||||
createFeatures([Feature.PerLanguageBundles]),
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
@@ -1295,13 +1279,16 @@ test.serial(
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
for (const { otherLanguagePacksReason, features } of [
|
||||
// The per-language feature is disabled.
|
||||
{ otherLanguagePacksReason: undefined, features: createFeatures([]) },
|
||||
for (const { otherLanguagePacksReason, variant } of [
|
||||
// Automatic per-language selection is limited to GitHub.com.
|
||||
{
|
||||
otherLanguagePacksReason: undefined,
|
||||
variant: GitHubVariant.GHES,
|
||||
},
|
||||
// The configured queries may need library packs for other languages.
|
||||
{
|
||||
otherLanguagePacksReason: "an example reason",
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
variant: GitHubVariant.DOTCOM,
|
||||
},
|
||||
]) {
|
||||
const source = await setupCodeql.getCodeQLSource(
|
||||
@@ -1311,9 +1298,9 @@ test.serial(
|
||||
otherLanguagePacksReason,
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
GitHubVariant.DOTCOM,
|
||||
variant,
|
||||
true, // tarSupportsZstd
|
||||
features,
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
@@ -1383,7 +1370,7 @@ for (const fallback of [false, true]) {
|
||||
["java"],
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
createFeatures([Feature.PerLanguageBundles]),
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
);
|
||||
|
||||
@@ -1463,7 +1450,7 @@ for (const bundle of ["per-language", "combined", "fallback"] as const) {
|
||||
};
|
||||
});
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addNoLanguageDiagnostic");
|
||||
const features = createFeatures([Feature.PerLanguageBundles]);
|
||||
const features = createFeatures([]);
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
@@ -1648,7 +1635,7 @@ for (const error of [
|
||||
["java"],
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
createFeatures([Feature.PerLanguageBundles]),
|
||||
createFeatures([]),
|
||||
new RecordingLogger(),
|
||||
),
|
||||
{ is: error },
|
||||
@@ -1714,15 +1701,19 @@ test.serial(
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await setupCodeql.getEnabledVersionsWithOverlayBaseDatabases(
|
||||
overlayMatchEnabledVersions,
|
||||
["javascript"],
|
||||
new RecordingLogger(),
|
||||
);
|
||||
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"],
|
||||
new RecordingLogger(),
|
||||
);
|
||||
t.deepEqual(result, [
|
||||
{ cliVersion: "2.20.1", tagName: "codeql-bundle-v2.20.1" },
|
||||
{ cliVersion: "2.20.0", tagName: "codeql-bundle-v2.20.0" },
|
||||
]);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -364,7 +364,7 @@ export async function getCodeQLSource(
|
||||
);
|
||||
}
|
||||
bundle = await getLatestNightlyBundle(
|
||||
{ env: getEnv(), features, logger },
|
||||
{ env: getEnv(), logger },
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
variant,
|
||||
@@ -605,17 +605,14 @@ export async function getCodeQLSource(
|
||||
|
||||
const action = { env: getEnv(), logger };
|
||||
const platform = getBundlePlatform();
|
||||
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
|
||||
{ ...action, features },
|
||||
{
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform,
|
||||
variant,
|
||||
},
|
||||
);
|
||||
const perLanguageBundleLanguage = getPerLanguageBundleLanguage(action, {
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform,
|
||||
variant,
|
||||
});
|
||||
|
||||
// Resolves the combined or per-language bundle URL for the requested release.
|
||||
const resolveBundleURL = (language?: BuiltInLanguage) =>
|
||||
@@ -1043,7 +1040,7 @@ function getTempExtractionDir(tempDir: string) {
|
||||
* Records the combined bundle URL from that release for use if the selected asset is missing.
|
||||
*/
|
||||
async function getLatestNightlyBundle(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
|
||||
action: ActionState<["Logger", "ReadOnlyEnv"]>,
|
||||
rawLanguages: string[] | undefined,
|
||||
otherLanguagePacksReason: string | undefined,
|
||||
variant: util.GitHubVariant,
|
||||
@@ -1059,7 +1056,7 @@ async function getLatestNightlyBundle(
|
||||
: "gzip";
|
||||
|
||||
const platform = getBundlePlatform();
|
||||
const language = await getPerLanguageBundleLanguage(action, {
|
||||
const language = getPerLanguageBundleLanguage(action, {
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
cliVersion: undefined,
|
||||
|
||||
@@ -1032,6 +1032,7 @@ export function createTestConfig(overrides: Partial<Config>): Config {
|
||||
overlayModeSetExplicitly: false,
|
||||
repositoryProperties: {},
|
||||
enableFileCoverageInformation: true,
|
||||
successfullyInitialised: false,
|
||||
} satisfies Config,
|
||||
overrides,
|
||||
);
|
||||
|
||||
15
src/util.ts
15
src/util.ts
@@ -900,6 +900,21 @@ export function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximates whether {@link error} is like a {@link NodeJS.ErrnoException}.
|
||||
* We only typically care about the `code` property, so that's what we check for here.
|
||||
*/
|
||||
export function isErrnoException(
|
||||
error: unknown,
|
||||
): error is NodeJS.ErrnoException {
|
||||
return (
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
"code" in error &&
|
||||
typeof error.code === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function prettyPrintPack(pack: Pack) {
|
||||
return `${pack.name}${pack.version ? `@${pack.version}` : ""}${
|
||||
pack.path ? `:${pack.path}` : ""
|
||||
|
||||
Reference in New Issue
Block a user