mirror of
https://github.com/github/codeql-action.git
synced 2026-10-06 10:55:17 +00:00
Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1767808e11 | ||
|
|
aeafd00d94 | ||
|
|
363d874905 | ||
|
|
fa90489493 | ||
|
|
d8251b8572 | ||
|
|
b6d38e563b | ||
|
|
b948c4d91e | ||
|
|
894842692f | ||
|
|
62b0dd96ff | ||
|
|
4d9cd8c21b | ||
|
|
412197a0f3 | ||
|
|
0cd78b35d9 | ||
|
|
c26ee0ca71 | ||
|
|
6ce88f6aaf | ||
|
|
1f8080af29 | ||
|
|
56b0d5f02c | ||
|
|
b804ae96cc | ||
|
|
27c60c532d | ||
|
|
c535168544 | ||
|
|
05d2a1a6fb | ||
|
|
f24d88130e | ||
|
|
f45aab1a23 | ||
|
|
d036b81d95 | ||
|
|
f306a93590 | ||
|
|
ffc4a0cd10 | ||
|
|
222d54a705 | ||
|
|
cbe61e2f00 | ||
|
|
f1b8b0f7b3 | ||
|
|
99522b4961 | ||
|
|
61817fadad | ||
|
|
8e838ff396 | ||
|
|
10e476ce02 | ||
|
|
fc2d2f7d2d | ||
|
|
62a2268b34 | ||
|
|
97a91d9861 | ||
|
|
85c0c095cb | ||
|
|
503e2f62a9 | ||
|
|
260cb2adcf | ||
|
|
31252c2639 | ||
|
|
1991d2001e | ||
|
|
0af3281195 | ||
|
|
8dfe937b99 | ||
|
|
8102666a8c | ||
|
|
7b6a151fb5 | ||
|
|
29b01b6799 | ||
|
|
fa2bea7c7a | ||
|
|
9fb2fa5f46 | ||
|
|
054b25e1c2 | ||
|
|
fa8392b7e5 | ||
|
|
38c1d74ffa | ||
|
|
19aa55de51 | ||
|
|
a7334dd080 | ||
|
|
aa2cb993a1 | ||
|
|
9c970806c4 | ||
|
|
b5f938c947 | ||
|
|
73dc34459c | ||
|
|
266e866100 | ||
|
|
2892aa5e19 | ||
|
|
8ad03a333e | ||
|
|
98af865db5 | ||
|
|
6aa6d5553a | ||
|
|
f42df5b5c1 | ||
|
|
a6ef2c96fc | ||
|
|
1ef28a1b76 | ||
|
|
26cb08bab0 | ||
|
|
f035ce3a98 | ||
|
|
5e4e2550b4 | ||
|
|
b13f5f47d5 | ||
|
|
c87fe5756c | ||
|
|
becb485c9f | ||
|
|
42277414c7 | ||
|
|
9c9e4b034d | ||
|
|
f0064b219d | ||
|
|
9691115b1c | ||
|
|
fa4712895b | ||
|
|
660f7c5f24 | ||
|
|
41a4b9a4cd | ||
|
|
fe0a932a6a | ||
|
|
8e36092a16 | ||
|
|
2294a7a8b1 | ||
|
|
c0369726ee | ||
|
|
a5c2338ec0 | ||
|
|
f042742baf | ||
|
|
33141e5cce | ||
|
|
81fb67799a | ||
|
|
53162242d5 | ||
|
|
3bacfe2c5b | ||
|
|
e574ef13a6 | ||
|
|
48321b2d48 | ||
|
|
f2ec2f6267 | ||
|
|
a9a8cd1aec | ||
|
|
dba87a18dc | ||
|
|
06344e2ba1 | ||
|
|
07dc94940e | ||
|
|
416ff0dea1 | ||
|
|
738bd62186 | ||
|
|
6dd161bc76 | ||
|
|
8a88af6849 | ||
|
|
ada4e83349 | ||
|
|
3a30b151d6 | ||
|
|
669351e880 | ||
|
|
b1668d6234 | ||
|
|
c0bd54fdf4 | ||
|
|
0263122209 | ||
|
|
c496c6cceb | ||
|
|
598cda36cf | ||
|
|
771560691a | ||
|
|
a92f7fb688 | ||
|
|
7f54212a01 | ||
|
|
977b29b897 | ||
|
|
bc0efd6d91 | ||
|
|
128614ad8b | ||
|
|
d63b2a40db | ||
|
|
b246e56069 | ||
|
|
bffae1c4b8 | ||
|
|
1ee3265262 | ||
|
|
bb1dc5460b | ||
|
|
47d607e2c1 | ||
|
|
cb31eabcd8 | ||
|
|
a7afe0a2d7 | ||
|
|
f8b1c08e6d | ||
|
|
ba80880079 | ||
|
|
1c5b675653 | ||
|
|
a97cdcae05 | ||
|
|
cc6c6911c3 | ||
|
|
a65b83a73d | ||
|
|
07fa87d333 | ||
|
|
f18f3536f1 | ||
|
|
ecec9b5a37 | ||
|
|
79fe3a1270 | ||
|
|
ead1f7d93f | ||
|
|
549d498da3 | ||
|
|
ed3a24ccbc | ||
|
|
28b8f598f5 | ||
|
|
a0cc01096a | ||
|
|
41c04be873 | ||
|
|
06f4ce74f9 | ||
|
|
5a2f29e0a1 | ||
|
|
cf9bbfe9a1 | ||
|
|
69f47159b1 | ||
|
|
f4fa111630 | ||
|
|
89606bbad1 | ||
|
|
bd2ddba96c | ||
|
|
ac47650173 | ||
|
|
2f552a99f3 | ||
|
|
f536ef48b7 | ||
|
|
dfb9bf52c9 | ||
|
|
2d47caf123 | ||
|
|
59ce3a25ba | ||
|
|
289376d7dd | ||
|
|
9961729e06 | ||
|
|
7dbd038544 | ||
|
|
f3e0c870be | ||
|
|
53e5014d5d | ||
|
|
f959bfdc31 | ||
|
|
eb76062ef2 | ||
|
|
ec5eecb246 | ||
|
|
1970c450ec | ||
|
|
e26a5c4139 | ||
|
|
f19a847dca | ||
|
|
d65664dcae | ||
|
|
0224ca9728 | ||
|
|
6041eff66b | ||
|
|
f45ef9d1c5 | ||
|
|
13354125bc | ||
|
|
f60f4d9500 | ||
|
|
a691c16115 | ||
|
|
b1c1bbb1e0 | ||
|
|
1cf8f518e1 | ||
|
|
faf4f3af21 | ||
|
|
e72db597b8 | ||
|
|
15b91dbb92 | ||
|
|
27a821e701 | ||
|
|
cc78526536 | ||
|
|
032138d5fb | ||
|
|
7be8083b7e | ||
|
|
80d5f4e36a | ||
|
|
297617deb9 | ||
|
|
f6491d646e | ||
|
|
4375107b7e | ||
|
|
a50b659501 | ||
|
|
5bdaf5d4af | ||
|
|
14c568a926 | ||
|
|
0484c3ad1d | ||
|
|
aaa014366e | ||
|
|
5bd5bf93f2 | ||
|
|
5584cec589 | ||
|
|
d9826583a3 | ||
|
|
9f28912d94 | ||
|
|
d9ef050686 | ||
|
|
a72798355c | ||
|
|
d40707c73d | ||
|
|
e855781840 | ||
|
|
23a5edca55 | ||
|
|
6f5a84b5b5 | ||
|
|
d23593114c | ||
|
|
b1363526cf | ||
|
|
83d1431d4e |
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`
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -8,8 +8,6 @@ updates:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
exclude:
|
||||
- "@actions/*"
|
||||
labels:
|
||||
- Rebuild
|
||||
# Ignore incompatible dependency updates
|
||||
@@ -33,8 +31,6 @@ updates:
|
||||
interval: weekly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
exclude:
|
||||
- "actions/*"
|
||||
labels:
|
||||
- Rebuild
|
||||
groups:
|
||||
|
||||
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:
|
||||
|
||||
2
.github/workflows/__config-input.yml
generated
vendored
2
.github/workflows/__config-input.yml
generated
vendored
@@ -51,6 +51,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
|
||||
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
|
||||
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
|
||||
2
.github/workflows/__packaging-config-js.yml
generated
vendored
2
.github/workflows/__packaging-config-js.yml
generated
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
|
||||
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
@@ -84,6 +84,8 @@ jobs:
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
- name: Install newer npm
|
||||
run: npm install -g npm@11.19.1
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Prepare test
|
||||
|
||||
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
|
||||
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -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
|
||||
|
||||
53
.github/workflows/pr-checks.yml
vendored
53
.github/workflows/pr-checks.yml
vendored
@@ -45,7 +45,13 @@ jobs:
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
# Install a new enough version of `npm` to understand `min-release-age`
|
||||
# that is still compatible with Node 20.
|
||||
- name: Install newer npm
|
||||
if: matrix.node-version == 20
|
||||
run: npm install -g npm@11.19.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -77,33 +83,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:
|
||||
@@ -122,11 +101,19 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Check for incorrect addresses in package-lock.json
|
||||
run: |
|
||||
if git grep -nE '(pkgs\.visualstudio\.com|pkgs\.dev\.azure\.com|packagefeedproxy\.microsoft\.io)' -- \
|
||||
'package-lock.json'; then
|
||||
echo "::error::package-lock.json contains internal package feed URLs. Replace them with public registry URLs."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
id: install-deps
|
||||
@@ -141,6 +128,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 +181,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 }}
|
||||
|
||||
29
.github/workflows/rebuild.yml
vendored
29
.github/workflows/rebuild.yml
vendored
@@ -54,32 +54,27 @@ jobs:
|
||||
run: |
|
||||
git fetch origin "$BASE_BRANCH"
|
||||
|
||||
# Allow merge conflicts in `lib`, since rebuilding should resolve them.
|
||||
git merge "origin/$BASE_BRANCH"
|
||||
MERGE_RESULT=$?
|
||||
|
||||
if [ "$MERGE_RESULT" -eq 0 ]; then
|
||||
# Allow merge conflicts in `lib`, since rebuilding should resolve them. Conflicts leave the
|
||||
# merge in progress, so check for `MERGE_HEAD` to tell them apart from failures that don't.
|
||||
if git merge "origin/$BASE_BRANCH"; then
|
||||
echo "Merge succeeded cleanly."
|
||||
elif [ "$MERGE_RESULT" -eq 1 ]; then
|
||||
echo "Merge conflicts detected (exit code $MERGE_RESULT), continuing."
|
||||
else
|
||||
echo "git merge failed with unexpected exit code $MERGE_RESULT."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MERGE_RESULT" -ne 0 ]; then
|
||||
elif git rev-parse --verify MERGE_HEAD >/dev/null 2>&1; then
|
||||
echo "Merge conflicts detected, continuing."
|
||||
echo "merge-in-progress=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check for merge conflicts outside of `lib`. Disable git diff's trailing whitespace check
|
||||
# since `node_modules/@types/semver/README.md` fails it.
|
||||
if git -c core.whitespace=-trailing-space diff --check | grep --invert-match '^lib/'; then
|
||||
# Check for merge conflicts outside of `lib`.
|
||||
CONFLICTS_OUTSIDE_LIB=$(git diff --name-only --diff-filter=U | grep --invert-match '^lib/' || true)
|
||||
if [ -n "$CONFLICTS_OUTSIDE_LIB" ]; then
|
||||
echo "Merge conflicts were detected outside of the lib directory. Please resolve them manually."
|
||||
git -c core.whitespace=-trailing-space diff --check | grep --invert-match '^lib/' || true
|
||||
echo "$CONFLICTS_OUTSIDE_LIB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No merge conflicts found outside the lib directory. We should be able to resolve all of" \
|
||||
"these by rebuilding the Action."
|
||||
else
|
||||
echo "git merge failed for a reason other than merge conflicts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compile TypeScript
|
||||
|
||||
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -7,6 +7,10 @@
|
||||
// transpiled JavaScript
|
||||
"build": true,
|
||||
"lib": true,
|
||||
|
||||
// exclude "tests" by default because it causes VSCode to start language-specific extensions
|
||||
// that are not typically needed during development (or indeed may not work correctly)
|
||||
"tests": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
@@ -18,7 +22,7 @@
|
||||
"git.ignoreLimitWarning": true,
|
||||
// Use the vendored TypeScript version to have a consistent development experience across
|
||||
// machines.
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,18 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.38.2 - 24 Sept 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)
|
||||
|
||||
## 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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
46731
lib/entry-points.js
generated
46731
lib/entry-points.js
generated
File diff suppressed because one or more lines are too long
1049
package-lock.json
generated
1049
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.38.0",
|
||||
"version": "4.38.3",
|
||||
"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,19 +39,19 @@
|
||||
"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.2",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.2",
|
||||
"undici": "^6.28.0"
|
||||
"undici": "^6.28.0",
|
||||
"uuid": "^14.0.2"
|
||||
},
|
||||
"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.5.4",
|
||||
"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.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -112,7 +112,7 @@ ${NO_CHANGES_STR}`;
|
||||
describe("updateChangelog", async () => {
|
||||
await it("removes `NO_CHANGES_STR` if present in [UNRELEASED] section", async () => {
|
||||
const result = updateChangelog(EMPTY_CHANGELOG, "");
|
||||
assert.ok(!result.includes(NO_CHANGES_STR.trim()));
|
||||
assert.ok(!result.includes(NO_CHANGES_STR));
|
||||
});
|
||||
|
||||
await it("doesn't remove `NO_CHANGES_STR` if present in versioned section", async () => {
|
||||
@@ -120,7 +120,7 @@ describe("updateChangelog", async () => {
|
||||
EMPTY_CHANGELOG.replace(UNRELEASED_PLACEHOLDER, "1.2.3"),
|
||||
"",
|
||||
);
|
||||
assert.ok(result.includes(NO_CHANGES_STR.trim()));
|
||||
assert.ok(result.includes(NO_CHANGES_STR));
|
||||
});
|
||||
|
||||
await it("throws if there are no sections", async () => {
|
||||
|
||||
@@ -9,17 +9,46 @@ import * as fs from "node:fs";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
addBodyLinesToUnreleasedSection,
|
||||
ChangelogSection,
|
||||
EMPTY_CHANGELOG,
|
||||
getHeader,
|
||||
getReleaseDateString,
|
||||
NO_CHANGES_STR,
|
||||
parseChangelog,
|
||||
processChangelogForBackports,
|
||||
renderChangelog,
|
||||
setVersionAndDate,
|
||||
UNRELEASED_PLACEHOLDER,
|
||||
} from "./changelog";
|
||||
import { CHANGELOG_FILE } from "./config";
|
||||
|
||||
const testDate = new Date(2026, 7, 14);
|
||||
|
||||
describe("getHeader", async () => {
|
||||
function Section(headerLine: string): ChangelogSection {
|
||||
return {
|
||||
headerLine,
|
||||
bodyLines: [],
|
||||
};
|
||||
}
|
||||
await it("returns non-headers unchanged", () => {
|
||||
assert.equal("foo", getHeader(Section("foo")));
|
||||
assert.equal("- bar", getHeader(Section("- bar")));
|
||||
});
|
||||
await it("strips octothorpes", async () => {
|
||||
assert.equal("foo", getHeader(Section("# foo")));
|
||||
assert.equal("foo", getHeader(Section("## foo")));
|
||||
assert.equal("foo", getHeader(Section("### foo")));
|
||||
assert.equal("foo", getHeader(Section("#### foo")));
|
||||
assert.equal("foo", getHeader(Section("##### foo")));
|
||||
assert.equal("foo", getHeader(Section("###### foo")));
|
||||
});
|
||||
await it("strips whitespace", async () => {
|
||||
assert.equal("foo", getHeader(Section("# foo ")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getReleaseDateString", async () => {
|
||||
await it("formats dates as expected", async () => {
|
||||
assert.equal(getReleaseDateString(testDate), "14 Aug 2026");
|
||||
@@ -70,3 +99,73 @@ describe("processChangelogForBackports", async () => {
|
||||
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("addBodyLinesToUnreleasedSection", async () => {
|
||||
function newChangelogWithSections(sections: ChangelogSection[]) {
|
||||
return {
|
||||
preamble: [],
|
||||
sections,
|
||||
};
|
||||
}
|
||||
|
||||
await it("throws error if '[UNRELEASED]' section is not first", async () => {
|
||||
const invalidChangelog = newChangelogWithSections([
|
||||
{
|
||||
headerLine: "## Release 1.0.0",
|
||||
bodyLines: [],
|
||||
},
|
||||
{
|
||||
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
|
||||
bodyLines: [],
|
||||
},
|
||||
]);
|
||||
assert.throws(() =>
|
||||
addBodyLinesToUnreleasedSection(invalidChangelog, ["foo"]),
|
||||
);
|
||||
});
|
||||
|
||||
await it("overwrites 'No user facing changes.'", async () => {
|
||||
const changelog = newChangelogWithSections([
|
||||
{
|
||||
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
|
||||
bodyLines: ["", NO_CHANGES_STR, ""],
|
||||
},
|
||||
]);
|
||||
|
||||
addBodyLinesToUnreleasedSection(changelog, ["- foo"]);
|
||||
|
||||
assert.equal(changelog.sections[0].bodyLines.length, 3);
|
||||
assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]);
|
||||
});
|
||||
|
||||
await it("does nothing if lines is empty", async () => {
|
||||
const changelog = newChangelogWithSections([
|
||||
{
|
||||
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
|
||||
bodyLines: ["", NO_CHANGES_STR, ""],
|
||||
},
|
||||
]);
|
||||
const changelogClone = structuredClone(changelog);
|
||||
|
||||
addBodyLinesToUnreleasedSection(changelog, []);
|
||||
|
||||
assert.deepEqual(changelog, changelogClone);
|
||||
});
|
||||
|
||||
await it("inserts a line", async () => {
|
||||
const changelog = newChangelogWithSections([
|
||||
{
|
||||
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
|
||||
bodyLines: ["", "- Added a new dependency.", ""],
|
||||
},
|
||||
]);
|
||||
const lineToInsert = "- foo";
|
||||
|
||||
addBodyLinesToUnreleasedSection(changelog, [lineToInsert]);
|
||||
|
||||
assert.equal(changelog.sections[0].bodyLines.length, 4);
|
||||
assert.ok(
|
||||
changelog.sections[0].bodyLines.some((line) => line === lineToInsert),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,14 +6,16 @@ import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
||||
|
||||
/** The default contents for a section in the changelog. */
|
||||
export const NO_CHANGES_STR = "No user facing changes.\n\n";
|
||||
export const NO_CHANGES_STR = "No user facing changes.";
|
||||
|
||||
/** Placeholder changelog content for a new release. */
|
||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||
|
||||
## ${UNRELEASED_PLACEHOLDER}
|
||||
|
||||
${NO_CHANGES_STR}`;
|
||||
${NO_CHANGES_STR}
|
||||
|
||||
`;
|
||||
|
||||
/**
|
||||
* Represents sections in a changelog.
|
||||
@@ -31,6 +33,13 @@ export interface Changelog {
|
||||
sections: ChangelogSection[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text of the header (without the '## ' prefix) of the given section.
|
||||
* */
|
||||
export function getHeader(section: ChangelogSection): string {
|
||||
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
|
||||
}
|
||||
|
||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||
export function getReleaseDateString(today: Date = new Date()): string {
|
||||
return today.toLocaleDateString("en-GB", {
|
||||
@@ -125,6 +134,42 @@ export function parseChangelog(content: string): Changelog {
|
||||
return { preamble, sections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the changenotes `lines` in the `[UNRELEASED]` section of `changelog`.
|
||||
* If the section contains the stock message {@link NO_CHANGES_STR}, then
|
||||
* `lines` will be inserted in place and the stock message will be deleted.
|
||||
*
|
||||
* @throws Error -- if the [UNRELEASED] section does not exist.
|
||||
*
|
||||
* @param changelog The CHANGELOG object to modify.
|
||||
* @param lines The changenotes to insert.
|
||||
*/
|
||||
export function addBodyLinesToUnreleasedSection(
|
||||
changelog: Changelog,
|
||||
lines: string[],
|
||||
) {
|
||||
// Do nothing if there is nothing to insert.
|
||||
if (lines.length === 0) return;
|
||||
|
||||
const unreleasedSection = changelog.sections[0];
|
||||
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
|
||||
throw Error(
|
||||
`'${UNRELEASED_PLACEHOLDER}' is not the first section of 'CHANGELOG.md'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
|
||||
unreleasedSection.bodyLines = ["", ...lines, ""];
|
||||
return;
|
||||
}
|
||||
|
||||
// The last body line should be a blank line (for spacing).
|
||||
// Remove it so that we can add `lines` and then add the blank line back.
|
||||
unreleasedSection.bodyLines.pop();
|
||||
unreleasedSection.bodyLines.push(...lines);
|
||||
unreleasedSection.bodyLines.push("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an array of lines into a single string by adding line breaks.
|
||||
*/
|
||||
@@ -204,7 +249,7 @@ export function processChangelogForBackports(
|
||||
|
||||
// Add an entry if we didn't keep any.
|
||||
if (!foundContent) {
|
||||
section.bodyLines.push(NO_CHANGES_STR.trim());
|
||||
section.bodyLines.push(NO_CHANGES_STR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
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 { withTmpFile } from "../../src/util";
|
||||
|
||||
import {
|
||||
hasValidChangenoteCategory,
|
||||
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 +24,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 +39,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 +50,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 +67,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 +82,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 +94,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 +108,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 +132,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 +144,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 +168,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,7 +178,7 @@ 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);
|
||||
},
|
||||
);
|
||||
134
pr-checks/changenotes.mts
Executable file
134
pr-checks/changenotes.mts
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import path from "path";
|
||||
|
||||
import { ExitCode } from "@actions/core";
|
||||
import { matter } from "lite-matter";
|
||||
|
||||
import {
|
||||
addBodyLinesToUnreleasedSection,
|
||||
parseChangelog,
|
||||
renderChangelog,
|
||||
withChangelog,
|
||||
} from "./changelog";
|
||||
import { isValidChangenoteFile } from "./changelog/validate.mjs";
|
||||
import { CHANGENOTES_DIR } from "./config";
|
||||
|
||||
/**
|
||||
* Describes a changenote file, including its file path, frontmatter, and content.
|
||||
*/
|
||||
interface ChangenoteFile {
|
||||
absolutePath: string;
|
||||
data: Record<string, any>;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute file paths of all files in
|
||||
* {@link CHANGENOTES_DIR} (except ".gitkeep").
|
||||
* */
|
||||
function listUnreleasedChangenoteDir(): string[] {
|
||||
return fs
|
||||
.readdirSync(CHANGENOTES_DIR)
|
||||
.filter((name) => name !== ".gitkeep")
|
||||
.map((name) => path.join(CHANGENOTES_DIR, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the {@link CHANGENOTES_DIR} directory for changenote files
|
||||
* and returns a parsed listing of those changenote files.
|
||||
*/
|
||||
function getChangenotes(): ChangenoteFile[] {
|
||||
return listUnreleasedChangenoteDir().map((absolutePath) => {
|
||||
return {
|
||||
absolutePath,
|
||||
...matter(fs.readFileSync(absolutePath, "utf-8")),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const entryPoint = process.argv[1];
|
||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||
try {
|
||||
process.exit(main());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(ExitCode.Failure);
|
||||
}
|
||||
}
|
||||
|
||||
function main(): ExitCode {
|
||||
const { positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
const [command] = positionals;
|
||||
switch (command) {
|
||||
case undefined:
|
||||
case "help":
|
||||
return usage();
|
||||
case "assemble":
|
||||
return assemble();
|
||||
case "validate":
|
||||
return validate();
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
return ExitCode.Failure;
|
||||
}
|
||||
}
|
||||
|
||||
function usage(): ExitCode {
|
||||
const message =
|
||||
"Usage: changenotes.mts assemble\n" +
|
||||
" changenotes.mts validate\n" +
|
||||
" changenotes.mts help";
|
||||
console.log(message);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
function assemble(): ExitCode {
|
||||
try {
|
||||
const changenotes = getChangenotes();
|
||||
const changenoteBodies = changenotes.map((c) => c.content);
|
||||
const changenotePaths = changenotes.map((c) => c.absolutePath);
|
||||
|
||||
withChangelog((contents) => {
|
||||
const changelog = parseChangelog(contents);
|
||||
addBodyLinesToUnreleasedSection(changelog, changenoteBodies);
|
||||
return renderChangelog(changelog);
|
||||
}, {});
|
||||
|
||||
// Delete changenotes only after successful processing.
|
||||
for (const p of changenotePaths) {
|
||||
fs.unlinkSync(p);
|
||||
}
|
||||
|
||||
return ExitCode.Success;
|
||||
} catch (e) {
|
||||
console.error("Failed to assemble changenotes to 'CHANGELOG.md'", e);
|
||||
}
|
||||
|
||||
return ExitCode.Failure;
|
||||
}
|
||||
|
||||
function validate(): ExitCode {
|
||||
try {
|
||||
const allChangenotesValid = getChangenotes().reduce(
|
||||
(r, changenote) => r && isValidChangenoteFile(changenote.absolutePath),
|
||||
true,
|
||||
);
|
||||
if (allChangenotesValid) {
|
||||
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
return ExitCode.Failure;
|
||||
}
|
||||
@@ -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}."
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -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"
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.23.12"
|
||||
"tsx": "^4.23.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -219,6 +221,12 @@ const languageSetups: LanguageSetups = {
|
||||
cache: "npm",
|
||||
},
|
||||
},
|
||||
// Install a new enough version of `npm` to understand `min-release-age`
|
||||
// that is still compatible with Node 20.
|
||||
{
|
||||
name: "Install newer npm",
|
||||
run: "npm install -g npm@11.19.1",
|
||||
},
|
||||
{
|
||||
name: "Install dependencies",
|
||||
run: "npm ci",
|
||||
@@ -253,8 +261,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 +520,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 +572,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,51 +0,0 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { isValidChangenoteFile } from "./cli/validate.ts";
|
||||
|
||||
const entryPoint = process.argv[1];
|
||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||
try {
|
||||
process.exit(main());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const { positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
const [command, ...paths] = positionals;
|
||||
switch (command) {
|
||||
case undefined:
|
||||
case "help":
|
||||
return usage();
|
||||
case "validate":
|
||||
return validate(paths);
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function usage(): number {
|
||||
console.log("Usage: changetool validate <path> [<path> ...]");
|
||||
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;
|
||||
}
|
||||
}
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -19,6 +19,10 @@ export interface BaseState {
|
||||
name: ActionName;
|
||||
/** When the Action was started. */
|
||||
startedAt: Date;
|
||||
/** The platform the Action is running on. */
|
||||
platform: NodeJS.Platform;
|
||||
/** The architecture of the host. */
|
||||
arch: NodeJS.Architecture;
|
||||
}
|
||||
|
||||
/** Describes different state features that an Action may have. */
|
||||
@@ -98,6 +102,8 @@ export async function runInActions(action: Action) {
|
||||
const actionState = {
|
||||
name: action.name,
|
||||
startedAt,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
logger,
|
||||
env,
|
||||
actions: actionsEnv,
|
||||
|
||||
@@ -7,13 +7,14 @@ 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,
|
||||
getCodeQLDatabasePath,
|
||||
ConfigurationError,
|
||||
getEnv,
|
||||
getErrorMessage,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -292,7 +293,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";
|
||||
}
|
||||
|
||||
@@ -412,14 +413,23 @@ export const persistInputs = function (env: Env = getEnv()) {
|
||||
/**
|
||||
* Restores all inputs to the action from the persisted state.
|
||||
*/
|
||||
export const restoreInputs = function () {
|
||||
const persistedInputs = core.getState(persistedInputsKey);
|
||||
if (persistedInputs) {
|
||||
for (const [name, value] of JSON.parse(persistedInputs)) {
|
||||
process.env[name] = value;
|
||||
export function restoreInputs(logger: Logger) {
|
||||
try {
|
||||
const persistedInputsValue = core.getState(persistedInputsKey);
|
||||
if (persistedInputsValue) {
|
||||
const persistedInputs = JSON.parse(persistedInputsValue);
|
||||
|
||||
for (const [name, value] of persistedInputs) {
|
||||
process.env[name] = value;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Unable to restore inputs: ${getErrorMessage(err)}`);
|
||||
throw new Error(
|
||||
"Failed to restore inputs from the state set by this action's main execution.",
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface PullRequestBranches {
|
||||
base: string;
|
||||
|
||||
@@ -25,8 +25,8 @@ export async function runWrapper() {
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
try {
|
||||
actionsUtil.restoreInputs();
|
||||
const logger = getActionsLogger();
|
||||
actionsUtil.restoreInputs(logger);
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
checkGitHubVersionInRange(gitHubVersion, logger);
|
||||
|
||||
|
||||
@@ -212,7 +212,11 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
|
||||
await runAutobuild(config, BuiltInLanguage.go, logger);
|
||||
}
|
||||
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
async function run({
|
||||
startedAt,
|
||||
logger,
|
||||
actions,
|
||||
}: ActionState<["Base", "Logger", "Actions"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
@@ -307,8 +311,13 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
logger,
|
||||
);
|
||||
|
||||
const checkoutPath = actions.getRequiredInput("checkout_path");
|
||||
|
||||
// Setup diff informed analysis if needed (based on whether init created the file)
|
||||
const diffRangePackDir = await setupDiffInformedQueryRun(logger);
|
||||
const diffRangePackDir = await setupDiffInformedQueryRun(
|
||||
logger,
|
||||
checkoutPath,
|
||||
);
|
||||
|
||||
await warnIfGoInstalledAfterInit(config, logger);
|
||||
await runAutobuildIfLegacyGoWorkflow(config, logger);
|
||||
@@ -354,7 +363,6 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
actionsUtil.getOptionalInput("upload"),
|
||||
);
|
||||
if (runStats) {
|
||||
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
|
||||
const category = actionsUtil.getOptionalInput("category");
|
||||
|
||||
uploadResults = await postProcessAndUploadSarif(
|
||||
@@ -388,18 +396,23 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// Possibly upload the overlay-base database to actions cache.
|
||||
// Note: Take care with the ordering of this call since databases may be cleaned up
|
||||
// at the `overlay` level.
|
||||
await cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger);
|
||||
await cleanupAndUploadOverlayBaseDatabaseToCache(
|
||||
codeql,
|
||||
config,
|
||||
logger,
|
||||
checkoutPath,
|
||||
);
|
||||
|
||||
// Possibly upload the database bundles for remote queries.
|
||||
// Note: Take care with the ordering of this call since databases may be cleaned up
|
||||
// at the `overlay` or `clear` level.
|
||||
databaseUploadResults = await cleanupAndUploadDatabases(
|
||||
{ logger, features },
|
||||
repositoryNwo,
|
||||
codeql,
|
||||
config,
|
||||
apiDetails,
|
||||
features,
|
||||
logger,
|
||||
checkoutPath,
|
||||
);
|
||||
|
||||
// Possibly upload the TRAP caches for later re-use
|
||||
|
||||
@@ -5,7 +5,7 @@ import { performance } from "perf_hooks";
|
||||
import * as io from "@actions/io";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { getTemporaryDirectory, getRequiredInput } from "./actions-util";
|
||||
import { getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { setupCppAutobuild } from "./autobuild";
|
||||
import { type CodeQL } from "./codeql";
|
||||
@@ -233,6 +233,7 @@ async function finalizeDatabaseCreation(
|
||||
*/
|
||||
export async function setupDiffInformedQueryRun(
|
||||
logger: Logger,
|
||||
checkoutPath: string,
|
||||
): Promise<string | undefined> {
|
||||
return await withGroupAsync(
|
||||
"Generating diff range extension pack",
|
||||
@@ -245,7 +246,6 @@ export async function setupDiffInformedQueryRun(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const checkoutPath = getRequiredInput("checkout_path");
|
||||
const packDir = writeDiffRangeDataExtensionPack(
|
||||
logger,
|
||||
diffRanges,
|
||||
|
||||
@@ -111,103 +111,115 @@ test.serial("getGitHubVersion for GHEC-DR", async (t) => {
|
||||
t.deepEqual({ type: util.GitHubVariant.GHEC_DR }, gheDotcom);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"wrapApiConfigurationError correctly wraps specific configuration errors",
|
||||
(t) => {
|
||||
test("wrapApiConfigurationError doesn't wrap errors it isn't supposed to", (t) => {
|
||||
const unwrappedErrors = [
|
||||
// We don't reclassify arbitrary errors
|
||||
const arbitraryError = new Error("arbitrary error");
|
||||
let res = api.wrapApiConfigurationError(arbitraryError);
|
||||
t.is(res, arbitraryError);
|
||||
new Error("arbitrary error"),
|
||||
// Same goes for arbitrary strings
|
||||
"arbitrary error",
|
||||
// If an HTTP error doesn't contain a specific error message, we don't wrap it.
|
||||
new util.HTTPError("arbitrary HTTP error", 456),
|
||||
];
|
||||
|
||||
// Same goes for arbitrary errors
|
||||
const configError = new util.ConfigurationError("arbitrary error");
|
||||
res = api.wrapApiConfigurationError(configError);
|
||||
t.is(res, configError);
|
||||
|
||||
// If an HTTP error doesn't contain a specific error message, we don't
|
||||
// wrap is an an API error.
|
||||
const httpError = new util.HTTPError("arbitrary HTTP error", 456);
|
||||
res = api.wrapApiConfigurationError(httpError);
|
||||
t.is(res, httpError);
|
||||
|
||||
// For other HTTP errors, we wrap them as Configuration errors if they contain
|
||||
// specific error messages.
|
||||
const httpNotFoundError = new util.HTTPError("commit not found", 404);
|
||||
res = api.wrapApiConfigurationError(httpNotFoundError);
|
||||
t.deepEqual(res, new util.ConfigurationError("commit not found"));
|
||||
|
||||
const refNotFoundError = new util.HTTPError(
|
||||
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
|
||||
404,
|
||||
);
|
||||
res = api.wrapApiConfigurationError(refNotFoundError);
|
||||
t.deepEqual(
|
||||
for (const unwrappedError of unwrappedErrors) {
|
||||
const res = api.wrapApiConfigurationError(unwrappedError);
|
||||
t.is(
|
||||
res,
|
||||
new util.ConfigurationError(
|
||||
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
|
||||
),
|
||||
unwrappedError,
|
||||
`${util.getErrorMessage(unwrappedError)} should not be wrapped by wrapApiConfigurationError`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const apiRateLimitError = new util.HTTPError(
|
||||
"API rate limit exceeded for installation",
|
||||
403,
|
||||
);
|
||||
res = api.wrapApiConfigurationError(apiRateLimitError);
|
||||
t.deepEqual(
|
||||
res,
|
||||
new util.ConfigurationError("API rate limit exceeded for installation"),
|
||||
);
|
||||
test("wrapApiConfigurationError correctly wraps specific configuration errors", (t) => {
|
||||
// For other HTTP errors, we wrap them as Configuration errors if they contain
|
||||
// specific error messages.
|
||||
const httpNotFoundError = new util.HTTPError("commit not found", 404);
|
||||
const refNotFoundError = new util.HTTPError(
|
||||
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
|
||||
404,
|
||||
);
|
||||
const apiRateLimitError = new util.HTTPError(
|
||||
"API rate limit exceeded for installation",
|
||||
403,
|
||||
);
|
||||
const resourceNotAccessibleError = new util.HTTPError(
|
||||
"Resource not accessible by integration",
|
||||
403,
|
||||
);
|
||||
const errorsToWrap = [
|
||||
httpNotFoundError,
|
||||
refNotFoundError,
|
||||
apiRateLimitError,
|
||||
resourceNotAccessibleError,
|
||||
];
|
||||
|
||||
const tokenSuggestionMessage =
|
||||
"Please check that your token is valid and has the required permissions: contents: read, security-events: write";
|
||||
const badCredentialsError = new util.HTTPError("Bad credentials", 401);
|
||||
res = api.wrapApiConfigurationError(badCredentialsError);
|
||||
for (const errorToWrap of errorsToWrap) {
|
||||
const res = api.wrapApiConfigurationError(errorToWrap);
|
||||
t.deepEqual(res, new util.ConfigurationError(errorToWrap.message));
|
||||
}
|
||||
});
|
||||
|
||||
test("wrapApiConfigurationError wraps token errors", async (t) => {
|
||||
const tokenSuggestionMessage =
|
||||
"Please check that your token is valid and has the required permissions: contents: read, security-events: write";
|
||||
const badCredentialsError = new util.HTTPError("Bad credentials", 401);
|
||||
const notFoundError = new util.HTTPError("Not Found", 404);
|
||||
const errorsToWrap = [badCredentialsError, notFoundError];
|
||||
|
||||
for (const errorToWrap of errorsToWrap) {
|
||||
const res = api.wrapApiConfigurationError(errorToWrap);
|
||||
t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage));
|
||||
}
|
||||
});
|
||||
|
||||
const notFoundError = new util.HTTPError("Not Found", 404);
|
||||
res = api.wrapApiConfigurationError(notFoundError);
|
||||
t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage));
|
||||
test("wrapApiConfigurationError wraps enablement errors", async (t) => {
|
||||
// Enablement errors.
|
||||
const enablementErrorMessages = [
|
||||
"Code Security must be enabled for this repository to use code scanning",
|
||||
"Advanced Security must be enabled for this repository to use code scanning",
|
||||
"Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.",
|
||||
"Code quality is not enabled for this repository. Please enable code quality in the repository settings.",
|
||||
];
|
||||
const transforms = [
|
||||
(msg: string) => msg,
|
||||
(msg: string) => msg.toLowerCase(),
|
||||
(msg: string) => msg.toLocaleUpperCase(),
|
||||
];
|
||||
|
||||
const resourceNotAccessibleError = new util.HTTPError(
|
||||
"Resource not accessible by integration",
|
||||
403,
|
||||
);
|
||||
res = api.wrapApiConfigurationError(resourceNotAccessibleError);
|
||||
t.deepEqual(
|
||||
res,
|
||||
new util.ConfigurationError("Resource not accessible by integration"),
|
||||
);
|
||||
|
||||
// Enablement errors.
|
||||
const enablementErrorMessages = [
|
||||
"Code Security must be enabled for this repository to use code scanning",
|
||||
"Advanced Security must be enabled for this repository to use code scanning",
|
||||
"Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.",
|
||||
"Code quality is not enabled for this repository. Please enable code quality in the repository settings.",
|
||||
];
|
||||
const transforms = [
|
||||
(msg: string) => msg,
|
||||
(msg: string) => msg.toLowerCase(),
|
||||
(msg: string) => msg.toLocaleUpperCase(),
|
||||
];
|
||||
|
||||
for (const enablementErrorMessage of enablementErrorMessages) {
|
||||
for (const transform of transforms) {
|
||||
const enablementError = new util.HTTPError(
|
||||
transform(enablementErrorMessage),
|
||||
403,
|
||||
);
|
||||
res = api.wrapApiConfigurationError(enablementError);
|
||||
t.deepEqual(
|
||||
res,
|
||||
new util.ConfigurationError(
|
||||
api.getFeatureEnablementError(enablementError.message),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const enablementErrorMessage of enablementErrorMessages) {
|
||||
for (const transform of transforms) {
|
||||
const enablementError = new util.HTTPError(
|
||||
transform(enablementErrorMessage),
|
||||
403,
|
||||
);
|
||||
const res = api.wrapApiConfigurationError(enablementError);
|
||||
t.deepEqual(
|
||||
res,
|
||||
new util.ConfigurationError(
|
||||
api.getFeatureEnablementError(enablementError.message),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("wrapApiConfigurationError doesn't double-wrap errors", async (t) => {
|
||||
// This test checks that errors don't get wrapped a second time if `wrapApiConfigurationError`
|
||||
// is called on an error that was already wrapped by a previous call to `wrapApiConfigurationError`.
|
||||
// Start by calling `wrapApiConfigurationError` on an unwrapped error that should be wrapped:
|
||||
const unwrappedError = new util.HTTPError("commit not found", 404);
|
||||
const wrappedError = api.wrapApiConfigurationError(unwrappedError);
|
||||
|
||||
// Sanity-check that it was wrapped, as expected.
|
||||
t.deepEqual(
|
||||
wrappedError,
|
||||
new util.ConfigurationError(unwrappedError.message),
|
||||
);
|
||||
|
||||
// The result of the second call should be exactly `wrappedError`:
|
||||
t.is(api.wrapApiConfigurationError(wrappedError), wrappedError);
|
||||
});
|
||||
|
||||
test("getRegistryProxy - returns undefined if the proxy is not configured", async (t) => {
|
||||
const target = callee(api.getRegistryProxy).withArgs();
|
||||
|
||||
@@ -219,25 +219,31 @@ export async function getGitHubVersionFromApi(
|
||||
return { type: GitHubVariant.DOTCOM };
|
||||
}
|
||||
|
||||
// Doesn't strictly have to be the meta endpoint as we're only
|
||||
// using the response headers which are available on every request.
|
||||
//
|
||||
// See https://docs.github.com/en/rest/meta/meta#get-github-meta-information.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const response = await apiClient.rest.meta.get();
|
||||
try {
|
||||
// Doesn't strictly have to be the meta endpoint as we're only
|
||||
// using the response headers which are available on every request.
|
||||
//
|
||||
// See https://docs.github.com/en/rest/meta/meta#get-github-meta-information.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const response = await apiClient.rest.meta.get();
|
||||
|
||||
// This happens on dotcom, although we expect to have already returned in that
|
||||
// case. This can also serve as a fallback in cases we haven't foreseen.
|
||||
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) {
|
||||
return { type: GitHubVariant.DOTCOM };
|
||||
// This happens on dotcom, although we expect to have already returned in that
|
||||
// case. This can also serve as a fallback in cases we haven't foreseen.
|
||||
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) {
|
||||
return { type: GitHubVariant.DOTCOM };
|
||||
}
|
||||
|
||||
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") {
|
||||
return { type: GitHubVariant.GHEC_DR };
|
||||
}
|
||||
|
||||
const version = response.headers[
|
||||
GITHUB_ENTERPRISE_VERSION_HEADER
|
||||
] as string;
|
||||
return { type: GitHubVariant.GHES, version };
|
||||
} catch (err) {
|
||||
throw wrapApiConfigurationError(err);
|
||||
}
|
||||
|
||||
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") {
|
||||
return { type: GitHubVariant.GHEC_DR };
|
||||
}
|
||||
|
||||
const version = response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] as string;
|
||||
return { type: GitHubVariant.GHES, version };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,9 +255,10 @@ export async function getGitHubVersionFromApi(
|
||||
*/
|
||||
export async function getGitHubVersion(): Promise<GitHubVersion> {
|
||||
if (cachedGitHubVersion === undefined) {
|
||||
const apiDetails = getApiDetails();
|
||||
cachedGitHubVersion = await getGitHubVersionFromApi(
|
||||
getApiClient(),
|
||||
getApiDetails(),
|
||||
createApiClientWithDetails(apiDetails),
|
||||
apiDetails,
|
||||
);
|
||||
}
|
||||
return cachedGitHubVersion;
|
||||
@@ -414,7 +421,14 @@ export function getFeatureEnablementError(message: string): string {
|
||||
return `Please verify that the necessary features are enabled: ${message}`;
|
||||
}
|
||||
|
||||
export function wrapApiConfigurationError(e: unknown) {
|
||||
/**
|
||||
* Decides whether `e` is a known error returned by the GitHub API that we should
|
||||
* classify as a `ConfigurationError`.
|
||||
*
|
||||
* @param e The error to classify.
|
||||
* @returns Either `e` or a corresponding `ConfigurationError`.
|
||||
*/
|
||||
export function wrapApiConfigurationError<T>(e: T): T | ConfigurationError {
|
||||
const httpError = asHTTPError(e);
|
||||
if (httpError !== undefined) {
|
||||
if (
|
||||
|
||||
75
src/codeql-bundle.test.ts
Normal file
75
src/codeql-bundle.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import test from "ava";
|
||||
|
||||
import { getCodeQLBundleFromUrl, getCodeQLBundleName } from "./codeql-bundle";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { BundlePlatform } from "./platform";
|
||||
|
||||
test("getCodeQLBundleName returns a per-language bundle name only when a language is specified", (t) => {
|
||||
t.is(
|
||||
getCodeQLBundleName("zstd", BundlePlatform.Linux64, BuiltInLanguage.java),
|
||||
"codeql-bundle-java-linux64.tar.zst",
|
||||
);
|
||||
t.is(
|
||||
getCodeQLBundleName("zstd", BundlePlatform.Linux64),
|
||||
"codeql-bundle-linux64.tar.zst",
|
||||
);
|
||||
});
|
||||
|
||||
test("getCodeQLBundleName names the Swift bundle for macOS", (t) => {
|
||||
t.is(
|
||||
getCodeQLBundleName("zstd", BundlePlatform.Osx64, BuiltInLanguage.swift),
|
||||
"codeql-bundle-swift-osx64.tar.zst",
|
||||
);
|
||||
});
|
||||
|
||||
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 });
|
||||
});
|
||||
71
src/codeql-bundle.ts
Normal file
71
src/codeql-bundle.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { BundlePlatform } from "./platform";
|
||||
import type { CompressionMethod } from "./tar";
|
||||
|
||||
/** 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;
|
||||
};
|
||||
|
||||
/** 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: 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;
|
||||
}
|
||||
|
||||
/** Returns the exact bundle asset name for a platform and optional language. */
|
||||
export function getCodeQLBundleName(
|
||||
compressionMethod: CompressionMethod,
|
||||
platform: BundlePlatform | undefined,
|
||||
language?: BuiltInLanguage,
|
||||
): string {
|
||||
const extensions: Record<CompressionMethod, string> = {
|
||||
gzip: ".tar.gz",
|
||||
zstd: ".tar.zst",
|
||||
};
|
||||
const extension = extensions[compressionMethod];
|
||||
if (platform === undefined) {
|
||||
return `codeql-bundle${extension}`;
|
||||
}
|
||||
if (language !== undefined) {
|
||||
return `codeql-bundle-${language}-${platform}${extension}`;
|
||||
}
|
||||
return `codeql-bundle-${platform}${extension}`;
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
@@ -937,6 +937,7 @@ async function getCodeQLForCmd(
|
||||
"--format=json",
|
||||
`--language=${language}`,
|
||||
"--extractor-include-aliases",
|
||||
"-J-XX:-UsePerfData",
|
||||
...getExtraOptionsFromEnv(["resolve", "extractor"]),
|
||||
],
|
||||
{
|
||||
@@ -951,7 +952,13 @@ async function getCodeQLForCmd(
|
||||
},
|
||||
},
|
||||
).exec();
|
||||
return JSON.parse(extractorPath) as string;
|
||||
try {
|
||||
return JSON.parse(extractorPath) as string;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to parse extractor path for '${language}' from CLI: ${getErrorMessage(err)}\nOutput was: ${extractorPath}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
async resolveQueriesStartingPacks(queries: string[]): Promise<string[]> {
|
||||
const codeqlArgs = [
|
||||
|
||||
@@ -1425,7 +1425,12 @@ export async function getConfig(
|
||||
}
|
||||
if (config.version !== getActionVersion()) {
|
||||
throw new ConfigurationError(
|
||||
`Loaded a configuration file for version '${config.version}', but running version '${getActionVersion()}'`,
|
||||
[
|
||||
`Loaded a configuration file for version '${config.version}', but running version '${getActionVersion()}'.`,
|
||||
"All steps in a workflow that use `github/codeql-action` must use the same version to work correctly.",
|
||||
"If you are using Dependabot to manage dependency updates, you can configure a dependency group to update all `github/codeql-action` steps at the same time.",
|
||||
"For more information, see https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#groups--",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
checkExpectedLogMessages,
|
||||
createFeatures,
|
||||
createTestConfig,
|
||||
getRecordingLogger,
|
||||
LoggedMessage,
|
||||
getTestEnv,
|
||||
initAllState,
|
||||
RecordingLogger,
|
||||
setupActionsVars,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
@@ -90,23 +91,24 @@ test.serial(
|
||||
"Abort database upload if 'upload-database' input set to false",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("false");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Database upload disabled in workflow. Skipping upload.",
|
||||
]);
|
||||
});
|
||||
@@ -117,7 +119,8 @@ test.serial(
|
||||
"Abort database upload if 'analysis-kinds: code-scanning' is not enabled",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -126,8 +129,9 @@ test.serial(
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
{
|
||||
@@ -135,10 +139,9 @@ test.serial(
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
},
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
|
||||
]);
|
||||
});
|
||||
@@ -147,7 +150,8 @@ test.serial(
|
||||
|
||||
test.serial("Abort database upload if running against GHES", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -157,16 +161,16 @@ test.serial("Abort database upload if running against GHES", async (t) => {
|
||||
const config = getTestConfig(tmpDir);
|
||||
config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" };
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Not running against github.com or GHEC-DR. Skipping upload.",
|
||||
]);
|
||||
});
|
||||
@@ -176,23 +180,24 @@ test.serial(
|
||||
"Abort database upload if not analyzing default branch",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("true");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Not analyzing default branch. Skipping upload.",
|
||||
]);
|
||||
});
|
||||
@@ -203,7 +208,8 @@ test.serial(
|
||||
"Don't crash if uploading a database fails with a non-retryable error",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -212,17 +218,17 @@ test.serial(
|
||||
|
||||
const databaseUploadSpy = await mockHttpRequests(422);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Failed to upload database for javascript: some error message",
|
||||
]);
|
||||
|
||||
@@ -236,7 +242,8 @@ test.serial(
|
||||
"Don't crash if uploading a database fails with a retryable error",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -251,17 +258,17 @@ test.serial(
|
||||
.stub(global, "setTimeout")
|
||||
.callsFake((fn: () => void) => originalSetTimeout(fn, 0));
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Failed to upload database for javascript: some error message",
|
||||
]);
|
||||
|
||||
@@ -279,7 +286,8 @@ test.serial(
|
||||
|
||||
test.serial("Successfully uploading a database to github.com", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -288,16 +296,16 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Successfully uploaded database for javascript",
|
||||
]);
|
||||
});
|
||||
@@ -305,7 +313,8 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
|
||||
|
||||
test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -314,8 +323,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
|
||||
|
||||
const databaseUploadSpy = await mockHttpRequests(201);
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const logger = new RecordingLogger();
|
||||
await cleanupAndUploadDatabases(
|
||||
initAllState({ env, logger }),
|
||||
testRepoName,
|
||||
getCodeQL(),
|
||||
getTestConfig(tmpDir),
|
||||
@@ -324,10 +334,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
|
||||
url: "https://tenant.ghe.com",
|
||||
apiURL: undefined,
|
||||
},
|
||||
createFeatures([]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
checkExpectedLogMessages(t, loggedMessages, [
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
"Successfully uploaded database for javascript",
|
||||
]);
|
||||
t.assert(
|
||||
@@ -343,7 +352,8 @@ test.serial(
|
||||
"Records overlay and clear cleanup sizes when uploading an overlay-base database",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -375,14 +385,16 @@ test.serial(
|
||||
const config = getTestConfig(tmpDir);
|
||||
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
initAllState({
|
||||
env,
|
||||
features: createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
}),
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
"",
|
||||
);
|
||||
|
||||
// The database should be cleaned up at the `overlay` level for the upload
|
||||
@@ -402,7 +414,8 @@ test.serial(
|
||||
"Does not measure clear cleanup size for a regular (non-overlay-base) upload",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -422,12 +435,15 @@ test.serial(
|
||||
});
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
initAllState({
|
||||
env,
|
||||
features: createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
}),
|
||||
testRepoName,
|
||||
codeql,
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
"",
|
||||
);
|
||||
|
||||
// A regular upload is cleaned only once, at the `clear` level.
|
||||
@@ -441,7 +457,8 @@ test.serial(
|
||||
|
||||
test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -465,12 +482,15 @@ test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
|
||||
config.debugMode = true;
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
initAllState({
|
||||
env,
|
||||
features: createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
}),
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
"",
|
||||
);
|
||||
|
||||
// In debug mode we clean up at the `overlay` level for the upload but skip
|
||||
@@ -486,7 +506,8 @@ test.serial(
|
||||
"Does not record a clear cleanup duration when the clear cleanup fails",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
const env = getTestEnv();
|
||||
setupActionsVars(tmpDir, tmpDir, {}, env);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
@@ -510,12 +531,15 @@ test.serial(
|
||||
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
initAllState({
|
||||
env,
|
||||
features: createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
}),
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
"",
|
||||
);
|
||||
|
||||
// When the `clear` cleanup fails, no size is measured, so we should not
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { AnalysisKind } from "./analyses";
|
||||
import {
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from "./api-client";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Feature } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
@@ -45,13 +46,15 @@ export interface DatabaseUploadResult {
|
||||
}
|
||||
|
||||
export async function cleanupAndUploadDatabases(
|
||||
action: ActionState<["Logger", "FeatureFlags"]>,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
apiDetails: GitHubApiDetails,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
checkoutPath: string,
|
||||
): Promise<DatabaseUploadResult[]> {
|
||||
const logger = action.logger;
|
||||
|
||||
if (actionsUtil.getRequiredInput("upload-database") !== "true") {
|
||||
logger.debug("Database upload disabled in workflow. Skipping upload.");
|
||||
return [];
|
||||
@@ -87,7 +90,7 @@ export async function cleanupAndUploadDatabases(
|
||||
// If config.overlayDatabaseMode is OverlayBase, then we have overlay base databases for all languages.
|
||||
const shouldUploadOverlayBase =
|
||||
config.overlayDatabaseMode === OverlayDatabaseMode.OverlayBase &&
|
||||
(await features.getValue(Feature.UploadOverlayDbToApi, codeql));
|
||||
(await action.features.getValue(Feature.UploadOverlayDbToApi, codeql));
|
||||
const cleanupLevel = shouldUploadOverlayBase
|
||||
? CleanupLevel.Overlay
|
||||
: CleanupLevel.Clear;
|
||||
@@ -110,9 +113,7 @@ export async function cleanupAndUploadDatabases(
|
||||
includeDiagnostics: false,
|
||||
});
|
||||
bundledDbSize = fs.statSync(bundledDb).size;
|
||||
const commitOid = await gitUtils.getCommitOid(
|
||||
actionsUtil.getRequiredInput("checkout_path"),
|
||||
);
|
||||
const commitOid = await gitUtils.getCommitOid(checkoutPath);
|
||||
// Upload with manual retry logic. We disable Octokit's built-in retries
|
||||
// because the request body is a ReadStream, which can only be consumed
|
||||
// once.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -29,10 +29,13 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = currentSha;
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("HEAD").resolves(currentSha);
|
||||
callback.withArgs(sinon.match.string, "HEAD").resolves(currentSha);
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 1);
|
||||
t.true(callback.calledOnceWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -48,11 +51,17 @@ test.serial(
|
||||
const sha = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("refs/remotes/pull/1/merge").resolves(sha);
|
||||
callback.withArgs("HEAD").resolves(sha);
|
||||
callback
|
||||
.withArgs(sinon.match.string, "refs/remotes/pull/1/merge")
|
||||
.resolves(sha);
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves(sha);
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, expectedRef);
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -66,11 +75,18 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40));
|
||||
callback
|
||||
.withArgs(tmpDir, "refs/remotes/pull/1/merge")
|
||||
.resolves("a".repeat(40));
|
||||
callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40));
|
||||
callback.throws(new Error("Unexpected getCommitOid call in test."));
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/1/head");
|
||||
|
||||
t.is(callback.callCount, 2);
|
||||
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
|
||||
t.true(callback.calledWith(tmpDir, "HEAD"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -92,11 +108,14 @@ test.serial(
|
||||
process.env["GITHUB_SHA"] = "a".repeat(40);
|
||||
|
||||
const callback = sinon.stub(gitUtils, "getCommitOid");
|
||||
callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40));
|
||||
callback.withArgs("HEAD").resolves("b".repeat(40));
|
||||
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("b".repeat(40));
|
||||
callback.withArgs(sinon.match.any, "HEAD").resolves("b".repeat(40));
|
||||
|
||||
const actualRef = await gitUtils.getRef();
|
||||
t.deepEqual(actualRef, "refs/pull/2/merge");
|
||||
|
||||
// getCommitOid shouldn't be called, because the ref should be taken from the input
|
||||
t.is(callback.callCount, 0);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getTemporaryDirectory,
|
||||
printDebugLogs,
|
||||
} from "./actions-util";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { getCodeQL } from "./codeql";
|
||||
import { type Config, getConfig } from "./config-utils";
|
||||
@@ -62,9 +62,11 @@ async function run(startedAt: Date) {
|
||||
const jobStatus = getOptionalInput("job-status");
|
||||
|
||||
// Restore inputs from `init` Action.
|
||||
restoreInputs();
|
||||
restoreInputs(logger);
|
||||
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
config = await getConfig(getTemporaryDirectory(), logger);
|
||||
|
||||
const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
|
||||
checkGitHubVersionInRange(gitHubVersion, logger);
|
||||
|
||||
const repositoryNwo = getRepositoryNwo();
|
||||
@@ -75,7 +77,6 @@ async function run(startedAt: Date) {
|
||||
logger,
|
||||
);
|
||||
|
||||
config = await getConfig(getTemporaryDirectory(), logger);
|
||||
if (config === undefined) {
|
||||
logger.warning(
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
|
||||
@@ -107,7 +108,7 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
} catch (unwrappedError) {
|
||||
const error = wrapError(unwrappedError);
|
||||
const error = wrapApiConfigurationError(wrapError(unwrappedError));
|
||||
core.setFailed(error.message);
|
||||
|
||||
const statusReportBase = await createStatusReportBase(
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
makeDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "./diagnostics";
|
||||
import { EnvVar } from "./environment";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
|
||||
import { loadRepositoryProperties } from "./feature-flags/properties";
|
||||
import {
|
||||
@@ -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";
|
||||
@@ -81,7 +81,6 @@ import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
getCodeQLMemoryLimit,
|
||||
getRequiredEnvParam,
|
||||
getThreadsFlagValue,
|
||||
initializeEnvironment,
|
||||
ConfigurationError,
|
||||
@@ -168,23 +167,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`
|
||||
@@ -238,8 +224,8 @@ async function run(
|
||||
apiDetails = {
|
||||
auth: getRequiredInput("token"),
|
||||
externalRepoAuth: getOptionalInput("external-repository-token"),
|
||||
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
|
||||
url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
|
||||
apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL),
|
||||
};
|
||||
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
@@ -268,7 +254,7 @@ async function run(
|
||||
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
|
||||
// source-root is absolute, it is used as given.
|
||||
sourceRoot = path.resolve(
|
||||
getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
actionState.env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE),
|
||||
getOptionalInput("source-root") || "",
|
||||
);
|
||||
|
||||
@@ -396,7 +382,9 @@ async function run(
|
||||
repository: repositoryNwo,
|
||||
tempDir: getTemporaryDirectory(),
|
||||
codeql,
|
||||
workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
workspacePath: actionState.env.getRequired(
|
||||
ActionsEnvVars.GITHUB_WORKSPACE,
|
||||
),
|
||||
sourceRoot,
|
||||
githubVersion: gitHubVersion,
|
||||
apiDetails,
|
||||
@@ -728,7 +716,7 @@ async function run(
|
||||
);
|
||||
}
|
||||
|
||||
const tracerConfig = await getCombinedTracerConfig(codeql, config);
|
||||
const tracerConfig = await getCombinedTracerConfig(logger, codeql, config);
|
||||
if (tracerConfig !== undefined) {
|
||||
for (const [key, value] of Object.entries(tracerConfig.env)) {
|
||||
core.exportVariable(key, value);
|
||||
|
||||
@@ -3,11 +3,7 @@ import * as fs from "fs";
|
||||
import * as actionsCache from "@actions/cache";
|
||||
import * as semver from "semver";
|
||||
|
||||
import {
|
||||
getRequiredInput,
|
||||
getWorkflowRunAttempt,
|
||||
getWorkflowRunID,
|
||||
} from "../actions-util";
|
||||
import { getWorkflowRunAttempt, getWorkflowRunID } from "../actions-util";
|
||||
import { getAutomationID, listActionsCaches } from "../api-client";
|
||||
import { createCacheKeyHash } from "../caching-utils";
|
||||
import { type CodeQL } from "../codeql";
|
||||
@@ -107,12 +103,13 @@ async function checkOverlayBaseDatabase(
|
||||
* Uploads the overlay-base database to the GitHub Actions cache. If conditions
|
||||
* for uploading are not met, the function does nothing and returns false.
|
||||
*
|
||||
* This function uses the `checkout_path` input to determine the repository path
|
||||
* This function uses the `checkoutPath` to determine the repository path
|
||||
* and works only when called from `analyze` or `upload-sarif`.
|
||||
*
|
||||
* @param codeql The CodeQL instance
|
||||
* @param config The configuration object
|
||||
* @param logger The logger instance
|
||||
* @param checkoutPath The path at which the repository is checked out at.
|
||||
* @returns A promise that resolves to true if the upload was performed and
|
||||
* successfully completed, or false otherwise
|
||||
*/
|
||||
@@ -120,6 +117,7 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
logger: Logger,
|
||||
checkoutPath: string,
|
||||
): Promise<boolean> {
|
||||
const overlayDatabaseMode = config.overlayDatabaseMode;
|
||||
if (overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase) {
|
||||
@@ -180,7 +178,6 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache(
|
||||
}
|
||||
|
||||
const codeQlVersion = (await codeql.getVersion()).version;
|
||||
const checkoutPath = getRequiredInput("checkout_path");
|
||||
const cacheSaveKey = await getCacheSaveKey(
|
||||
config,
|
||||
codeQlVersion,
|
||||
|
||||
175
src/per-language-bundles.test.ts
Normal file
175
src/per-language-bundles.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
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]),
|
||||
...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({
|
||||
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,
|
||||
);
|
||||
});
|
||||
146
src/per-language-bundles.ts
Normal file
146
src/per-language-bundles.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
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");
|
||||
}
|
||||
|
||||
// Nightly releases are identified by dates rather than versions. If
|
||||
// `isLatestNightly` is `true`, the latest nightly is requested with
|
||||
// `tools: nightly` and we don't yet have the corresponding tag at this point.
|
||||
// Therefore, we skip the version check and don't have an equivalent.
|
||||
// We can safely assume that the latest nightly will have per-language bundles.
|
||||
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;
|
||||
}
|
||||
|
||||
/** Explains why an eligible per-language bundle is being replaced by a combined bundle. */
|
||||
export function logPerLanguageBundleFallback(
|
||||
{ logger }: ActionState<["Logger"]>,
|
||||
language: BuiltInLanguage,
|
||||
location: string,
|
||||
): void {
|
||||
logger.warning(
|
||||
`No per-language CodeQL bundle for '${language}' was found at ${location}, so ` +
|
||||
"falling back to the bundle that contains all languages. This analysis will still " +
|
||||
"produce correct results, but will take longer to set up.",
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import { Logger } from "../logging";
|
||||
import { getErrorMessage } from "../util";
|
||||
|
||||
import * as sarif from "sarif";
|
||||
|
||||
@@ -48,7 +49,13 @@ export function getToolNames(sarifFile: Partial<sarif.Log>): string[] {
|
||||
* @returns The resulting JSON value, cast to a SARIF `Log`.
|
||||
*/
|
||||
export function readSarifFile(sarifFilePath: string): Partial<sarif.Log> {
|
||||
return JSON.parse(fs.readFileSync(sarifFilePath, "utf8")) as sarif.Log;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(sarifFilePath, "utf8")) as sarif.Log;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Parsing SARIF file at '${sarifFilePath}' failed: ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a list of paths to sarif files and combines them together,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getGitHubVersion } from "./api-client";
|
||||
import { CodeQL } from "./codeql";
|
||||
import { ComputedInput, getToolsInput } from "./config/inputs";
|
||||
import { getRawLanguagesNoAutodetect } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { loadRepositoryProperties } from "./feature-flags/properties";
|
||||
import { initCodeQL } from "./init";
|
||||
@@ -22,17 +22,16 @@ 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,
|
||||
checkForTimeout,
|
||||
checkGitHubVersionInRange,
|
||||
getRequiredEnvParam,
|
||||
initializeEnvironment,
|
||||
ConfigurationError,
|
||||
wrapError,
|
||||
@@ -79,23 +78,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 });
|
||||
}
|
||||
@@ -121,8 +107,8 @@ async function run(
|
||||
const apiDetails = {
|
||||
auth: getRequiredInput("token"),
|
||||
externalRepoAuth: getOptionalInput("external-repository-token"),
|
||||
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
|
||||
url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
|
||||
apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL),
|
||||
};
|
||||
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
|
||||
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,12 @@ import {
|
||||
isRunningLocalAction,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import {
|
||||
CodeQLBundle,
|
||||
CodeQLDownloadSource,
|
||||
getCodeQLBundleFromUrl,
|
||||
getCodeQLBundleName,
|
||||
} from "./codeql-bundle";
|
||||
import * as defaults from "./defaults.json";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
@@ -30,8 +37,14 @@ import {
|
||||
Feature,
|
||||
FeatureEnablement,
|
||||
} from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
||||
import {
|
||||
getPerLanguageBundleLanguage,
|
||||
logPerLanguageBundleFallback,
|
||||
} from "./per-language-bundles";
|
||||
import { getBundlePlatform } from "./platform";
|
||||
import * as tar from "./tar";
|
||||
import {
|
||||
deleteToolcacheBundles,
|
||||
@@ -44,6 +57,8 @@ import {
|
||||
import * as util from "./util";
|
||||
import { isGoodVersion } from "./util";
|
||||
|
||||
export type { CodeQLDownloadSource } from "./codeql-bundle";
|
||||
|
||||
export enum ToolsSource {
|
||||
Unknown = "UNKNOWN",
|
||||
Local = "LOCAL",
|
||||
@@ -59,37 +74,6 @@ const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"];
|
||||
const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
|
||||
const CODEQL_TOOLCACHE_INPUT = "toolcache";
|
||||
|
||||
function getCodeQLBundleExtension(
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
): string {
|
||||
switch (compressionMethod) {
|
||||
case "gzip":
|
||||
return ".tar.gz";
|
||||
case "zstd":
|
||||
return ".tar.zst";
|
||||
default:
|
||||
util.assertNever(compressionMethod);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCodeQLBundleName(
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
): string {
|
||||
const extension = getCodeQLBundleExtension(compressionMethod);
|
||||
|
||||
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 {
|
||||
return `codeql-bundle${extension}`;
|
||||
}
|
||||
return `codeql-bundle-${platform}${extension}`;
|
||||
}
|
||||
|
||||
export function getCodeQLActionRepository(logger: Logger): string {
|
||||
if (isRunningLocalAction()) {
|
||||
// This handles the case where the Action does not come from an Action repository,
|
||||
@@ -107,7 +91,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 +110,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 +147,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;
|
||||
@@ -229,17 +212,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 +421,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 +431,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 +440,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 +475,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 +563,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 +580,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 +703,56 @@ 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 platform = getBundlePlatform();
|
||||
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
|
||||
{ env: getEnv(), features, logger },
|
||||
{
|
||||
rawLanguages,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform,
|
||||
variant,
|
||||
},
|
||||
);
|
||||
|
||||
// Resolves the combined or per-language bundle URL for the requested release.
|
||||
const resolveBundleURL = (language?: BuiltInLanguage) =>
|
||||
getCodeQLBundleDownloadURL(
|
||||
bundleTagName,
|
||||
apiDetails,
|
||||
getCodeQLBundleName(compressionMethod, platform, 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 +762,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 +777,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 +796,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 +810,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 +839,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 +854,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 +1025,6 @@ export async function setupCodeQLBundle(
|
||||
);
|
||||
|
||||
let codeqlFolder: string;
|
||||
let toolsVersion = source.toolsVersion;
|
||||
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
|
||||
let toolsSource: ToolsSource;
|
||||
switch (source.sourceType) {
|
||||
@@ -1021,18 +1045,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 +1069,73 @@ 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;
|
||||
}
|
||||
logPerLanguageBundleFallback(action, bundle.language, bundle.url);
|
||||
|
||||
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 +1153,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 +1171,16 @@ async function getNightlyToolsUrl(logger: Logger) {
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
const platform = getBundlePlatform();
|
||||
const language = await getPerLanguageBundleLanguage(action, {
|
||||
rawLanguages,
|
||||
cliVersion: undefined,
|
||||
compressionMethod,
|
||||
platform,
|
||||
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 +1196,21 @@ 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, platform, language),
|
||||
);
|
||||
return language === undefined
|
||||
? { kind: "combined", url }
|
||||
: {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language,
|
||||
combinedBundleURL: assetUrl(
|
||||
getCodeQLBundleName(compressionMethod, platform),
|
||||
),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to retrieve the latest nightly release: ${util.wrapError(e)}`,
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function runWrapper() {
|
||||
|
||||
try {
|
||||
// Restore inputs from `start-proxy` Action.
|
||||
actionsUtil.restoreInputs();
|
||||
actionsUtil.restoreInputs(logger);
|
||||
|
||||
// Kill the running proxy
|
||||
const pid = core.getState("proxy-process-pid");
|
||||
@@ -38,12 +38,6 @@ export async function runWrapper() {
|
||||
logger.info(
|
||||
"Debug mode is on. Uploading proxy log as Actions debugging artifact...",
|
||||
);
|
||||
if (config?.gitHubVersion.type === undefined) {
|
||||
logger.warning(
|
||||
`Did not upload debug artifacts because cannot determine the GitHub variant running.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
checkGitHubVersionInRange(gitHubVersion, logger);
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
};
|
||||
|
||||
// Start the Proxy
|
||||
const proxyBin = await getProxyBinaryPath(logger, features);
|
||||
const proxyBin = await getProxyBinaryPath({ ...action, features });
|
||||
const proxyInfo = await startProxy(
|
||||
proxyBin,
|
||||
proxyConfig,
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as toolcache from "@actions/tool-cache";
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import * as apiClient from "./api-client";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { setUpFeatureFlagTests } from "./feature-flags/testing-util";
|
||||
@@ -12,12 +13,14 @@ import { UnvalidatedObject, validateSchema } from "./json";
|
||||
import { makeFromSchema } from "./json/testing-util";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
import * as startProxyExports from "./start-proxy";
|
||||
import * as statusReport from "./status-report";
|
||||
import {
|
||||
assertNotLogged,
|
||||
checkExpectedLogMessages,
|
||||
createFeatures,
|
||||
initAllState,
|
||||
makeMacro,
|
||||
makeTestToken,
|
||||
RecordingLogger,
|
||||
@@ -685,6 +688,12 @@ test("getCredentials always returns ALWAYS_ENABLED_REGISTRY_TYPE credentials for
|
||||
}
|
||||
});
|
||||
|
||||
test("getProxyPackage - includes platform in name", (t) => {
|
||||
for (const platform of Object.values(BundlePlatform)) {
|
||||
t.true(startProxyExports.getProxyPackage(platform).includes(platform));
|
||||
}
|
||||
});
|
||||
|
||||
function mockGetApiClient(endpoints: any) {
|
||||
return (
|
||||
sinon
|
||||
@@ -721,6 +730,44 @@ function mockOfflineFeatures(tempDir: string, logger: Logger) {
|
||||
return setUpFeatureFlagTests(tempDir, logger, gitHubVersion);
|
||||
}
|
||||
|
||||
/** Gets the `BundlePlatform` based on the `action` state, but defaults to `Linux64` if undefined. */
|
||||
function getTestPlatform(action: ActionState<["Base"]>) {
|
||||
return (
|
||||
getBundlePlatform(action.platform, action.arch) ?? BundlePlatform.Linux64
|
||||
);
|
||||
}
|
||||
|
||||
test.serial(
|
||||
"getDownloadUrl logs unknown platforms/arch and defaults to linux64",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
mockGetReleaseByTag();
|
||||
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const state = initAllState({
|
||||
platform: "android",
|
||||
arch: "ppc",
|
||||
logger,
|
||||
features,
|
||||
});
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(BundlePlatform.Linux64),
|
||||
),
|
||||
);
|
||||
|
||||
t.true(
|
||||
logger.hasMessage(`Unsupported platform android on architecture ppc`),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getDownloadUrl returns fallback when `getReleaseByVersion` rejects",
|
||||
async (t) => {
|
||||
@@ -729,15 +776,15 @@ test.serial(
|
||||
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
getRunnerLogger(true),
|
||||
features,
|
||||
);
|
||||
const state = initAllState({ logger, features });
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -751,18 +798,18 @@ test.serial(
|
||||
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const state = initAllState({ logger, features });
|
||||
|
||||
for (const assets of testAssets) {
|
||||
const stub = mockGetReleaseByTag(assets);
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
getRunnerLogger(true),
|
||||
features,
|
||||
);
|
||||
const info = await startProxyExports.getDownloadUrl(state);
|
||||
|
||||
t.is(info.version, startProxyExports.UPDATEJOB_PROXY_VERSION);
|
||||
t.is(
|
||||
info.url,
|
||||
startProxyExports.getFallbackUrl(startProxyExports.getProxyPackage()),
|
||||
startProxyExports.getFallbackUrl(
|
||||
startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
),
|
||||
);
|
||||
|
||||
stub.restore();
|
||||
@@ -773,18 +820,19 @@ test.serial(
|
||||
|
||||
test.serial("getDownloadUrl returns matching release asset", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const state = initAllState({ logger });
|
||||
const assets = [
|
||||
{ name: "foo", url: "other-url" },
|
||||
{ name: startProxyExports.getProxyPackage(), url: "url-we-want" },
|
||||
{
|
||||
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
url: "url-we-want",
|
||||
},
|
||||
];
|
||||
mockGetReleaseByTag(assets);
|
||||
|
||||
await withTmpDir(async (tempDir) => {
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const info = await startProxyExports.getDownloadUrl(
|
||||
getRunnerLogger(true),
|
||||
features,
|
||||
);
|
||||
const info = await startProxyExports.getDownloadUrl({ ...state, features });
|
||||
|
||||
t.is(info.version, defaults.cliVersion);
|
||||
t.is(info.url, "url-we-want");
|
||||
@@ -918,7 +966,9 @@ test.serial(
|
||||
sinon.stub(toolcache, "find").returns(toolcachePath);
|
||||
|
||||
const features = mockOfflineFeatures(tempDir, logger);
|
||||
const path = await startProxyExports.getProxyBinaryPath(logger, features);
|
||||
const path = await startProxyExports.getProxyBinaryPath(
|
||||
initAllState({ logger, features }),
|
||||
);
|
||||
|
||||
t.assert(path);
|
||||
t.is(
|
||||
@@ -933,9 +983,13 @@ test.serial(
|
||||
"getProxyBinaryPath - downloads proxy if not in cache",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const state = initAllState({ logger });
|
||||
const downloadUrl = "url-we-want";
|
||||
mockGetReleaseByTag([
|
||||
{ name: startProxyExports.getProxyPackage(), url: downloadUrl },
|
||||
{
|
||||
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
url: downloadUrl,
|
||||
},
|
||||
]);
|
||||
|
||||
const toolcachePath = "/path/to/proxy/dir";
|
||||
@@ -958,10 +1012,10 @@ test.serial(
|
||||
.resolves(extractedPath);
|
||||
const cacheDir = sinon.stub(toolcache, "cacheDir").resolves(toolcachePath);
|
||||
|
||||
const path = await startProxyExports.getProxyBinaryPath(
|
||||
logger,
|
||||
createFeatures([]),
|
||||
);
|
||||
const path = await startProxyExports.getProxyBinaryPath({
|
||||
...state,
|
||||
features: createFeatures([]),
|
||||
});
|
||||
|
||||
t.assert(find.calledOnce);
|
||||
t.assert(getApiDetails.calledOnce);
|
||||
@@ -976,7 +1030,7 @@ test.serial(
|
||||
);
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Found '${startProxyExports.getProxyPackage()}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
|
||||
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${defaults.bundleVersion}' at '${downloadUrl}'`,
|
||||
]);
|
||||
},
|
||||
);
|
||||
@@ -985,6 +1039,7 @@ test.serial(
|
||||
"getProxyBinaryPath - downloads proxy based on features if not in cache",
|
||||
async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const state = initAllState({ logger });
|
||||
const expectedTag = "codeql-bundle-v2.20.1";
|
||||
const expectedParams = {
|
||||
owner: "github",
|
||||
@@ -994,7 +1049,7 @@ test.serial(
|
||||
const downloadUrl = "url-we-want";
|
||||
const assets = [
|
||||
{
|
||||
name: startProxyExports.getProxyPackage(),
|
||||
name: startProxyExports.getProxyPackage(getTestPlatform(state)),
|
||||
url: downloadUrl,
|
||||
},
|
||||
];
|
||||
@@ -1045,7 +1100,10 @@ test.serial(
|
||||
.resolves({
|
||||
enabledVersions: [{ cliVersion: "2.20.1", tagName: expectedTag }],
|
||||
});
|
||||
const path = await startProxyExports.getProxyBinaryPath(logger, features);
|
||||
const path = await startProxyExports.getProxyBinaryPath({
|
||||
...state,
|
||||
features,
|
||||
});
|
||||
|
||||
t.assert(getDefaultCliVersion.calledOnce);
|
||||
sinon.assert.calledOnceWithMatch(
|
||||
@@ -1067,7 +1125,7 @@ test.serial(
|
||||
});
|
||||
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Found '${startProxyExports.getProxyPackage()}' in release '${expectedTag}' at '${downloadUrl}'`,
|
||||
`Found '${startProxyExports.getProxyPackage(getTestPlatform(state))}' in release '${expectedTag}' at '${downloadUrl}'`,
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as path from "path";
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getApiClient,
|
||||
getApiDetails,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
import * as json from "./json";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { BundlePlatform, getBundlePlatform } from "./platform";
|
||||
import {
|
||||
Address,
|
||||
Registry,
|
||||
@@ -370,14 +372,10 @@ export function getCredentials(
|
||||
|
||||
/**
|
||||
* Gets the name of the proxy release asset for the current platform.
|
||||
*
|
||||
* @param platform The platform to get the asset name for.
|
||||
*/
|
||||
export function getProxyPackage(): string {
|
||||
const platform =
|
||||
process.platform === "win32"
|
||||
? "win64"
|
||||
: process.platform === "darwin"
|
||||
? "osx64"
|
||||
: "linux64";
|
||||
export function getProxyPackage(platform: BundlePlatform): string {
|
||||
return `${UPDATEJOB_PROXY}-${platform}.tar.gz`;
|
||||
}
|
||||
|
||||
@@ -417,25 +415,33 @@ async function getCliVersionFromFeatures(
|
||||
* Determines the URL of the proxy release asset that we should download if its not
|
||||
* already in the toolcache, and its version.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param features Information about enabled features.
|
||||
* @param action The action state.
|
||||
* @returns Returns the download URL and version of the proxy package we plan to use.
|
||||
*/
|
||||
export async function getDownloadUrl(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
|
||||
): Promise<{ url: string; version: string }> {
|
||||
const proxyPackage = getProxyPackage();
|
||||
// Default to linux64 if we don't recognise the platform+arch pair.
|
||||
// This maintains the behaviour we had before switching to `getBundlePlatform` here.
|
||||
let platform = getBundlePlatform(action.platform, action.arch);
|
||||
if (platform === undefined) {
|
||||
action.logger.warning(
|
||||
`Unsupported platform ${action.platform} on architecture ${action.arch}, defaulting to ${BundlePlatform.Linux64}`,
|
||||
);
|
||||
platform = BundlePlatform.Linux64;
|
||||
}
|
||||
|
||||
const proxyPackage = getProxyPackage(platform);
|
||||
|
||||
try {
|
||||
const useFeaturesToDetermineCLI = await features.getValue(
|
||||
const useFeaturesToDetermineCLI = await action.features.getValue(
|
||||
Feature.StartProxyUseFeaturesRelease,
|
||||
);
|
||||
|
||||
// Retrieve information about the CLI version we should use. This will be either the linked
|
||||
// version, or the one enabled by FFs.
|
||||
const versionInfo = useFeaturesToDetermineCLI
|
||||
? (await getCliVersionFromFeatures(features)).enabledVersions[0]
|
||||
? (await getCliVersionFromFeatures(action.features)).enabledVersions[0]
|
||||
: {
|
||||
cliVersion: defaults.cliVersion,
|
||||
tagName: defaults.bundleVersion,
|
||||
@@ -447,7 +453,7 @@ export async function getDownloadUrl(
|
||||
// Search the release's assets to find the one we are looking for.
|
||||
for (const asset of cliRelease.data.assets) {
|
||||
if (asset.name === proxyPackage) {
|
||||
logger.info(
|
||||
action.logger.info(
|
||||
`Found '${proxyPackage}' in release '${versionInfo.tagName}' at '${asset.url}'`,
|
||||
);
|
||||
return {
|
||||
@@ -460,13 +466,13 @@ export async function getDownloadUrl(
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.warning(
|
||||
action.logger.warning(
|
||||
`Failed to retrieve information about the linked release: ${getErrorMessage(ex)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to the hard-coded URL.
|
||||
logger.info(
|
||||
action.logger.info(
|
||||
`Did not find '${proxyPackage}' in the linked release, falling back to hard-coded version.`,
|
||||
);
|
||||
return {
|
||||
@@ -559,15 +565,15 @@ export function getProxyFilename() {
|
||||
* runner's tool cache. Otherwise, it downloads and extracts the proxy binary,
|
||||
* and stores it in the tool cache.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param action The action state.
|
||||
* @returns The path to the proxy binary.
|
||||
*/
|
||||
export async function getProxyBinaryPath(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
action: ActionState<["Base", "Logger", "FeatureFlags"]>,
|
||||
): Promise<string> {
|
||||
const logger = action.logger;
|
||||
const proxyFileName = getProxyFilename();
|
||||
const proxyInfo = await getDownloadUrl(logger, features);
|
||||
const proxyInfo = await getDownloadUrl(action);
|
||||
|
||||
let proxyBin = toolcache.find(proxyFileName, proxyInfo.version);
|
||||
if (!proxyBin) {
|
||||
|
||||
@@ -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,
|
||||
@@ -233,6 +234,8 @@ export function initAllState(
|
||||
return {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
logger: new RecordingLogger(),
|
||||
env,
|
||||
actions: getTestActionsEnv(env),
|
||||
@@ -933,21 +936,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);
|
||||
|
||||
@@ -7,7 +7,12 @@ import * as sinon from "sinon";
|
||||
import { CodeQL, getCodeQLForTesting } from "./codeql";
|
||||
import * as configUtils from "./config-utils";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { createTestConfig, makeVersionInfo, setupTests } from "./testing-utils";
|
||||
import {
|
||||
createTestConfig,
|
||||
makeVersionInfo,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
import { ToolsFeature } from "./tools-features";
|
||||
import { getCombinedTracerConfig } from "./tracer-config";
|
||||
import * as util from "./util";
|
||||
@@ -42,18 +47,20 @@ async function stubCodeql(
|
||||
}
|
||||
|
||||
test("getCombinedTracerConfig - return undefined when no languages are traced languages", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
const config = getTestConfig(tmpDir);
|
||||
// No traced languages
|
||||
config.languages = [BuiltInLanguage.javascript, BuiltInLanguage.python];
|
||||
t.deepEqual(
|
||||
await getCombinedTracerConfig(await stubCodeql(), config),
|
||||
await getCombinedTracerConfig(logger, await stubCodeql(), config),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("getCombinedTracerConfig", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
await util.withTmpDir(async (tmpDir) => {
|
||||
const config = getTestConfig(tmpDir);
|
||||
|
||||
@@ -82,7 +89,11 @@ test("getCombinedTracerConfig", async (t) => {
|
||||
);
|
||||
fs.writeFileSync(startTracingJson, JSON.stringify(startTracingEnv));
|
||||
|
||||
const result = await getCombinedTracerConfig(await stubCodeql(), config);
|
||||
const result = await getCombinedTracerConfig(
|
||||
logger,
|
||||
await stubCodeql(),
|
||||
config,
|
||||
);
|
||||
t.notDeepEqual(result, undefined);
|
||||
|
||||
t.false(Object.prototype.hasOwnProperty.call(result?.env, "CODEQL_RUNNER"));
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from "path";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config-utils";
|
||||
import { Logger } from "./logging";
|
||||
import { asyncSome, BuildMode } from "./util";
|
||||
import { asyncSome, BuildMode, getErrorMessage } from "./util";
|
||||
|
||||
export type TracerConfig = {
|
||||
env: { [key: string]: string };
|
||||
@@ -77,23 +77,28 @@ export async function endTracingForCluster(
|
||||
}
|
||||
|
||||
async function getTracerConfigForCluster(
|
||||
logger: Logger,
|
||||
config: Config,
|
||||
): Promise<TracerConfig> {
|
||||
const tracingEnvVariables = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(
|
||||
config.dbLocation,
|
||||
"temp/tracingEnvironment/start-tracing.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
const filePath = path.resolve(
|
||||
config.dbLocation,
|
||||
"temp/tracingEnvironment/start-tracing.json",
|
||||
);
|
||||
return {
|
||||
env: tracingEnvVariables,
|
||||
};
|
||||
try {
|
||||
const tracingEnvVariables = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
return {
|
||||
env: tracingEnvVariables,
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Failed to parse tracing environment from '${filePath}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
throw new Error(`Failed to parse tracing environment from '${filePath}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCombinedTracerConfig(
|
||||
logger: Logger,
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
): Promise<TracerConfig | undefined> {
|
||||
@@ -101,5 +106,5 @@ export async function getCombinedTracerConfig(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await getTracerConfigForCluster(config);
|
||||
return await getTracerConfigForCluster(logger, config);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ export async function runWrapper() {
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
try {
|
||||
// Restore inputs from `upload-sarif` Action.
|
||||
actionsUtil.restoreInputs();
|
||||
const logger = getActionsLogger();
|
||||
|
||||
// Restore inputs from `upload-sarif` Action.
|
||||
actionsUtil.restoreInputs(logger);
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
checkGitHubVersionInRange(gitHubVersion, 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());
|
||||
|
||||
49
src/util.ts
49
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 `startTime` was recorded with `performance.now()`. */
|
||||
export function durationMsSince(startTime: number): number {
|
||||
return Math.round(performance.now() - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param milliseconds time to delay
|
||||
* @param opts options
|
||||
@@ -865,7 +894,13 @@ export function parseMatrixInput(
|
||||
if (matrixInput === undefined || matrixInput === "null") {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(matrixInput) as { [key: string]: string };
|
||||
try {
|
||||
return JSON.parse(matrixInput) as { [key: string]: string };
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to parse matrix input '${matrixInput}': ${getErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapError(error: unknown): Error {
|
||||
@@ -1008,7 +1043,11 @@ export enum BuildMode {
|
||||
}
|
||||
|
||||
export function cloneObject<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj)) as T;
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj)) as T;
|
||||
} catch (err) {
|
||||
throw new Error(`Cloning object failed: ${getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanUpPath(file: string, name: string, logger: Logger) {
|
||||
|
||||
@@ -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