mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 09:14:58 +00:00
Merge pull request #4152 from github/update-v4.38.1-a65b83a73
Merge main into releases/v4
This commit is contained in:
25
.github/copilot-instructions.md
vendored
25
.github/copilot-instructions.md
vendored
@@ -1,14 +1,33 @@
|
||||
# CodeQL Action - Copilot Instructions
|
||||
|
||||
The CodeQL Action is used in GitHub Actions workflows to run CodeQL scans using the CodeQL CLI.
|
||||
|
||||
## Overview
|
||||
|
||||
- The repository contains two TypeScript projects.
|
||||
- The main TypeScript codebase is in the `src` directory, with accompanying unit tests in `.test.ts` files in the same directory.
|
||||
- The main codebase is compiled to bundled JavaScript code, which is also contained in the repository in the `lib` directory.
|
||||
- A secondary TypeScript codebase with scripts that are only used for development purposes or by CI is in the `pr-checks` directory. This codebase is not compiled to bundled JavaScript. It is executed directly with `tsx`, which handles compilation internally.
|
||||
|
||||
## Review instructions
|
||||
|
||||
- When wording review comments, be helpful and friendly. Assume that the PR author has written the code with the best of intentions. Word your comments constructively as suggestions for improvements. Do not word suggestions as commands.
|
||||
- If you want to comment on a change that you believe will fail a CI check, do not present the CI failure you expect as a fact. Instead, write that you think a change "may" lead to a failure in CI. Suggest that, if such a failure manifests, the changes you are commenting on may be the place responsible for the failure and are worth looking at.
|
||||
- If a suggestion you make is suitable for a follow-up, such as a refactoring that doesn't change the behaviour or fixing a typo in a comment, mention that it can be addressed in a later PR rather than blocking this one.
|
||||
- If a change is a net improvement, for example because it improves on an existing limitation of existing code, do not complain about pre-existing problems that remain. You may comment on them, but you should make it clear that the thing you are commenting on is not new by writing e.g. "Not new in this PR, but [..]" followed by your description of the issue and a suggestion that it could be improved at the same time with e.g. "Consider whether this is worth addressing as part of this PR as well."
|
||||
|
||||
## Generated code
|
||||
|
||||
The CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from.
|
||||
The main codebase of the CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from. The secondary TypeScript codebase has sources in the `pr-checks` directory, which are executed directly with `tsx` and not compiled to JavaScript in the `lib` folder.
|
||||
|
||||
GitHub Actions workflows in the `.github/workflows` directory whose filenames start with two underscores (e.g. `__all-platform-bundle.yml`) are automatically generated using the `pr-checks/sync.sh` script from template files in the `pr-checks/checks` directory. Therefore, you do not need to review files in the `.github/workflows` directory that start with two underscores. However, you should review changes to the `pr-checks` directory as well as workflows in the `.github/workflows` directory that do not start with underscores.
|
||||
|
||||
## Release process
|
||||
|
||||
Changes that are merged to the `main` branch are not used in production straight away. Instead, changes from `main` must first be released using the release process, which has to be manually triggered by a staff member. Therefore, it is possible for further changes to be made to the code after some changes have been merged to `main` but before they are released. Once the release process automation is started, a series of PRs are automatically generated:
|
||||
|
||||
- A release-PR is a PR which matches all of the following patterns:
|
||||
|
||||
- PR title starts with the exact prefix: `Merge main into releases/v`
|
||||
- Equivalent regex: `^Merge main into releases\/v`
|
||||
- AND head (source) branch matches the update-branch pattern:
|
||||
@@ -17,5 +36,5 @@ GitHub Actions workflows in the `.github/workflows` directory whose filenames st
|
||||
- Regex: `^releases\/v` (e.g. `releases/v3`)
|
||||
|
||||
- Consider all files in release-PRs to have already been reviewed and do not review them, except for the following two files:
|
||||
- `CHANGELOG.md`
|
||||
- `package.json`
|
||||
- `CHANGELOG.md`
|
||||
- `package.json`
|
||||
|
||||
2
.github/workflows/__autobuild-direct-tracing-with-working-dir.yml
generated
vendored
2
.github/workflows/__autobuild-direct-tracing-with-working-dir.yml
generated
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
2
.github/workflows/__build-mode-autobuild.yml
generated
vendored
2
.github/workflows/__build-mode-autobuild.yml
generated
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
3
.github/workflows/__bundle-toolcache.yml
generated
vendored
3
.github/workflows/__bundle-toolcache.yml
generated
vendored
@@ -80,7 +80,8 @@ jobs:
|
||||
- id: init
|
||||
uses: ./../action/init
|
||||
with:
|
||||
languages: javascript
|
||||
# Request multiple languages so this check uses the combined bundle.
|
||||
languages: javascript,python
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
|
||||
164
.github/workflows/__per-language-bundle-validation.yml
generated
vendored
Normal file
164
.github/workflows/__per-language-bundle-validation.yml
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
# 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 - Per-language bundles
|
||||
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: per-language-bundle-validation-${{github.ref}}
|
||||
jobs:
|
||||
per-language-bundle-validation:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
expected-extractors: actions javascript
|
||||
- language: cpp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: manual
|
||||
build-command: gcc -o main main.c
|
||||
- language: csharp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: none
|
||||
- language: go
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: autobuild
|
||||
- language: java
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: none
|
||||
- language: javascript
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: python
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: ruby
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: rust
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: swift
|
||||
os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
build-mode: autobuild
|
||||
name: Per-language bundles
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Prepare test
|
||||
id: prepare-test
|
||||
uses: ./.github/actions/prepare-test
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix['build-mode'] }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Check that the bundle contains only the expected extractors
|
||||
env:
|
||||
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
|
||||
LANGUAGE: ${{ matrix.language }}
|
||||
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
|
||||
run: |
|
||||
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
|
||||
echo "Extractors in the bundle:"
|
||||
echo "$extractors"
|
||||
echo "Expected: $EXPECTED_EXTRACTORS"
|
||||
|
||||
for expected in $EXPECTED_EXTRACTORS; do
|
||||
if ! echo "$extractors" | grep -qx "$expected"; then
|
||||
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# If the bundle contained extractors beyond those the language needs, then it would not
|
||||
# have been trimmed, and this job would be silently validating the combined bundle.
|
||||
for other in actions cpp csharp go java javascript python ruby rust swift; do
|
||||
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
|
||||
continue
|
||||
fi
|
||||
if echo "$extractors" | grep -qx "$other"; then
|
||||
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Check that the bundle was not added to the toolcache
|
||||
env:
|
||||
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.
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
|
||||
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
|
||||
exit 1
|
||||
fi
|
||||
- name: Build code
|
||||
if: matrix['build-command']
|
||||
run: ${{ matrix['build-command'] }}
|
||||
- uses: ./../action/analyze
|
||||
id: analysis
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Check that a database was created for the language
|
||||
env:
|
||||
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
|
||||
LANGUAGE: ${{ matrix.language }}
|
||||
run: |
|
||||
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
|
||||
if [ -z "$database" ] || [ ! -d "$database" ]; then
|
||||
echo "::error::No CodeQL database was created for ${LANGUAGE}."
|
||||
echo "Databases: $DB_LOCATIONS"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created a ${LANGUAGE} database at ${database}."
|
||||
env:
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
@@ -75,7 +75,8 @@ jobs:
|
||||
uses: ./../action/.github/actions/check-codescanning-config
|
||||
with:
|
||||
expected-config-file-contents: "{}"
|
||||
languages: javascript
|
||||
# Request multiple languages so later checks can reuse the combined bundle.
|
||||
languages: javascript,python
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
|
||||
- name: Packs from input
|
||||
|
||||
39
.github/workflows/pr-checks.yml
vendored
39
.github/workflows/pr-checks.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -77,33 +77,6 @@ jobs:
|
||||
sarif_file: eslint.sarif
|
||||
category: eslint
|
||||
|
||||
changetool-tests:
|
||||
name: changetool unit tests
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: pr-checks-changetool-tests-${{ github.ref }}-${{ github.event_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run changetool unit tests
|
||||
run: npm --workspace changetool test
|
||||
|
||||
# These checks do not need to be run as part of the same matrix that we use for the `unit-tests`
|
||||
# job.
|
||||
other-checks:
|
||||
@@ -126,7 +99,7 @@ jobs:
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
id: install-deps
|
||||
@@ -141,6 +114,10 @@ jobs:
|
||||
working-directory: pr-checks
|
||||
run: npx tsx --test
|
||||
|
||||
- name: Run `pr-checks/changenotes.mts` to ensure that all unreleased change notes are valid
|
||||
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
|
||||
run: npx tsx pr-checks/changenotes.mts validate
|
||||
|
||||
- name: Verify all Actions use the same Node version
|
||||
id: head-version
|
||||
run: |
|
||||
@@ -190,14 +167,14 @@ jobs:
|
||||
path: ${{ runner.temp }}/repo-size/
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 'Backport: Check out base ref'
|
||||
- name: "Backport: Check out base ref"
|
||||
id: checkout-base
|
||||
if: ${{ startsWith(github.head_ref, 'backport-') }}
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
- name: 'Backport: Verify Node versions unchanged'
|
||||
- name: "Backport: Verify Node versions unchanged"
|
||||
if: steps.checkout-base.outcome == 'success'
|
||||
env:
|
||||
HEAD_VERSION: ${{ steps.head-version.outputs.node_version }}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## 4.38.1 - 18 Sept 2026
|
||||
|
||||
- The CodeQL Action now has experimental support for CodeQL releases for which per-language bundles are available. Per-language bundles support analysis for a single language and are therefore smaller than the combined bundles that allow analysis for all supported languages. As a result, per-language bundles take up less space on disk and are faster to download. We expect to roll this change out to everyone in the coming weeks. [#4146](https://github.com/github/codeql-action/pull/4146)
|
||||
|
||||
## 4.38.0 - 09 Sept 2026
|
||||
|
||||
- On GitHub-hosted runners, the CodeQL Action now deletes unused CodeQL bundles from the toolcache before downloading a different bundle, which frees up disk space for the analysis. We expect to roll this change out to everyone in September. [#4124](https://github.com/github/codeql-action/pull/4124)
|
||||
|
||||
@@ -158,7 +158,7 @@ export default [
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts", "**/*.js"],
|
||||
files: ["**/*.ts", "**/*.js", "**/*.mts"],
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
@@ -180,7 +180,7 @@ export default [
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["pr-checks/**/*.ts"],
|
||||
files: ["pr-checks/**/*.ts", "pr-checks/**/*.mts"],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
@@ -209,18 +209,4 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["scripts/changetool/**/*.ts"],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./scripts/changetool/tsconfig.json",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/extensions": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
865
lib/entry-points.js
generated
865
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
800
package-lock.json
generated
800
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.38.0",
|
||||
"version": "4.38.1",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -17,8 +17,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks",
|
||||
"scripts/changetool"
|
||||
"pr-checks"
|
||||
],
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^5.0.3",
|
||||
@@ -31,7 +30,7 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": "^15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
@@ -40,7 +39,7 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.0",
|
||||
"js-yaml": "^5.4.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -50,9 +49,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/compat": "^2.1.1",
|
||||
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
||||
"@octokit/types": "^17.0.0",
|
||||
"@octokit/types": "^18.0.0",
|
||||
"@types/archiver": "^8.0.0",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -67,14 +66,14 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^64.2.1",
|
||||
"eslint-plugin-jsdoc": "^64.3.8",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.11.0",
|
||||
"globals": "^17.12.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.68.0"
|
||||
"typescript-eslint": "^8.70.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -119,3 +119,14 @@ export function isValidChangenoteFile(filename: string): boolean {
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the change-note files of the given list of file paths, ignoring ".gitkeep".
|
||||
* @param filepaths A list of filepaths to validate
|
||||
* @returns True if all the paths are valid, false otherwise.
|
||||
*/
|
||||
export function isValidAllChangenoteFiles(filepaths: string[]): boolean {
|
||||
return filepaths
|
||||
.filter((f) => f !== ".gitkeep")
|
||||
.reduce((r, filePath) => r && isValidChangenoteFile(filePath), true);
|
||||
}
|
||||
@@ -1,36 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { withTmpDir, withTmpFile } from "../../src/util";
|
||||
|
||||
import {
|
||||
hasValidChangenoteCategory,
|
||||
isValidAllChangenoteFiles,
|
||||
isValidChangenoteContent,
|
||||
isValidChangenoteFile,
|
||||
isValidChangenoteFilename,
|
||||
hasValidChangenoteCategory,
|
||||
VALID_CHANGE_NOTE_CATEGORIES,
|
||||
} from "./validate.ts";
|
||||
|
||||
async function withTmpFile<T>(
|
||||
baseFileName: string,
|
||||
contents: string,
|
||||
body: (filePath: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const tmpDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "changetool-validate-test-"),
|
||||
);
|
||||
try {
|
||||
const filePath = path.join(tmpDir, baseFileName);
|
||||
fs.writeFileSync(filePath, contents);
|
||||
return await body(filePath);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} from "./validate.mjs";
|
||||
|
||||
await describe("isValidChangenoteContent", async () => {
|
||||
await it("recognizes an unordered Markdown list", async () => {
|
||||
await it("recognizes an unordered Markdown list", () => {
|
||||
const inputs = [
|
||||
"- One changenote entry",
|
||||
"- First item\n- Second item",
|
||||
@@ -42,7 +27,7 @@ await describe("isValidChangenoteContent", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize non-Markdown text", async () => {
|
||||
await it("does not recognize non-Markdown text", () => {
|
||||
const inputs = [
|
||||
"This is not a list.",
|
||||
'["this", "is", "JSON"]',
|
||||
@@ -57,7 +42,7 @@ await describe("isValidChangenoteContent", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize ordered Markdown lists", async () => {
|
||||
await it("does not recognize ordered Markdown lists", () => {
|
||||
const inputs = [
|
||||
"1. First item\n2. Second item",
|
||||
"\n\n\n1. First item\n1. Second item",
|
||||
@@ -68,7 +53,7 @@ await describe("isValidChangenoteContent", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("requires all list items to use a hyphen bullet", async () => {
|
||||
await it("requires all list items to use a hyphen bullet", () => {
|
||||
const inputs = [
|
||||
"* Fixed a bug\n* Added feature",
|
||||
"+ Fixed a bug\n+ Added feature",
|
||||
@@ -85,7 +70,7 @@ await describe("isValidChangenoteContent", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not contain other Markdown elements", async () => {
|
||||
await it("does not contain other Markdown elements", () => {
|
||||
const inputs = [
|
||||
"- Fixed a bug\n\nParagraph of text",
|
||||
"- Fixed a bug\n\n* Added a feature",
|
||||
@@ -100,7 +85,7 @@ await describe("isValidChangenoteContent", async () => {
|
||||
});
|
||||
|
||||
await describe("isValidChangenoteFilename", async () => {
|
||||
await it("accepts valid filenames", async () => {
|
||||
await it("accepts valid filenames", () => {
|
||||
const inputs = [
|
||||
"2023-01-01-fix-bug.md",
|
||||
"2023-12-31-add-feature.md",
|
||||
@@ -112,7 +97,7 @@ await describe("isValidChangenoteFilename", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("rejects invalid filenames", async () => {
|
||||
await it("rejects invalid filenames", () => {
|
||||
const inputs = [
|
||||
"missing-date-from-filename.md",
|
||||
"2021-01-01.md",
|
||||
@@ -126,14 +111,14 @@ await describe("isValidChangenoteFilename", async () => {
|
||||
});
|
||||
|
||||
await describe("hasValidChangenoteCategory", async () => {
|
||||
await it("accepts valid categories", async () => {
|
||||
await it("accepts valid categories", () => {
|
||||
for (const category of Object.keys(VALID_CHANGE_NOTE_CATEGORIES)) {
|
||||
const frontmatter = { category };
|
||||
assert.equal(hasValidChangenoteCategory(frontmatter), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("rejects invalid categories", async () => {
|
||||
await it("rejects invalid categories", () => {
|
||||
const inputs = [
|
||||
"",
|
||||
"invalid-category",
|
||||
@@ -150,7 +135,7 @@ await describe("hasValidChangenoteCategory", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
await it("reject missing category", async () => {
|
||||
await it("reject missing category", () => {
|
||||
assert.equal(hasValidChangenoteCategory({}), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: null }), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: undefined }), false);
|
||||
@@ -162,17 +147,21 @@ await describe("isValidChangenoteFile", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects a non-existent path", async () => {
|
||||
assert.equal(isValidChangenoteFile("non-existent-file.md"), false);
|
||||
});
|
||||
|
||||
await it("rejects invalid filename", async () => {
|
||||
await withTmpFile(
|
||||
"fix-bug.md",
|
||||
"---\ncategory: fix\n---\n- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
@@ -182,7 +171,7 @@ await describe("isValidChangenoteFile", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
@@ -192,9 +181,45 @@ await describe("isValidChangenoteFile", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n* Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidAllChangenoteFiles", async () => {
|
||||
await it("accepts list of file paths of valid change-notes", async () => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
|
||||
const fileName2 = path.join(tmpDir, "2026-01-02-add-feature.md");
|
||||
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
|
||||
fs.writeFileSync(
|
||||
fileName2,
|
||||
"---\ncategory: feature\n---\n- Added a feature\n",
|
||||
);
|
||||
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), true);
|
||||
});
|
||||
});
|
||||
|
||||
await it("accepts the empty list", async () => {
|
||||
assert.equal(isValidAllChangenoteFiles([]), true);
|
||||
});
|
||||
|
||||
await it("accepts list of .gitkeep", async () => {
|
||||
assert.equal(isValidAllChangenoteFiles([".gitkeep"]), true);
|
||||
});
|
||||
|
||||
await it("rejects list containing a file path to an invalid change-note", async () => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const fileName1 = path.join(tmpDir, "2026-01-01-fix-bug.md");
|
||||
const fileName2 = path.join(tmpDir, "2026-01-02-wrong-category.md");
|
||||
fs.writeFileSync(fileName1, "---\ncategory: fix\n---\n- Fixed a bug\n");
|
||||
fs.writeFileSync(
|
||||
fileName2,
|
||||
"---\ncategory: foobar\n---\n- Added a feature\n",
|
||||
);
|
||||
assert.equal(isValidAllChangenoteFiles([fileName1, fileName2]), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
scripts/changetool/index.ts → pr-checks/changenotes.mts
Normal file → Executable file
33
scripts/changetool/index.ts → pr-checks/changenotes.mts
Normal file → Executable file
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { isValidChangenoteFile } from "./cli/validate.ts";
|
||||
import { isValidAllChangenoteFiles } from "./changelog/validate.mjs";
|
||||
import { CHANGENOTES_DIR } from "./config";
|
||||
|
||||
const entryPoint = process.argv[1];
|
||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||
@@ -18,13 +22,13 @@ function main(): number {
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
const [command, ...paths] = positionals;
|
||||
const [command] = positionals;
|
||||
switch (command) {
|
||||
case undefined:
|
||||
case "help":
|
||||
return usage();
|
||||
case "validate":
|
||||
return validate(paths);
|
||||
return validate();
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
return 1;
|
||||
@@ -32,20 +36,21 @@ function main(): number {
|
||||
}
|
||||
|
||||
function usage(): number {
|
||||
console.log("Usage: changetool validate <path> [<path> ...]");
|
||||
console.log(`Usage: changenotes.mts validate`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function validate(paths: string[]): number {
|
||||
let valid = true;
|
||||
if (paths.length === 0) {
|
||||
console.error("error: no paths provided (see 'help' command for usage)");
|
||||
return 1;
|
||||
}
|
||||
for (const path of paths) {
|
||||
if (!isValidChangenoteFile(path)) {
|
||||
valid = false;
|
||||
function validate(): number {
|
||||
try {
|
||||
if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) {
|
||||
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
|
||||
return 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
return valid ? 0 : 1;
|
||||
return 1;
|
||||
}
|
||||
@@ -30,7 +30,8 @@ steps:
|
||||
- id: init
|
||||
uses: ./../action/init
|
||||
with:
|
||||
languages: javascript
|
||||
# Request multiple languages so this check uses the combined bundle.
|
||||
languages: javascript,python
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
|
||||
117
pr-checks/checks/per-language-bundle-validation.yml
Normal file
117
pr-checks/checks/per-language-bundle-validation.yml
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
# Actions also needs the JavaScript extractor.
|
||||
expected-extractors: actions javascript
|
||||
- language: cpp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: manual
|
||||
build-command: gcc -o main main.c
|
||||
- language: csharp
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: none
|
||||
- language: go
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: autobuild
|
||||
- language: java
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
build-mode: none
|
||||
- language: javascript
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: python
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: ruby
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: rust
|
||||
os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- language: swift
|
||||
os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
build-mode: autobuild
|
||||
env:
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix['build-mode'] }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Check that the bundle contains only the expected extractors
|
||||
env:
|
||||
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
|
||||
LANGUAGE: ${{ matrix.language }}
|
||||
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
|
||||
run: |
|
||||
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
|
||||
echo "Extractors in the bundle:"
|
||||
echo "$extractors"
|
||||
echo "Expected: $EXPECTED_EXTRACTORS"
|
||||
|
||||
for expected in $EXPECTED_EXTRACTORS; do
|
||||
if ! echo "$extractors" | grep -qx "$expected"; then
|
||||
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# If the bundle contained extractors beyond those the language needs, then it would not
|
||||
# have been trimmed, and this job would be silently validating the combined bundle.
|
||||
for other in actions cpp csharp go java javascript python ruby rust swift; do
|
||||
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
|
||||
continue
|
||||
fi
|
||||
if echo "$extractors" | grep -qx "$other"; then
|
||||
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
- name: Check that the bundle was not added to the toolcache
|
||||
env:
|
||||
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.
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
|
||||
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
|
||||
exit 1
|
||||
fi
|
||||
- name: Build code
|
||||
if: matrix['build-command']
|
||||
run: ${{ matrix['build-command'] }}
|
||||
- uses: ./../action/analyze
|
||||
id: analysis
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Check that a database was created for the language
|
||||
env:
|
||||
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
|
||||
LANGUAGE: ${{ matrix.language }}
|
||||
run: |
|
||||
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
|
||||
if [ -z "$database" ] || [ ! -d "$database" ]; then
|
||||
echo "::error::No CodeQL database was created for ${LANGUAGE}."
|
||||
echo "Databases: $DB_LOCATIONS"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created a ${LANGUAGE} database at ${database}."
|
||||
@@ -18,6 +18,9 @@ export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
|
||||
/** The path of the changelog. */
|
||||
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
|
||||
|
||||
/** The path to the unreleased change-notes directory. */
|
||||
export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
|
||||
|
||||
/** The path to the esbuild metadata file. */
|
||||
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
{
|
||||
"private": true,
|
||||
"description": "Dependencies for the sync.ts",
|
||||
"description": "Dependencies for codeql-action scripts",
|
||||
"dependencies": {
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/core": "^7.0.8",
|
||||
"@octokit/plugin-paginate-rest": ">=15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.23.12"
|
||||
"tsx": "^4.23.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,19 @@ set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Run `npm ci` in CI or `npm install` otherwise.
|
||||
#
|
||||
# `pr-checks` is an npm workspace of the repository root and the two share a single hoisted
|
||||
# `node_modules` directory. Running npm from this directory puts it in workspace mode, where it
|
||||
# ignores the root project's own dependencies by default. `npm ci` would then rebuild the shared
|
||||
# `node_modules` with only this workspace's dependencies, removing the root's ones, which breaks
|
||||
# anything that imports from `src` (such as `sync.ts` itself). `--include-workspace-root` keeps the
|
||||
# root project's dependencies in the installed tree.
|
||||
if [ "$GITHUB_ACTIONS" = "true" ]; then
|
||||
echo "In Actions, running 'npm ci' for 'sync.ts'..."
|
||||
npm ci
|
||||
npm ci --include-workspace-root
|
||||
else
|
||||
echo "Running 'npm install' for 'sync.ts'..."
|
||||
npm install --no-audit --no-fund
|
||||
npm install --no-audit --no-fund --include-workspace-root
|
||||
fi
|
||||
|
||||
npx tsx sync.ts
|
||||
|
||||
@@ -79,6 +79,8 @@ interface Specification extends JobSpecification {
|
||||
useAllPlatformBundle?: string;
|
||||
/** Values for the `analysis-kinds` matrix dimension. */
|
||||
analysisKinds?: string[];
|
||||
/** Overrides the generated job matrix using GitHub Actions matrix syntax. */
|
||||
matrix?: Record<string, unknown>;
|
||||
|
||||
/** Container image configuration for the job. */
|
||||
container?: any;
|
||||
@@ -253,8 +255,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"dd06d9cba3e5552c54d9f8ea23572deb30010f7c",
|
||||
"v6.0.0",
|
||||
"de7274f081f381c8f8158605e0321c36c376e2e6",
|
||||
"v6.0.1",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
@@ -512,9 +514,6 @@ function generateJob(
|
||||
specDocument: yaml.Document,
|
||||
checkSpecification: Specification,
|
||||
) {
|
||||
const matrix: Array<Record<string, any>> =
|
||||
generateJobMatrix(checkSpecification);
|
||||
|
||||
const useAllPlatformBundle = checkSpecification.useAllPlatformBundle
|
||||
? checkSpecification.useAllPlatformBundle
|
||||
: "false";
|
||||
@@ -567,8 +566,8 @@ function generateJob(
|
||||
const checkJob: Record<string, any> = {
|
||||
strategy: {
|
||||
"fail-fast": false,
|
||||
matrix: {
|
||||
include: matrix,
|
||||
matrix: checkSpecification.matrix ?? {
|
||||
include: generateJobMatrix(checkSpecification),
|
||||
},
|
||||
},
|
||||
name: checkSpecification.name,
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"module": "preserve",
|
||||
"rootDir": "..",
|
||||
"sourceMap": false,
|
||||
"noEmit": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./*.ts", "../src/**/*.ts"],
|
||||
"include": ["./**/*.ts", "./**/*.mts", "../src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "changetool",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Validates change-notes and merges them into CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx index.ts",
|
||||
"test": "node --test --experimental-strip-types cli/*.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.2.0",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "preserve",
|
||||
"allowImportingTsExtensions": true,
|
||||
"rootDir": ".",
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
|
||||
import type { Config } from "./config-utils";
|
||||
import { Env, EnvVar, ActionsEnvVars } from "./environment";
|
||||
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
doesDirectoryExist,
|
||||
@@ -292,7 +292,7 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
|
||||
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
|
||||
* `/opt/hostedtoolcache`.
|
||||
*/
|
||||
export function isGitHubHostedRunner(env: Env = getEnv()) {
|
||||
export function isGitHubHostedRunner(env: ReadOnlyEnv = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
|
||||
}
|
||||
|
||||
|
||||
56
src/codeql-bundle.test.ts
Normal file
56
src/codeql-bundle.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import test from "ava";
|
||||
|
||||
import { getCodeQLBundleFromUrl } from "./codeql-bundle";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
|
||||
for (const [assetName, language] of [
|
||||
["codeql-bundle-java-linux64.tar.zst", BuiltInLanguage.java],
|
||||
["codeql-bundle-swift-osx64.tar.zst", BuiltInLanguage.swift],
|
||||
// Recognize unpublished language/platform combinations to keep them out of the toolcache.
|
||||
["codeql-bundle-csharp-win64.tar.gz", BuiltInLanguage.csharp],
|
||||
["codeql-bundle-java-kotlin-linux64.tar.zst", BuiltInLanguage.java],
|
||||
["codeql-bundle-%70ython-linux64.tar.zst", BuiltInLanguage.python],
|
||||
] as const) {
|
||||
test(`getCodeQLBundleFromUrl identifies ${assetName} without adding a fallback`, (t) => {
|
||||
const url = `https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${assetName}`;
|
||||
t.deepEqual(getCodeQLBundleFromUrl(url), {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("getCodeQLBundleFromUrl preserves encoding, query parameters and fragments", (t) => {
|
||||
const url =
|
||||
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/codeql-bundle-%70ython-linux64.tar.zst?download=1#asset";
|
||||
t.deepEqual(getCodeQLBundleFromUrl(url), {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language: BuiltInLanguage.python,
|
||||
});
|
||||
});
|
||||
|
||||
test("getCodeQLBundleFromUrl treats unrecognized assets as combined bundles", (t) => {
|
||||
for (const name of [
|
||||
"codeql-bundle-linux64.tar.zst",
|
||||
"codeql-bundle-osx64.tar.gz",
|
||||
"codeql-bundle-win64.tar.zst",
|
||||
// The all-platform bundle.
|
||||
"codeql-bundle.tar.gz",
|
||||
// A platform we do not publish per-language bundles for, whose name also contains a hyphen.
|
||||
"codeql-bundle-linux-arm64.tar.zst",
|
||||
// Not a language we know about.
|
||||
"codeql-bundle-cobol-linux64.tar.zst",
|
||||
// A name we cannot decode must not be mistaken for a language either.
|
||||
"codeql-bundle-%zz-linux64.tar.zst",
|
||||
]) {
|
||||
const url = `https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${name}`;
|
||||
t.deepEqual(getCodeQLBundleFromUrl(url), { kind: "combined", url });
|
||||
}
|
||||
});
|
||||
|
||||
test("getCodeQLBundleFromUrl preserves URLs it cannot parse", (t) => {
|
||||
const url = "not a url";
|
||||
t.deepEqual(getCodeQLBundleFromUrl(url), { kind: "combined", url });
|
||||
});
|
||||
33
src/codeql-bundle.ts
Normal file
33
src/codeql-bundle.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
|
||||
/** Describes the contents and location of a downloadable CodeQL bundle. */
|
||||
export type CodeQLBundle =
|
||||
| { kind: "combined"; url: string }
|
||||
| {
|
||||
kind: "per-language";
|
||||
url: string;
|
||||
language: BuiltInLanguage;
|
||||
/** Only set when the Action selected the bundle, allowing a same-version fallback. */
|
||||
combinedBundleURL?: string;
|
||||
};
|
||||
|
||||
const PER_LANGUAGE_BUNDLE_NAME =
|
||||
/^codeql-bundle-(.+)-(?:linux64|osx64|win64)\.tar\.(?:gz|zst)$/;
|
||||
|
||||
/** Classifies an explicit tools URL without changing it or adding a fallback. */
|
||||
export function getCodeQLBundleFromUrl(url: string): CodeQLBundle {
|
||||
let assetName: string;
|
||||
try {
|
||||
const pathname = new URL(url).pathname;
|
||||
// URL-encoded names must not bypass the toolcache safeguard.
|
||||
assetName = decodeURIComponent(pathname.split("/").pop() ?? "");
|
||||
} catch {
|
||||
return { kind: "combined", url };
|
||||
}
|
||||
|
||||
const match = assetName.match(PER_LANGUAGE_BUNDLE_NAME);
|
||||
const language = match ? parseBuiltInLanguage(match[1]) : undefined;
|
||||
return language === undefined
|
||||
? { kind: "combined", url }
|
||||
: { kind: "per-language", url, language };
|
||||
}
|
||||
@@ -90,7 +90,7 @@ async function installIntoToolcache({
|
||||
tmpDir: string;
|
||||
}) {
|
||||
const url = mockBundleDownloadApi({ apiDetails, isPinned, tagName });
|
||||
await codeql.setupCodeQL(
|
||||
return await codeql.setupCodeQL(
|
||||
cliVersion !== undefined ? undefined : url,
|
||||
apiDetails,
|
||||
tmpDir,
|
||||
@@ -259,6 +259,65 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
for (const { cliVersion, tagName, expectedToolcacheVersion } of [
|
||||
{
|
||||
cliVersion: "2.21.0",
|
||||
tagName: "codeql-bundle-20240101",
|
||||
expectedToolcacheVersion: "2.21.0",
|
||||
},
|
||||
{
|
||||
cliVersion: "2.21.0-rc.1",
|
||||
tagName: "codeql-bundle-20240101",
|
||||
expectedToolcacheVersion: "0.0.0-20240101",
|
||||
},
|
||||
{
|
||||
cliVersion: "2.21.0+20240101",
|
||||
tagName: "codeql-bundle-20240101",
|
||||
expectedToolcacheVersion: "0.0.0-20240101",
|
||||
},
|
||||
{
|
||||
cliVersion: "2.21.0",
|
||||
tagName: "custom-release",
|
||||
expectedToolcacheVersion: undefined,
|
||||
},
|
||||
]) {
|
||||
test.serial(
|
||||
`preserves CLI version ${cliVersion} when installing ${tagName}`,
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
|
||||
const result = await installIntoToolcache({
|
||||
cliVersion,
|
||||
isPinned: false,
|
||||
tagName,
|
||||
tmpDir,
|
||||
});
|
||||
|
||||
t.is(result.toolsVersion, cliVersion);
|
||||
t.is(result.toolsSource, ToolsSource.Download);
|
||||
t.true(
|
||||
Number.isInteger(result.toolsDownloadStatusReport?.totalDurationMs),
|
||||
);
|
||||
t.deepEqual(
|
||||
toolcache.findAllVersions("CodeQL"),
|
||||
expectedToolcacheVersion === undefined
|
||||
? []
|
||||
: [expectedToolcacheVersion],
|
||||
);
|
||||
if (expectedToolcacheVersion !== undefined) {
|
||||
const cachedFolder = toolcache.find(
|
||||
"CodeQL",
|
||||
expectedToolcacheVersion,
|
||||
);
|
||||
t.truthy(cachedFolder);
|
||||
t.true(fs.existsSync(`${cachedFolder}.complete`));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES = [
|
||||
{
|
||||
tagName: "codeql-bundle-2.17.6",
|
||||
|
||||
@@ -164,6 +164,11 @@ 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",
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
@@ -434,6 +439,11 @@ 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",
|
||||
|
||||
@@ -63,13 +63,13 @@ import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
ActionName,
|
||||
InitStatusReport,
|
||||
InitToolsDownloadFields,
|
||||
InitWithConfigStatusReport,
|
||||
createInitWithConfigStatusReport,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
} from "./status-report";
|
||||
import { createInitToolsDownloadFields } from "./status-report/tools-download";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
import { ToolsFeature } from "./tools-features";
|
||||
import { getCombinedTracerConfig } from "./tracer-config";
|
||||
@@ -168,23 +168,10 @@ async function sendCompletedStatusReport(
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
|
||||
const initToolsDownloadFields: InitToolsDownloadFields = {};
|
||||
|
||||
if (toolsDownloadStatusReport?.downloadDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_download_duration_ms =
|
||||
toolsDownloadStatusReport.downloadDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.extractionDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_extraction_duration_ms =
|
||||
toolsDownloadStatusReport.extractionDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.totalDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_total_duration_ms =
|
||||
toolsDownloadStatusReport.totalDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
const initToolsDownloadFields = createInitToolsDownloadFields(
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
);
|
||||
|
||||
if (config !== undefined) {
|
||||
// Append fields that are dependent on `config`
|
||||
|
||||
177
src/per-language-bundles.test.ts
Normal file
177
src/per-language-bundles.test.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import test from "ava";
|
||||
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import {
|
||||
getPerLanguageBundleLanguage,
|
||||
MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
|
||||
PerLanguageBundleOptions,
|
||||
} from "./per-language-bundles";
|
||||
import { BundlePlatform } from "./platform";
|
||||
import {
|
||||
createFeatures,
|
||||
getRecordingLogger,
|
||||
getTestEnv,
|
||||
initAllState,
|
||||
LoggedMessage,
|
||||
} from "./testing-utils";
|
||||
import { GitHubVariant } from "./util";
|
||||
|
||||
/** Options for which we would use a per-language bundle. */
|
||||
const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = {
|
||||
rawLanguages: ["java"],
|
||||
// Any version at least as new as the minimum will do.
|
||||
cliVersion: MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
|
||||
compressionMethod: "zstd",
|
||||
platform: BundlePlatform.Linux64,
|
||||
variant: GitHubVariant.DOTCOM,
|
||||
};
|
||||
|
||||
async function checkEligibility(
|
||||
overrides: Partial<PerLanguageBundleOptions>,
|
||||
stateOverrides: Partial<ReturnType<typeof initAllState>> = {},
|
||||
) {
|
||||
return getPerLanguageBundleLanguage(
|
||||
initAllState({
|
||||
env: getTestEnv({
|
||||
[ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted",
|
||||
}),
|
||||
features: createFeatures([Feature.PerLanguageBundles]),
|
||||
logger: getRecordingLogger([], { logToConsole: false }),
|
||||
...stateOverrides,
|
||||
}),
|
||||
{ ...ELIGIBLE_OPTIONS, ...overrides },
|
||||
);
|
||||
}
|
||||
|
||||
for (const platform of Object.values(BundlePlatform)) {
|
||||
test(`getPerLanguageBundleLanguage selects only supported languages on ${platform}`, async (t) => {
|
||||
for (const language of Object.values(BuiltInLanguage)) {
|
||||
const supported =
|
||||
language === BuiltInLanguage.swift
|
||||
? platform === BundlePlatform.Osx64
|
||||
: platform === BundlePlatform.Linux64;
|
||||
t.is(
|
||||
await checkEligibility({ rawLanguages: [language], platform }),
|
||||
supported ? language : undefined,
|
||||
language,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("getPerLanguageBundleLanguage normalizes aliases before selecting a bundle", async (t) => {
|
||||
t.is(
|
||||
await checkEligibility({ rawLanguages: ["java-kotlin"] }),
|
||||
BuiltInLanguage.java,
|
||||
);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage rejects unknown platforms", async (t) => {
|
||||
t.is(await checkEligibility({ platform: undefined }), undefined);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires exactly one language", async (t) => {
|
||||
t.is(await checkEligibility({ rawLanguages: undefined }), undefined);
|
||||
t.is(await checkEligibility({ rawLanguages: [] }), undefined);
|
||||
t.is(await checkEligibility({ rawLanguages: ["java", "python"] }), undefined);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires a known language", async (t) => {
|
||||
t.is(await checkEligibility({ rawLanguages: ["cobol"] }), undefined);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires a zstd bundle", async (t) => {
|
||||
t.is(await checkEligibility({ compressionMethod: "gzip" }), undefined);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires GitHub.com", async (t) => {
|
||||
// Other products resolve the combined bundle against their own instance, so asking for a
|
||||
// per-language bundle they do not mirror would move the download off that instance.
|
||||
for (const variant of [GitHubVariant.GHES, GitHubVariant.GHEC_DR]) {
|
||||
t.is(await checkEligibility({ variant }), undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires a GitHub-hosted runner", async (t) => {
|
||||
// A self-hosted runner may have a toolcache that persists between jobs, which is worth more than
|
||||
// a smaller download.
|
||||
t.is(
|
||||
await checkEligibility(
|
||||
{},
|
||||
{
|
||||
env: getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
|
||||
},
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Self-hosted runners are routinely configured to look like hosted ones, for example by mounting
|
||||
// a persistent volume at `/opt/hostedtoolcache`, so we require the service to tell us explicitly.
|
||||
t.is(
|
||||
await checkEligibility(
|
||||
{},
|
||||
{
|
||||
env: getTestEnv({ RUNNER_TOOL_CACHE: "/opt/hostedtoolcache" }),
|
||||
},
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("getPerLanguageBundleLanguage requires a supported release version", async (t) => {
|
||||
t.is(await checkEligibility({ cliVersion: undefined }), undefined);
|
||||
t.is(await checkEligibility({ cliVersion: "2.27.0" }), undefined);
|
||||
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({
|
||||
env: getTestEnv(),
|
||||
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 per_language_bundles 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);
|
||||
|
||||
for (const overrides of [
|
||||
{ rawLanguages: undefined },
|
||||
{ rawLanguages: ["java", "python"] },
|
||||
{ compressionMethod: "gzip" as const },
|
||||
{ platform: BundlePlatform.Osx64 },
|
||||
{ variant: GitHubVariant.GHES },
|
||||
{ variant: GitHubVariant.GHEC_DR },
|
||||
]) {
|
||||
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" }),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
130
src/per-language-bundles.ts
Normal file
130
src/per-language-bundles.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import { isGitHubHostedRunner } from "./actions-util";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { BundlePlatform } from "./platform";
|
||||
import * as tar from "./tar";
|
||||
import { GitHubVariant } from "./util";
|
||||
|
||||
/** Minimum CLI version for selecting a per-language release bundle. */
|
||||
export const MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION = "2.27.1";
|
||||
|
||||
/** Languages with per-language bundles published for each platform. */
|
||||
const PER_LANGUAGE_BUNDLE_LANGUAGES: Readonly<
|
||||
Record<BundlePlatform, ReadonlySet<BuiltInLanguage>>
|
||||
> = {
|
||||
[BundlePlatform.Linux64]: new Set([
|
||||
BuiltInLanguage.actions,
|
||||
BuiltInLanguage.cpp,
|
||||
BuiltInLanguage.csharp,
|
||||
BuiltInLanguage.go,
|
||||
BuiltInLanguage.java,
|
||||
BuiltInLanguage.javascript,
|
||||
BuiltInLanguage.python,
|
||||
BuiltInLanguage.ruby,
|
||||
BuiltInLanguage.rust,
|
||||
]),
|
||||
[BundlePlatform.LinuxArm64]: new Set(),
|
||||
[BundlePlatform.Osx64]: new Set([BuiltInLanguage.swift]),
|
||||
[BundlePlatform.Win64]: new Set(),
|
||||
};
|
||||
|
||||
/** Inputs that determine whether we may download a per-language bundle. */
|
||||
export interface PerLanguageBundleOptions {
|
||||
/** Explicit input only: autodetection needs a CLI instance. */
|
||||
rawLanguages: string[] | undefined;
|
||||
/** Requested CLI version, if known. Ignored when requesting the latest nightly. */
|
||||
cliVersion: string | undefined;
|
||||
compressionMethod: tar.CompressionMethod;
|
||||
/** Platform for which the bundle is requested. */
|
||||
platform: BundlePlatform | undefined;
|
||||
variant: GitHubVariant;
|
||||
/** Whether the Action is selecting the latest nightly rather than a release version. */
|
||||
isLatestNightly?: boolean;
|
||||
}
|
||||
|
||||
/** Returns the eligible bundle language, or undefined for the combined bundle. */
|
||||
export async function getPerLanguageBundleLanguage(
|
||||
{
|
||||
env,
|
||||
features,
|
||||
logger,
|
||||
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
|
||||
options: PerLanguageBundleOptions,
|
||||
): Promise<BuiltInLanguage | undefined> {
|
||||
const {
|
||||
rawLanguages,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform,
|
||||
variant,
|
||||
isLatestNightly,
|
||||
} = options;
|
||||
|
||||
const explain = (reason: string) => {
|
||||
logger.debug(`Not using a per-language CodeQL bundle since ${reason}.`);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
if (!(await features.getValue(Feature.PerLanguageBundles))) {
|
||||
return explain(`the ${Feature.PerLanguageBundles} feature is disabled`);
|
||||
}
|
||||
|
||||
if (rawLanguages?.length !== 1) {
|
||||
return explain(
|
||||
`exactly one language must be requested via the 'languages' input, but ${
|
||||
rawLanguages?.length ?? 0
|
||||
} were`,
|
||||
);
|
||||
}
|
||||
|
||||
const language = parseBuiltInLanguage(rawLanguages[0]);
|
||||
if (language === undefined) {
|
||||
return explain(`'${rawLanguages[0]}' is not a known CodeQL language`);
|
||||
}
|
||||
|
||||
if (compressionMethod !== "zstd") {
|
||||
// Per-language bundles are only published as zstd archives.
|
||||
return explain(`the bundle would be downloaded as '${compressionMethod}'`);
|
||||
}
|
||||
|
||||
if (variant !== GitHubVariant.DOTCOM) {
|
||||
// Tenant mirrors may lack these assets, and an unreachable github.com fails with a
|
||||
// connection error rather than a recoverable 404.
|
||||
return explain(`we are running against ${variant}`);
|
||||
}
|
||||
|
||||
if (!isGitHubHostedRunner(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");
|
||||
}
|
||||
|
||||
// Check whether per-language bundles are published for the requested CLI version.
|
||||
// Latest-nightly selection skips this release-version check, but not the other eligibility checks.
|
||||
if (!isLatestNightly) {
|
||||
if (cliVersion === undefined) {
|
||||
return explain("the requested CLI version is unknown");
|
||||
}
|
||||
|
||||
if (!semver.gte(cliVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION)) {
|
||||
return explain(
|
||||
`the requested CodeQL version ${cliVersion} is older than ${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}, which is the ` +
|
||||
"first version for which per-language bundles are published",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
platform === undefined ||
|
||||
!PER_LANGUAGE_BUNDLE_LANGUAGES[platform].has(language)
|
||||
) {
|
||||
return explain(
|
||||
`no per-language bundle is published for ${language} on ${platform ?? "an unknown platform"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
18
src/platform.test.ts
Normal file
18
src/platform.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import test from "ava";
|
||||
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
|
||||
for (const [platform, arch, expected] of [
|
||||
["linux", "x64", BundlePlatform.Linux64],
|
||||
["linux", "arm64", BundlePlatform.LinuxArm64],
|
||||
["linux", "ia32", BundlePlatform.Linux64],
|
||||
["darwin", "x64", BundlePlatform.Osx64],
|
||||
["darwin", "arm64", BundlePlatform.Osx64],
|
||||
["win32", "x64", BundlePlatform.Win64],
|
||||
["win32", "arm64", BundlePlatform.Win64],
|
||||
["freebsd", "x64", undefined],
|
||||
] as const) {
|
||||
test(`getBundlePlatform maps ${platform}/${arch} to ${expected ?? "an all-platform bundle"}`, (t) => {
|
||||
t.is(getBundlePlatform(platform, arch), expected);
|
||||
});
|
||||
}
|
||||
26
src/platform.ts
Normal file
26
src/platform.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/** Platform identifiers used in CodeQL bundle asset names. */
|
||||
export enum BundlePlatform {
|
||||
Linux64 = "linux64",
|
||||
LinuxArm64 = "linux-arm64",
|
||||
Osx64 = "osx64",
|
||||
Win64 = "win64",
|
||||
}
|
||||
|
||||
/** Returns the bundle platform, or undefined when an all-platform bundle is required. */
|
||||
export function getBundlePlatform(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
arch: NodeJS.Architecture = process.arch,
|
||||
): BundlePlatform | undefined {
|
||||
switch (platform) {
|
||||
case "win32":
|
||||
return BundlePlatform.Win64;
|
||||
case "linux":
|
||||
return arch === "arm64"
|
||||
? BundlePlatform.LinuxArm64
|
||||
: BundlePlatform.Linux64;
|
||||
case "darwin":
|
||||
return BundlePlatform.Osx64;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,11 @@ import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
ActionName,
|
||||
InitStatusReport,
|
||||
InitToolsDownloadFields,
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
} from "./status-report";
|
||||
import { createInitToolsDownloadFields } from "./status-report/tools-download";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
import {
|
||||
checkDiskUsage,
|
||||
@@ -79,23 +79,10 @@ async function sendCompletedStatusReport(
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
|
||||
const initToolsDownloadFields: InitToolsDownloadFields = {};
|
||||
|
||||
if (toolsDownloadStatusReport?.downloadDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_download_duration_ms =
|
||||
toolsDownloadStatusReport.downloadDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.extractionDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_extraction_duration_ms =
|
||||
toolsDownloadStatusReport.extractionDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.totalDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_total_duration_ms =
|
||||
toolsDownloadStatusReport.totalDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
const initToolsDownloadFields = createInitToolsDownloadFields(
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
);
|
||||
|
||||
await sendStatusReport({ ...initStatusReport, ...initToolsDownloadFields });
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import { OutgoingHttpHeaders } from "http";
|
||||
import * as path from "path";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
isRunningLocalAction,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { CodeQLBundle, getCodeQLBundleFromUrl } from "./codeql-bundle";
|
||||
import * as defaults from "./defaults.json";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
@@ -30,8 +32,11 @@ import {
|
||||
Feature,
|
||||
FeatureEnablement,
|
||||
} from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
||||
import { getPerLanguageBundleLanguage } from "./per-language-bundles";
|
||||
import { getBundlePlatform } from "./platform";
|
||||
import * as tar from "./tar";
|
||||
import {
|
||||
deleteToolcacheBundles,
|
||||
@@ -72,21 +77,25 @@ function getCodeQLBundleExtension(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the CodeQL bundle asset to download.
|
||||
*
|
||||
* @param compressionMethod The compression method of the bundle.
|
||||
* @param language Optional language for a per-language bundle. If omitted, returns a combined bundle name.
|
||||
*/
|
||||
export function getCodeQLBundleName(
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
language?: BuiltInLanguage,
|
||||
): string {
|
||||
const extension = getCodeQLBundleExtension(compressionMethod);
|
||||
const platform = getBundlePlatform();
|
||||
|
||||
let platform: string;
|
||||
if (process.platform === "win32") {
|
||||
platform = "win64";
|
||||
} else if (process.platform === "linux") {
|
||||
platform = process.arch === "arm64" ? "linux-arm64" : "linux64";
|
||||
} else if (process.platform === "darwin") {
|
||||
platform = "osx64";
|
||||
} else {
|
||||
if (platform === undefined) {
|
||||
return `codeql-bundle${extension}`;
|
||||
}
|
||||
if (language !== undefined) {
|
||||
return `codeql-bundle-${language}-${platform}${extension}`;
|
||||
}
|
||||
return `codeql-bundle-${platform}${extension}`;
|
||||
}
|
||||
|
||||
@@ -107,7 +116,7 @@ export function getCodeQLActionRepository(logger: Logger): string {
|
||||
async function getCodeQLBundleDownloadURL(
|
||||
tagName: string,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
codeQLBundleName: string,
|
||||
logger: Logger,
|
||||
): Promise<string> {
|
||||
const codeQLActionRepository = getCodeQLActionRepository(logger);
|
||||
@@ -126,7 +135,6 @@ async function getCodeQLBundleDownloadURL(
|
||||
return !self.slice(0, index).some((other) => deepEqual(source, other));
|
||||
},
|
||||
);
|
||||
const codeQLBundleName = getCodeQLBundleName(compressionMethod);
|
||||
for (const downloadSource of uniqueDownloadSources) {
|
||||
const [apiURL, repository] = downloadSource;
|
||||
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
|
||||
@@ -164,7 +172,7 @@ function tryGetBundleVersionFromTagName(
|
||||
tagName: string,
|
||||
logger: Logger,
|
||||
): string | undefined {
|
||||
const match = tagName.match(/^codeql-bundle-(.*)$/);
|
||||
const match = tagName.match(/^codeql-bundle-(.+)$/);
|
||||
if (match === null || match.length < 2) {
|
||||
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
|
||||
return undefined;
|
||||
@@ -215,6 +223,22 @@ export function convertToSemVer(version: string, logger: Logger): string {
|
||||
return s;
|
||||
}
|
||||
|
||||
/** A resolved download, including its bundle identity and version. */
|
||||
export interface CodeQLDownloadSource {
|
||||
/** Distinguishes downloads from local archives and cached installations. */
|
||||
sourceType: "download";
|
||||
/** The bundle to download. */
|
||||
bundle: CodeQLBundle;
|
||||
/** The compression format of the bundle archive. */
|
||||
compressionMethod: tar.CompressionMethod;
|
||||
/** Bundle version of the tools, if known. */
|
||||
bundleVersion?: string;
|
||||
/** Requested CLI version, if known. */
|
||||
cliVersion?: string;
|
||||
/** Resolved version for telemetry, independent of whether the bundle can be cached. */
|
||||
toolsVersion: string;
|
||||
}
|
||||
|
||||
export type CodeQLToolsSource =
|
||||
| {
|
||||
codeqlTarPath: string;
|
||||
@@ -229,17 +253,7 @@ export type CodeQLToolsSource =
|
||||
/** Human-readable description of the source of the tools for telemetry purposes. */
|
||||
toolsVersion: string;
|
||||
}
|
||||
| {
|
||||
/** Bundle version of the tools, if known. */
|
||||
bundleVersion?: string;
|
||||
/** CLI version of the tools, if known. */
|
||||
cliVersion?: string;
|
||||
compressionMethod: tar.CompressionMethod;
|
||||
codeqlURL: string;
|
||||
sourceType: "download";
|
||||
/** Human-readable description of the source of the tools for telemetry purposes. */
|
||||
toolsVersion: string;
|
||||
};
|
||||
| CodeQLDownloadSource;
|
||||
|
||||
/**
|
||||
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
|
||||
@@ -448,7 +462,7 @@ export async function getCodeQLSource(
|
||||
};
|
||||
}
|
||||
|
||||
/** CLI version number, for example 2.12.6. */
|
||||
/** Requested CLI version number, for example 2.12.6. */
|
||||
let cliVersion: string | undefined;
|
||||
/** Tag name of the CodeQL bundle, for example `codeql-bundle-20230120`. */
|
||||
let tagName: string | undefined;
|
||||
@@ -458,6 +472,7 @@ export async function getCodeQLSource(
|
||||
* This does not always include a tag name.
|
||||
*/
|
||||
let url: string | undefined;
|
||||
let bundle: CodeQLBundle | undefined;
|
||||
|
||||
// We allow forcing the nightly CLI via the FF for `dynamic` events (or in test mode) where the
|
||||
// `tools` input cannot be adjusted to explicitly request it.
|
||||
@@ -466,7 +481,8 @@ export async function getCodeQLSource(
|
||||
const forceNightly = forceNightlyValueFF && canForceNightlyWithFF;
|
||||
|
||||
// For advanced workflows, a value from `CODEQL_NIGHTLY_TOOLS_INPUTS` can be specified explicitly
|
||||
// for the `tools` input in the workflow file.
|
||||
// for the `tools` input. This is the computed input, so it may come from the repository property
|
||||
// rather than the workflow file.
|
||||
const nightlyRequestedByToolsInput =
|
||||
toolsInput !== undefined &&
|
||||
CODEQL_NIGHTLY_TOOLS_INPUTS.includes(toolsInput);
|
||||
@@ -500,7 +516,12 @@ export async function getCodeQLSource(
|
||||
`Using the latest CodeQL CLI nightly, as requested by 'tools: ${toolsInput}'.`,
|
||||
);
|
||||
}
|
||||
toolsInput = await getNightlyToolsUrl(logger);
|
||||
bundle = await getLatestNightlyBundle(
|
||||
{ env: getEnv(), features, logger },
|
||||
rawLanguages,
|
||||
variant,
|
||||
);
|
||||
toolsInput = bundle.url;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,7 +604,7 @@ export async function getCodeQLSource(
|
||||
if (tagName) {
|
||||
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
|
||||
// If the bundle version is a semantic version, it is a CLI version number.
|
||||
if (bundleVersion && semver.valid(bundleVersion)) {
|
||||
if (bundleVersion !== undefined && semver.valid(bundleVersion)) {
|
||||
cliVersion = convertToSemVer(bundleVersion, logger);
|
||||
}
|
||||
}
|
||||
@@ -600,13 +621,15 @@ export async function getCodeQLSource(
|
||||
}
|
||||
|
||||
const bundleVersion =
|
||||
tagName && tryGetBundleVersionFromTagName(tagName, logger);
|
||||
const humanReadableVersion =
|
||||
tagName !== undefined
|
||||
? tryGetBundleVersionFromTagName(tagName, logger)
|
||||
: undefined;
|
||||
const resolvedVersion =
|
||||
cliVersion ??
|
||||
(bundleVersion && convertToSemVer(bundleVersion, logger)) ??
|
||||
tagName ??
|
||||
url ??
|
||||
"unknown";
|
||||
(bundleVersion !== undefined
|
||||
? convertToSemVer(bundleVersion, logger)
|
||||
: undefined);
|
||||
const humanReadableVersion = resolvedVersion ?? tagName ?? url ?? "unknown";
|
||||
|
||||
logger.debug(
|
||||
"Attempting to obtain CodeQL tools. " +
|
||||
@@ -721,18 +744,55 @@ export async function getCodeQLSource(
|
||||
let compressionMethod: tar.CompressionMethod;
|
||||
|
||||
if (!url) {
|
||||
const bundleTagName = tagName;
|
||||
if (bundleTagName === undefined) {
|
||||
throw new Error(
|
||||
"Could not determine a release tag for the requested CodeQL bundle.",
|
||||
);
|
||||
}
|
||||
|
||||
compressionMethod =
|
||||
cliVersion !== undefined &&
|
||||
(await useZstdBundle(cliVersion, tarSupportsZstd))
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
url = await getCodeQLBundleDownloadURL(
|
||||
tagName!,
|
||||
apiDetails,
|
||||
compressionMethod,
|
||||
logger,
|
||||
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
|
||||
{ env: getEnv(), features, logger },
|
||||
{
|
||||
rawLanguages,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform: getBundlePlatform(),
|
||||
variant,
|
||||
},
|
||||
);
|
||||
|
||||
// Resolves the combined or per-language bundle URL for the requested release.
|
||||
const resolveBundleURL = (language?: BuiltInLanguage) =>
|
||||
getCodeQLBundleDownloadURL(
|
||||
bundleTagName,
|
||||
apiDetails,
|
||||
getCodeQLBundleName(compressionMethod, language),
|
||||
logger,
|
||||
);
|
||||
|
||||
const combinedBundleURL = await resolveBundleURL();
|
||||
if (perLanguageBundleLanguage !== undefined) {
|
||||
logger.info(
|
||||
`Selected the per-language CodeQL bundle for '${perLanguageBundleLanguage}'.`,
|
||||
);
|
||||
url = await resolveBundleURL(perLanguageBundleLanguage);
|
||||
bundle = {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language: perLanguageBundleLanguage,
|
||||
combinedBundleURL,
|
||||
};
|
||||
} else {
|
||||
url = combinedBundleURL;
|
||||
bundle = { kind: "combined", url };
|
||||
}
|
||||
} else {
|
||||
const method = tar.inferCompressionMethod(url);
|
||||
if (method === undefined) {
|
||||
@@ -742,6 +802,13 @@ export async function getCodeQLSource(
|
||||
);
|
||||
}
|
||||
compressionMethod = method;
|
||||
|
||||
bundle ??= getCodeQLBundleFromUrl(url);
|
||||
if (bundle.kind === "per-language") {
|
||||
logger.info(
|
||||
`${url} appears to be a CodeQL bundle that contains only ${bundle.language}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (cliVersion) {
|
||||
@@ -750,12 +817,12 @@ export async function getCodeQLSource(
|
||||
logger.info(`Using CodeQL CLI sourced from ${url} .`);
|
||||
}
|
||||
return {
|
||||
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
|
||||
bundle,
|
||||
bundleVersion,
|
||||
cliVersion,
|
||||
codeqlURL: url,
|
||||
compressionMethod,
|
||||
sourceType: "download",
|
||||
toolsVersion: cliVersion ?? humanReadableVersion,
|
||||
toolsVersion: resolvedVersion ?? "unknown",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -769,7 +836,7 @@ async function tryGetFallbackToolcacheVersion(
|
||||
logger: Logger,
|
||||
): Promise<string | undefined> {
|
||||
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
|
||||
if (!bundleVersion) {
|
||||
if (bundleVersion === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const fallbackVersion = convertToSemVer(bundleVersion, logger);
|
||||
@@ -783,20 +850,17 @@ async function tryGetFallbackToolcacheVersion(
|
||||
// Exported using `export const` for testing purposes. Specifically, we want to
|
||||
// be able to stub this function and have other functions in this file use that stub.
|
||||
export const downloadCodeQL = async function (
|
||||
codeqlURL: string,
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
maybeBundleVersion: string | undefined,
|
||||
maybeCliVersion: string | undefined,
|
||||
source: CodeQLDownloadSource,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
tarVersion: tar.TarVersion | undefined,
|
||||
tempDir: string,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
): Promise<{
|
||||
codeqlFolder: string;
|
||||
statusReport: ToolsDownloadStatusReport;
|
||||
toolsVersion: string;
|
||||
}> {
|
||||
const { bundle, compressionMethod } = source;
|
||||
const codeqlURL = bundle.url;
|
||||
const parsedCodeQLURL = new URL(codeqlURL);
|
||||
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
|
||||
const headers: OutgoingHttpHeaders = {
|
||||
@@ -815,17 +879,11 @@ export const downloadCodeQL = async function (
|
||||
);
|
||||
}
|
||||
|
||||
const toolcacheInfo = getToolcacheDestinationInfo(
|
||||
maybeBundleVersion,
|
||||
maybeCliVersion,
|
||||
logger,
|
||||
const toolcacheDestination = getToolcacheDestination({ logger }, source);
|
||||
const extractedBundlePath = toolcacheDestination.orElse(
|
||||
getTempExtractionDir(tempDir),
|
||||
);
|
||||
|
||||
const extractedBundlePath =
|
||||
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
|
||||
|
||||
await tryDeleteToolcacheBundles({ env: getEnv(), features, logger });
|
||||
|
||||
const statusReport = await downloadAndExtract(
|
||||
codeqlURL,
|
||||
compressionMethod,
|
||||
@@ -836,46 +894,53 @@ export const downloadCodeQL = async function (
|
||||
logger,
|
||||
);
|
||||
|
||||
if (!toolcacheInfo) {
|
||||
logger.debug(
|
||||
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
|
||||
`URL ${codeqlURL}.`,
|
||||
);
|
||||
return {
|
||||
codeqlFolder: extractedBundlePath,
|
||||
statusReport,
|
||||
toolsVersion: maybeCliVersion ?? "unknown",
|
||||
};
|
||||
if (toolcacheDestination.isSuccess()) {
|
||||
writeToolcacheMarkerFile(toolcacheDestination.value, logger);
|
||||
} else {
|
||||
logger.debug(toolcacheDestination.value);
|
||||
}
|
||||
|
||||
writeToolcacheMarkerFile(toolcacheInfo.path, logger);
|
||||
|
||||
return {
|
||||
codeqlFolder: extractedBundlePath,
|
||||
statusReport,
|
||||
toolsVersion: maybeCliVersion ?? toolcacheInfo.version,
|
||||
statusReport:
|
||||
bundle.kind === "per-language"
|
||||
? {
|
||||
...statusReport,
|
||||
perLanguage: { tools_bundle_language: bundle.language },
|
||||
}
|
||||
: statusReport,
|
||||
};
|
||||
};
|
||||
|
||||
function getToolcacheDestinationInfo(
|
||||
maybeBundleVersion: string | undefined,
|
||||
maybeCliVersion: string | undefined,
|
||||
logger: Logger,
|
||||
): { path: string; version: string } | undefined {
|
||||
if (maybeBundleVersion) {
|
||||
const version = getCanonicalToolcacheVersion(
|
||||
maybeCliVersion,
|
||||
maybeBundleVersion,
|
||||
logger,
|
||||
/**
|
||||
* Returns the canonical toolcache directory, or the reason the bundle cannot be cached.
|
||||
*/
|
||||
function getToolcacheDestination(
|
||||
{ logger }: ActionState<["Logger"]>,
|
||||
source: CodeQLDownloadSource,
|
||||
): util.Result<string, string> {
|
||||
if (source.bundle.kind !== "combined") {
|
||||
return new util.Failure(
|
||||
"Not caching the CodeQL tools because they came from a bundle that contains only a " +
|
||||
"single language.",
|
||||
);
|
||||
}
|
||||
if (!source.bundleVersion) {
|
||||
return new util.Failure(
|
||||
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
|
||||
`URL ${source.bundle.url}.`,
|
||||
);
|
||||
|
||||
return {
|
||||
path: getToolcacheDirectory(version),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return new util.Success(
|
||||
getToolcacheDirectory(
|
||||
getCanonicalToolcacheVersion(
|
||||
source.cliVersion,
|
||||
source.bundleVersion,
|
||||
logger,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1000,7 +1065,6 @@ export async function setupCodeQLBundle(
|
||||
);
|
||||
|
||||
let codeqlFolder: string;
|
||||
let toolsVersion = source.toolsVersion;
|
||||
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
|
||||
let toolsSource: ToolsSource;
|
||||
switch (source.sourceType) {
|
||||
@@ -1021,18 +1085,13 @@ export async function setupCodeQLBundle(
|
||||
toolsSource = ToolsSource.Toolcache;
|
||||
break;
|
||||
case "download": {
|
||||
const result = await downloadCodeQL(
|
||||
source.codeqlURL,
|
||||
source.compressionMethod,
|
||||
source.bundleVersion,
|
||||
source.cliVersion,
|
||||
const result = await downloadCodeQLBundle(
|
||||
{ env: getEnv(), features, logger },
|
||||
source,
|
||||
apiDetails,
|
||||
zstdAvailability.version,
|
||||
tempDir,
|
||||
features,
|
||||
logger,
|
||||
);
|
||||
toolsVersion = result.toolsVersion;
|
||||
codeqlFolder = result.codeqlFolder;
|
||||
toolsDownloadStatusReport = result.statusReport;
|
||||
toolsSource = ToolsSource.Download;
|
||||
@@ -1050,10 +1109,77 @@ export async function setupCodeQLBundle(
|
||||
codeqlFolder,
|
||||
toolsDownloadStatusReport,
|
||||
toolsSource,
|
||||
toolsVersion,
|
||||
toolsVersion: source.toolsVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs eligible toolcache cleanup once, then downloads and extracts the resolved bundle.
|
||||
*
|
||||
* If an automatically selected per-language bundle is missing, downloads the combined bundle
|
||||
* from the same release instead. Explicit bundle URLs are not substituted.
|
||||
*
|
||||
* @returns The extraction directory and download timings.
|
||||
*/
|
||||
export async function downloadCodeQLBundle(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
|
||||
source: CodeQLDownloadSource,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
tarVersion: tar.TarVersion | undefined,
|
||||
tempDir: string,
|
||||
): Promise<{
|
||||
codeqlFolder: string;
|
||||
statusReport: ToolsDownloadStatusReport;
|
||||
}> {
|
||||
const { bundle } = source;
|
||||
const { logger } = action;
|
||||
|
||||
await tryDeleteToolcacheBundles(action);
|
||||
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
return await downloadCodeQL(
|
||||
source,
|
||||
apiDetails,
|
||||
tarVersion,
|
||||
tempDir,
|
||||
logger,
|
||||
);
|
||||
} catch (e) {
|
||||
if (
|
||||
bundle.kind !== "per-language" ||
|
||||
bundle.combinedBundleURL === undefined ||
|
||||
util.asHTTPError(e)?.status !== 404
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
logger.warning(
|
||||
`No per-language CodeQL bundle for '${bundle.language}' was found at ${bundle.url}, so ` +
|
||||
"falling back to the bundle that contains all languages. This analysis will still " +
|
||||
"produce correct results, but will take longer to set up.",
|
||||
);
|
||||
|
||||
const result = await downloadCodeQL(
|
||||
{
|
||||
...source,
|
||||
bundle: { kind: "combined", url: bundle.combinedBundleURL },
|
||||
},
|
||||
apiDetails,
|
||||
tarVersion,
|
||||
tempDir,
|
||||
logger,
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
statusReport: {
|
||||
...result.statusReport,
|
||||
totalDurationMs: util.durationMsSince(startTime),
|
||||
perLanguage: { tools_per_language_bundle_fallback: true },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function useZstdBundle(
|
||||
cliVersion: string,
|
||||
tarSupportsZstd: boolean,
|
||||
@@ -1071,9 +1197,15 @@ function getTempExtractionDir(tempDir: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the latest nightly CodeQL bundle.
|
||||
* Selects a bundle from the latest nightly release, preferring a per-language bundle when eligible.
|
||||
* Records the combined bundle URL from that release for use if the selected asset is missing.
|
||||
*/
|
||||
async function getNightlyToolsUrl(logger: Logger) {
|
||||
async function getLatestNightlyBundle(
|
||||
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
|
||||
rawLanguages: string[] | undefined,
|
||||
variant: util.GitHubVariant,
|
||||
): Promise<CodeQLBundle> {
|
||||
const { logger } = action;
|
||||
const zstdAvailability = await tar.isZstdAvailable(logger);
|
||||
// The nightly is guaranteed to have a zstd bundle
|
||||
const compressionMethod = (await useZstdBundle(
|
||||
@@ -1083,6 +1215,15 @@ async function getNightlyToolsUrl(logger: Logger) {
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
const language = await getPerLanguageBundleLanguage(action, {
|
||||
rawLanguages,
|
||||
cliVersion: undefined,
|
||||
compressionMethod,
|
||||
platform: getBundlePlatform(),
|
||||
variant,
|
||||
isLatestNightly: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// Since nightlies are prereleases, we can't just download the latest release
|
||||
// on the repository. So instead we need to find the latest pre-release
|
||||
@@ -1098,7 +1239,17 @@ async function getNightlyToolsUrl(logger: Logger) {
|
||||
if (!latestRelease) {
|
||||
throw new Error("Could not find the latest nightly release.");
|
||||
}
|
||||
return `https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${getCodeQLBundleName(compressionMethod)}`;
|
||||
const assetUrl = (name: string) =>
|
||||
`https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${name}`;
|
||||
const url = assetUrl(getCodeQLBundleName(compressionMethod, language));
|
||||
return language === undefined
|
||||
? { kind: "combined", url }
|
||||
: {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language,
|
||||
combinedBundleURL: assetUrl(getCodeQLBundleName(compressionMethod)),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to retrieve the latest nightly release: ${util.wrapError(e)}`,
|
||||
|
||||
@@ -624,29 +624,6 @@ export interface InitWithConfigStatusReport extends InitStatusReport {
|
||||
config_file: string;
|
||||
}
|
||||
|
||||
/** Fields of the init status report populated when the tools source is `download`. */
|
||||
export interface InitToolsDownloadFields {
|
||||
/**
|
||||
* Time taken to download the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
tools_download_duration_ms?: number;
|
||||
/**
|
||||
* Time taken to extract the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
tools_extraction_duration_ms?: number;
|
||||
/**
|
||||
* Total time taken to make the bundle available on disk, in milliseconds. This includes any time
|
||||
* spent on a streaming attempt that failed and fell back to downloading before extracting.
|
||||
*/
|
||||
tools_total_duration_ms?: number;
|
||||
/**
|
||||
* Whether the relevant tools dotcom feature flags have been misconfigured.
|
||||
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
|
||||
tools_feature_flags_valid?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes a `InitWithConfigStatusReport` from the given values.
|
||||
*
|
||||
|
||||
80
src/status-report/tools-download.test.ts
Normal file
80
src/status-report/tools-download.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import test from "ava";
|
||||
|
||||
import { BuiltInLanguage } from "../languages";
|
||||
|
||||
import { createInitToolsDownloadFields } from "./tools-download";
|
||||
|
||||
test("createInitToolsDownloadFields omits absent download data", (t) => {
|
||||
t.deepEqual(createInitToolsDownloadFields(undefined, undefined), {});
|
||||
});
|
||||
|
||||
test("createInitToolsDownloadFields reports feature flags without a download", (t) => {
|
||||
t.deepEqual(createInitToolsDownloadFields(undefined, false), {
|
||||
tools_feature_flags_valid: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("createInitToolsDownloadFields reports only the total for a streaming download", (t) => {
|
||||
t.deepEqual(
|
||||
createInitToolsDownloadFields({ totalDurationMs: 300 }, undefined),
|
||||
{ tools_total_duration_ms: 300 },
|
||||
);
|
||||
});
|
||||
|
||||
test("createInitToolsDownloadFields preserves per-language metadata", (t) => {
|
||||
t.deepEqual(
|
||||
createInitToolsDownloadFields(
|
||||
{
|
||||
totalDurationMs: 300,
|
||||
perLanguage: { tools_bundle_language: BuiltInLanguage.java },
|
||||
},
|
||||
true,
|
||||
),
|
||||
{
|
||||
tools_total_duration_ms: 300,
|
||||
tools_bundle_language: BuiltInLanguage.java,
|
||||
tools_feature_flags_valid: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("createInitToolsDownloadFields preserves fallback and per-attempt timings", (t) => {
|
||||
t.deepEqual(
|
||||
createInitToolsDownloadFields(
|
||||
{
|
||||
downloadDurationMs: 200,
|
||||
extractionDurationMs: 100,
|
||||
totalDurationMs: 1000,
|
||||
perLanguage: { tools_per_language_bundle_fallback: true },
|
||||
},
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
tools_download_duration_ms: 200,
|
||||
tools_extraction_duration_ms: 100,
|
||||
tools_total_duration_ms: 1000,
|
||||
tools_per_language_bundle_fallback: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("createInitToolsDownloadFields preserves zero durations and false flags", (t) => {
|
||||
t.deepEqual(
|
||||
createInitToolsDownloadFields(
|
||||
{
|
||||
downloadDurationMs: 0,
|
||||
extractionDurationMs: 0,
|
||||
totalDurationMs: 0,
|
||||
perLanguage: { tools_per_language_bundle_fallback: false },
|
||||
},
|
||||
false,
|
||||
),
|
||||
{
|
||||
tools_download_duration_ms: 0,
|
||||
tools_extraction_duration_ms: 0,
|
||||
tools_total_duration_ms: 0,
|
||||
tools_per_language_bundle_fallback: false,
|
||||
tools_feature_flags_valid: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
56
src/status-report/tools-download.ts
Normal file
56
src/status-report/tools-download.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { ToolsDownloadStatusReport } from "../tools-download";
|
||||
|
||||
/** Telemetry describing per-language bundle downloads. */
|
||||
export interface PerLanguageToolsStatusReport {
|
||||
/** The language of the single-language bundle that was downloaded, if any. */
|
||||
tools_bundle_language?: string;
|
||||
/**
|
||||
* Whether we tried to download a single-language bundle, but it did not exist and we fell back to
|
||||
* the combined bundle.
|
||||
*/
|
||||
tools_per_language_bundle_fallback?: boolean;
|
||||
}
|
||||
|
||||
/** Fields of the init status report populated when the tools source is `download`. */
|
||||
export interface InitToolsDownloadFields extends PerLanguageToolsStatusReport {
|
||||
/**
|
||||
* Time taken to download the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
tools_download_duration_ms?: number;
|
||||
/**
|
||||
* Time taken to extract the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
tools_extraction_duration_ms?: number;
|
||||
/**
|
||||
* Total time taken to make the bundle available on disk, including failed download attempts
|
||||
* before a fallback, in milliseconds.
|
||||
*/
|
||||
tools_total_duration_ms?: number;
|
||||
/**
|
||||
* Whether the relevant tools dotcom feature flags have been misconfigured.
|
||||
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
|
||||
tools_feature_flags_valid?: boolean;
|
||||
}
|
||||
|
||||
/** Converts download results to telemetry fields shared by the init and setup-codeql Actions. */
|
||||
export function createInitToolsDownloadFields(
|
||||
report: ToolsDownloadStatusReport | undefined,
|
||||
toolsFeatureFlagsValid: boolean | undefined,
|
||||
): InitToolsDownloadFields {
|
||||
const fields: InitToolsDownloadFields = { ...report?.perLanguage };
|
||||
if (report?.downloadDurationMs !== undefined) {
|
||||
fields.tools_download_duration_ms = report.downloadDurationMs;
|
||||
}
|
||||
if (report?.extractionDurationMs !== undefined) {
|
||||
fields.tools_extraction_duration_ms = report.extractionDurationMs;
|
||||
}
|
||||
if (report?.totalDurationMs !== undefined) {
|
||||
fields.tools_total_duration_ms = report.totalDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
fields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { getBundlePlatform } from "./platform";
|
||||
import { ActionName } from "./status-report";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
@@ -933,21 +934,14 @@ export function mockBundleDownloadApi({
|
||||
platformSpecific?: boolean;
|
||||
tagName: string;
|
||||
}): string {
|
||||
const platform =
|
||||
process.platform === "win32"
|
||||
? "win64"
|
||||
: process.platform === "linux"
|
||||
? process.arch === "arm64"
|
||||
? "linux-arm64"
|
||||
: "linux64"
|
||||
: "osx64";
|
||||
const platform = platformSpecific ? getBundlePlatform() : undefined;
|
||||
|
||||
const baseUrl = apiDetails?.url ?? "https://example.com";
|
||||
|
||||
const bundleUrls = ["tar.gz", "tar.zst"].map((extension) => {
|
||||
const relativeUrl = apiDetails
|
||||
? `/${repo}/releases/download/${tagName}/codeql-bundle${
|
||||
platformSpecific ? `-${platform}` : ""
|
||||
platform !== undefined ? `-${platform}` : ""
|
||||
}.${extension}`
|
||||
: `/download/${tagName}/codeql-bundle.${extension}`;
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { once } from "events";
|
||||
import * as fs from "fs";
|
||||
import { ClientRequest, IncomingMessage } from "http";
|
||||
import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
import test from "ava";
|
||||
import { https } from "follow-redirects";
|
||||
import nock from "nock";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
@@ -10,14 +14,14 @@ import { getRunnerLogger } from "./logging";
|
||||
import * as tar from "./tar";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { downloadAndExtract } from "./tools-download";
|
||||
import { withTmpDir } from "./util";
|
||||
import * as util from "./util";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract reports the durations when downloading before extracting",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
const archivePath = path.join(tmpDir, "codeql-bundle.tar.gz");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
sinon.stub(toolcache, "downloadTool").resolves(archivePath);
|
||||
@@ -43,13 +47,16 @@ test.serial(
|
||||
test.serial(
|
||||
"downloadAndExtract falls back to downloading before extracting if streaming fails",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
const downloadTool = sinon
|
||||
.stub(toolcache, "downloadTool")
|
||||
.resolves(archivePath);
|
||||
.callsFake(async () => {
|
||||
t.false(fs.existsSync(destination));
|
||||
return archivePath;
|
||||
});
|
||||
const extract = sinon.stub(tar, "extract").resolves(destination);
|
||||
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
|
||||
const request = nock("https://example.com")
|
||||
@@ -78,10 +85,134 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract rethrows a 404 rather than retrying the download",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
const downloadTool = sinon.stub(toolcache, "downloadTool");
|
||||
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
|
||||
const request = nock("https://example.com")
|
||||
.get("/codeql-bundle.tar.zst")
|
||||
.reply(404, "Not found");
|
||||
|
||||
const error = await t.throwsAsync(
|
||||
downloadAndExtract(
|
||||
"https://example.com/codeql-bundle.tar.zst",
|
||||
"zstd",
|
||||
destination,
|
||||
undefined,
|
||||
{},
|
||||
{ type: "gnu", version: "1.34" },
|
||||
getRunnerLogger(true),
|
||||
),
|
||||
{
|
||||
instanceOf: util.HTTPError,
|
||||
message:
|
||||
"Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst. HTTP status code: 404.",
|
||||
},
|
||||
);
|
||||
|
||||
t.is(error?.status, 404);
|
||||
t.true(request.isDone());
|
||||
t.false(extractTarZst.called);
|
||||
t.false(downloadTool.called);
|
||||
t.false(fs.existsSync(destination));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract falls back to downloading before extracting on a server error",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
const downloadTool = sinon
|
||||
.stub(toolcache, "downloadTool")
|
||||
.callsFake(async () => {
|
||||
t.false(fs.existsSync(destination));
|
||||
return archivePath;
|
||||
});
|
||||
const extract = sinon.stub(tar, "extract").resolves(destination);
|
||||
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
|
||||
const request = nock("https://example.com")
|
||||
.get("/codeql-bundle.tar.zst")
|
||||
.reply(500);
|
||||
|
||||
const statusReport = await downloadAndExtract(
|
||||
"https://example.com/codeql-bundle.tar.zst",
|
||||
"zstd",
|
||||
destination,
|
||||
undefined,
|
||||
{},
|
||||
{ type: "gnu", version: "1.34" },
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
t.assert(Number.isInteger(statusReport.downloadDurationMs));
|
||||
t.true(request.isDone());
|
||||
t.false(extractTarZst.called);
|
||||
t.true(downloadTool.calledOnce);
|
||||
t.true(extract.calledOnce);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract handles an unknown status as a non-HTTP error",
|
||||
async (t) => {
|
||||
const asHTTPError = sinon.spy(util, "asHTTPError");
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
|
||||
const destination = path.join(tmpDir, "codeql");
|
||||
const response = sinon.createStubInstance(IncomingMessage);
|
||||
response.statusCode = undefined;
|
||||
sinon
|
||||
.stub(https, "get")
|
||||
.callsArgWith(2, response)
|
||||
.returns(sinon.createStubInstance(ClientRequest));
|
||||
const warning = sinon.stub(core, "warning");
|
||||
const downloadTool = sinon
|
||||
.stub(toolcache, "downloadTool")
|
||||
.resolves(archivePath);
|
||||
const extract = sinon.stub(tar, "extract").resolves(destination);
|
||||
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
|
||||
|
||||
await downloadAndExtract(
|
||||
"https://example.com/codeql-bundle.tar.zst",
|
||||
"zstd",
|
||||
destination,
|
||||
undefined,
|
||||
{},
|
||||
{ type: "gnu", version: "1.34" },
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
t.is(
|
||||
warning.firstCall.args[0],
|
||||
"Failed to download and extract CodeQL bundle using streaming with error: Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst.",
|
||||
);
|
||||
t.true(response.resume.calledOnce);
|
||||
t.false(extractTarZst.called);
|
||||
t.true(downloadTool.calledOnce);
|
||||
t.true(extract.calledOnce);
|
||||
});
|
||||
|
||||
t.true(asHTTPError.calledOnce);
|
||||
t.true(asHTTPError.firstCall.args[0] instanceof Error);
|
||||
t.false(asHTTPError.firstCall.args[0] instanceof util.HTTPError);
|
||||
t.is(asHTTPError.firstCall.returnValue, undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadAndExtract reports only the total duration when streaming extraction",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
sinon.stub(process, "platform").value("linux");
|
||||
const downloadTool = sinon.stub(toolcache, "downloadTool");
|
||||
const extractTarZst = sinon
|
||||
|
||||
@@ -13,8 +13,16 @@ import * as semver from "semver";
|
||||
import { ActionState } from "./action-common";
|
||||
import { ActionsEnvVars, getEnv, ReadOnlyEnv } from "./environment";
|
||||
import { formatDuration, Logger } from "./logging";
|
||||
import type { PerLanguageToolsStatusReport } from "./status-report/tools-download";
|
||||
import * as tar from "./tar";
|
||||
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
|
||||
import {
|
||||
asHTTPError,
|
||||
cleanUpPath,
|
||||
durationMsSince,
|
||||
getErrorMessage,
|
||||
getRequiredEnvParam,
|
||||
HTTPError,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
* High watermark to use when streaming the download and extraction of the CodeQL tools.
|
||||
@@ -44,10 +52,11 @@ export type ToolsDownloadStatusReport = {
|
||||
*/
|
||||
extractionDurationMs?: number;
|
||||
/**
|
||||
* Total time taken to make the bundle available on disk, in milliseconds. This includes any time
|
||||
* spent on a streaming attempt that failed and fell back to downloading before extracting.
|
||||
* Total time taken to make the bundle available on disk, including failed download attempts
|
||||
* before a fallback, in milliseconds.
|
||||
*/
|
||||
totalDurationMs: number;
|
||||
perLanguage?: PerLanguageToolsStatusReport;
|
||||
};
|
||||
|
||||
export async function downloadAndExtract(
|
||||
@@ -78,7 +87,7 @@ export async function downloadAndExtract(
|
||||
logger,
|
||||
);
|
||||
|
||||
const totalDurationMs = Math.round(performance.now() - startTime);
|
||||
const totalDurationMs = durationMsSince(startTime);
|
||||
logger.info(
|
||||
`Finished downloading and extracting CodeQL bundle to ${dest} (${formatDuration(
|
||||
totalDurationMs,
|
||||
@@ -88,14 +97,20 @@ export async function downloadAndExtract(
|
||||
return { totalDurationMs };
|
||||
}
|
||||
} catch (e) {
|
||||
// If we failed during processing, we want to clean up the destination directory
|
||||
// before we either try again or give up.
|
||||
await cleanUpPath(dest, "CodeQL bundle", logger);
|
||||
|
||||
// Retrying a 404 is pointless: the asset does not exist, so downloading it a different way
|
||||
// will fail in the same way.
|
||||
if (asHTTPError(e)?.status === 404) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
core.warning(
|
||||
`Failed to download and extract CodeQL bundle using streaming with error: ${getErrorMessage(e)}`,
|
||||
);
|
||||
core.warning(`Falling back to downloading the bundle before extracting.`);
|
||||
|
||||
// If we failed during processing, we want to clean up the destination directory
|
||||
// before we try again.
|
||||
await cleanUpPath(dest, "CodeQL bundle", logger);
|
||||
}
|
||||
|
||||
const toolsDownloadStart = performance.now();
|
||||
@@ -105,7 +120,7 @@ export async function downloadAndExtract(
|
||||
authorization,
|
||||
headers,
|
||||
);
|
||||
const downloadDurationMs = Math.round(performance.now() - toolsDownloadStart);
|
||||
const downloadDurationMs = durationMsSince(toolsDownloadStart);
|
||||
|
||||
logger.info(
|
||||
`Finished downloading CodeQL bundle to ${archivedBundlePath} (${formatDuration(
|
||||
@@ -125,7 +140,7 @@ export async function downloadAndExtract(
|
||||
tarVersion,
|
||||
logger,
|
||||
);
|
||||
extractionDurationMs = Math.round(performance.now() - extractionStart);
|
||||
extractionDurationMs = durationMsSince(extractionStart);
|
||||
logger.info(
|
||||
`Finished extracting CodeQL bundle to ${dest} (${formatDuration(
|
||||
extractionDurationMs,
|
||||
@@ -138,7 +153,7 @@ export async function downloadAndExtract(
|
||||
return {
|
||||
downloadDurationMs,
|
||||
extractionDurationMs,
|
||||
totalDurationMs: Math.round(performance.now() - startTime),
|
||||
totalDurationMs: durationMsSince(startTime),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,9 +206,14 @@ async function downloadAndExtractZstdWithStreaming(
|
||||
if (response.statusCode !== 200) {
|
||||
// Discard the response body so that the connection can be released.
|
||||
response.resume();
|
||||
throw new Error(
|
||||
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
|
||||
);
|
||||
const baseMessage = `Failed to download CodeQL bundle from ${codeqlURL}.`;
|
||||
if (response.statusCode !== undefined) {
|
||||
throw new HTTPError(
|
||||
`${baseMessage} HTTP status code: ${response.statusCode}.`,
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
throw new Error(baseMessage);
|
||||
}
|
||||
|
||||
await tar.extractTarZst(response, dest, tarVersion, logger);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import path from "path";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import test from "ava";
|
||||
@@ -508,6 +509,26 @@ test("joinAtMost - truncates list if array is > than limit", (t) => {
|
||||
t.false(result.includes("test6"));
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"durationMsSince rounds elapsed milliseconds rather than the timestamps",
|
||||
(t) => {
|
||||
const startTime = 1000.25;
|
||||
const now = sinon.stub(performance, "now");
|
||||
for (const [endTime, expected] of [
|
||||
[1000.25, 0],
|
||||
[1000.74, 0],
|
||||
[1000.75, 1],
|
||||
[1001.74, 1],
|
||||
[1001.75, 2],
|
||||
[2000.74, 1000],
|
||||
[2000.75, 1001],
|
||||
]) {
|
||||
now.returns(endTime);
|
||||
t.is(util.durationMsSince(startTime), expected);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("Success creates a success result", (t) => {
|
||||
const result = new util.Success("test value");
|
||||
t.true(result.isSuccess());
|
||||
|
||||
35
src/util.ts
35
src/util.ts
@@ -2,6 +2,7 @@ import * as fs from "fs";
|
||||
import * as fsPromises from "fs/promises";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as io from "@actions/io";
|
||||
@@ -84,9 +85,32 @@ export async function withTmpDir<T>(
|
||||
body: (tmpDir: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codeql-action-"));
|
||||
const result = await body(tmpDir);
|
||||
await fs.promises.rm(tmpDir, { force: true, recursive: true });
|
||||
return result;
|
||||
try {
|
||||
return await body(tmpDir);
|
||||
} finally {
|
||||
await fs.promises.rm(tmpDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary file with the given contents, runs the given body, and
|
||||
* then deletes the file. Note that, to create a temporary file, we first create
|
||||
* a temporary directory via {@link withTmpDir} and then create the file within
|
||||
* that directory.
|
||||
* @param baseFileName The name to assign the temporary file.
|
||||
* @param contents The contents to write to the temporary file.
|
||||
* @param body The function to execute with the temporary file.
|
||||
*/
|
||||
export async function withTmpFile<T>(
|
||||
baseFileName: string,
|
||||
contents: string,
|
||||
body: (filePath: string) => Promise<T> | T,
|
||||
): Promise<T> {
|
||||
return withTmpDir(async (tmpDir) => {
|
||||
const filePath = path.join(tmpDir, baseFileName);
|
||||
fs.writeFileSync(filePath, contents);
|
||||
return body(filePath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -658,6 +682,11 @@ export async function bundleDb(
|
||||
return databaseBundlePath;
|
||||
}
|
||||
|
||||
/** Returns the elapsed milliseconds, rounded, since a `performance.now()` timestamp. */
|
||||
export function durationMsSince(startTime: number): number {
|
||||
return Math.round(performance.now() - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param milliseconds time to delay
|
||||
* @param opts options
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"@octokit/core/dist-types/types": ["./node_modules/@octokit/core/dist-types/types.d.ts"]
|
||||
},
|
||||
},
|
||||
"exclude": ["node_modules", "pr-checks", "scripts/changetool"]
|
||||
"exclude": ["node_modules", "pr-checks"]
|
||||
}
|
||||
|
||||
0
unreleased-change-notes/.gitkeep
Normal file
0
unreleased-change-notes/.gitkeep
Normal file
Reference in New Issue
Block a user