mirror of
https://github.com/github/codeql-action.git
synced 2026-10-06 19:05:18 +00:00
Compare commits
342 Commits
codeql-bun
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f455295c9b | ||
|
|
7265309f7a | ||
|
|
d1ca36fda6 | ||
|
|
2c8d410c03 | ||
|
|
118af5959a | ||
|
|
61c3b54338 | ||
|
|
12db63b222 | ||
|
|
55e4091e2b | ||
|
|
8e6b600bd0 | ||
|
|
9d3242ebc0 | ||
|
|
e0b8480262 | ||
|
|
6cb83faca6 | ||
|
|
a4a04dc476 | ||
|
|
966f3c3593 | ||
|
|
711f3bffae | ||
|
|
ac4b9b891d | ||
|
|
c1e7769cf5 | ||
|
|
1767808e11 | ||
|
|
aeafd00d94 | ||
|
|
363d874905 | ||
|
|
fa90489493 | ||
|
|
d8251b8572 | ||
|
|
b6d38e563b | ||
|
|
b948c4d91e | ||
|
|
894842692f | ||
|
|
62b0dd96ff | ||
|
|
c275b4a69e | ||
|
|
4d9cd8c21b | ||
|
|
412197a0f3 | ||
|
|
0cd78b35d9 | ||
|
|
c26ee0ca71 | ||
|
|
6ce88f6aaf | ||
|
|
1f8080af29 | ||
|
|
56b0d5f02c | ||
|
|
b804ae96cc | ||
|
|
a47cc9ff16 | ||
|
|
27c60c532d | ||
|
|
c535168544 | ||
|
|
488027b53c | ||
|
|
dc1f3ac122 | ||
|
|
05d2a1a6fb | ||
|
|
f6a7f00613 | ||
|
|
113b18e688 | ||
|
|
e869836b5a | ||
|
|
1c0814d7d2 | ||
|
|
ce28f3e445 | ||
|
|
f24d88130e | ||
|
|
f45aab1a23 | ||
|
|
d036b81d95 | ||
|
|
f306a93590 | ||
|
|
ffc4a0cd10 | ||
|
|
d06baaa128 | ||
|
|
a6cd2a544a | ||
|
|
1bb99cb5c7 | ||
|
|
725421c272 | ||
|
|
70897a78fe | ||
|
|
a4fbe39872 | ||
|
|
a98f604084 | ||
|
|
2da0d298e0 | ||
|
|
87a1923fea | ||
|
|
222d54a705 | ||
|
|
cbe61e2f00 | ||
|
|
f1b8b0f7b3 | ||
|
|
99522b4961 | ||
|
|
7816c33354 | ||
|
|
d8b6f2a09b | ||
|
|
1609a51ab7 | ||
|
|
5e3132db95 | ||
|
|
b5a476b47f | ||
|
|
61817fadad | ||
|
|
cd1a7c16f0 | ||
|
|
8e838ff396 | ||
|
|
10e476ce02 | ||
|
|
fc2d2f7d2d | ||
|
|
62a2268b34 | ||
|
|
97a91d9861 | ||
|
|
85c0c095cb | ||
|
|
503e2f62a9 | ||
|
|
260cb2adcf | ||
|
|
31252c2639 | ||
|
|
1991d2001e | ||
|
|
0af3281195 | ||
|
|
8dfe937b99 | ||
|
|
8102666a8c | ||
|
|
7b6a151fb5 | ||
|
|
29b01b6799 | ||
|
|
fa2bea7c7a | ||
|
|
9fb2fa5f46 | ||
|
|
054b25e1c2 | ||
|
|
fa8392b7e5 | ||
|
|
38c1d74ffa | ||
|
|
19aa55de51 | ||
|
|
a7334dd080 | ||
|
|
aa2cb993a1 | ||
|
|
9c970806c4 | ||
|
|
415d925fee | ||
|
|
b5f938c947 | ||
|
|
73dc34459c | ||
|
|
266e866100 | ||
|
|
2892aa5e19 | ||
|
|
8ad03a333e | ||
|
|
b435ce4361 | ||
|
|
477ba697e3 | ||
|
|
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 | ||
|
|
b96794f015 | ||
|
|
02d5093871 | ||
|
|
7e08580a93 | ||
|
|
bfcc52b4f5 | ||
|
|
8c251e757c | ||
|
|
0b7ca400df | ||
|
|
40484b3395 | ||
|
|
d23593114c | ||
|
|
b1363526cf | ||
|
|
83d1431d4e | ||
|
|
977e6ceaea | ||
|
|
40a6b38247 | ||
|
|
deece8f852 | ||
|
|
034db721dd | ||
|
|
313a0b9922 | ||
|
|
31da345c07 | ||
|
|
46dfb14111 | ||
|
|
e06b60f75f | ||
|
|
a48f2d3077 | ||
|
|
657964c39f | ||
|
|
d2fe508a6d | ||
|
|
16da74769e | ||
|
|
e13c3dc834 | ||
|
|
1331773b9a | ||
|
|
2681b03bd6 | ||
|
|
a7a90f3ffb | ||
|
|
4a0b22ec25 | ||
|
|
762a5ed7f7 | ||
|
|
ceb85f25b5 | ||
|
|
9fddc16f0d | ||
|
|
36cbf13b5e | ||
|
|
38dd4a088a | ||
|
|
2f3c1c964a | ||
|
|
5914b031b1 | ||
|
|
102499482f | ||
|
|
9401a335a3 | ||
|
|
3ee8a9398b | ||
|
|
25da1b1495 | ||
|
|
9509fd0822 | ||
|
|
20ec60c2d9 | ||
|
|
4cccb3aa86 | ||
|
|
b724a86493 | ||
|
|
2a03009e3f | ||
|
|
da21771e2e | ||
|
|
bb36f8049c | ||
|
|
276e2ce25d | ||
|
|
ccd5275aa4 | ||
|
|
a0c73122a6 | ||
|
|
50e244824e | ||
|
|
025009006c | ||
|
|
6f530319d8 | ||
|
|
1bcdd0c019 | ||
|
|
f049ecb9ce | ||
|
|
25694c08ea | ||
|
|
0963041ab4 | ||
|
|
1a8ddd7325 | ||
|
|
abc579a511 | ||
|
|
4d1d53ec73 | ||
|
|
6d4d5dfcd6 | ||
|
|
7acc225a21 | ||
|
|
76ba03be44 | ||
|
|
6441b26a49 | ||
|
|
cacb7b37ed | ||
|
|
e2cd31e32e | ||
|
|
be555cf0a3 | ||
|
|
0447ab23b5 | ||
|
|
50ec5a5c18 | ||
|
|
5aea817801 | ||
|
|
580da893f6 | ||
|
|
426c10420b | ||
|
|
dc5bc53050 | ||
|
|
d75af13705 | ||
|
|
0006c77502 | ||
|
|
ec1bd1999f | ||
|
|
4d40c93072 | ||
|
|
3e93618b99 | ||
|
|
42c2ea9ef7 | ||
|
|
f37565646f | ||
|
|
0f2e2bde5c | ||
|
|
cdf488f595 | ||
|
|
7243f38558 | ||
|
|
920ba7cd15 | ||
|
|
ecfa6e1681 | ||
|
|
adcdf4a70d | ||
|
|
b5383aad77 | ||
|
|
9d89e2d1d6 | ||
|
|
bee82de8ba | ||
|
|
c05e445696 | ||
|
|
a952c0706c | ||
|
|
97b7459e09 | ||
|
|
fcd8d74cda | ||
|
|
b5d34388b7 | ||
|
|
1f46830a1f | ||
|
|
b60777a215 | ||
|
|
f3deecb42d | ||
|
|
c8ba2d2a05 | ||
|
|
d7d901a925 | ||
|
|
cb9d39fcc1 | ||
|
|
93d793c5fe | ||
|
|
9875827ada | ||
|
|
b4c390c362 | ||
|
|
43ae5e70b0 | ||
|
|
4c96123771 |
@@ -92,8 +92,7 @@ runs:
|
||||
Please do the following:
|
||||
|
||||
- [ ] Approve running the full set of PR checks.
|
||||
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is
|
||||
selected rather than "Squash and merge" or "Rebase and merge".
|
||||
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is selected rather than "Squash and merge" or "Rebase and merge".
|
||||
EOF
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
16
.github/instructions/release-merge.instructions.md
vendored
Normal file
16
.github/instructions/release-merge.instructions.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
applyTo: "CHANGELOG.md,src/defaults.json,lib/defaults.json,src/api-compatibility.json"
|
||||
---
|
||||
|
||||
# Merging release, mergeback, and backport PRs
|
||||
|
||||
The release process creates a cascade of PRs (`main` → `releases/vN`, then
|
||||
`releases/vN` → `main` mergeback, then `releases/vN` → `releases/v(N-1)`
|
||||
backport). These PRs reliably touch `CHANGELOG.md`, `src/defaults.json` /
|
||||
`lib/defaults.json` (bundle/CLI version bump), and `src/api-compatibility.json`.
|
||||
|
||||
Such PRs **must be merged with a merge commit**. Never squash or rebase, as
|
||||
that breaks the branch linkage the release automation relies on.
|
||||
|
||||
When arming auto-merge on these PRs, use `--merge` (e.g. `gh pr merge --merge`),
|
||||
not `--squash` or `--rebase`.
|
||||
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- os: macos-latest
|
||||
- os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
- os: windows-latest
|
||||
version: nightly-latest
|
||||
|
||||
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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.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
|
||||
|
||||
1
.github/workflows/__export-file-baseline-information.yml
generated
vendored
1
.github/workflows/__export-file-baseline-information.yml
generated
vendored
@@ -124,4 +124,5 @@ jobs:
|
||||
env:
|
||||
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
|
||||
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
|
||||
106
.github/workflows/__linux-arm64.yml
generated
vendored
Normal file
106
.github/workflows/__linux-arm64.yml
generated
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
# 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 - Linux Arm64
|
||||
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:
|
||||
dotnet-version:
|
||||
type: string
|
||||
description: The version of .NET to install
|
||||
required: false
|
||||
default: 9.x
|
||||
go-version:
|
||||
type: string
|
||||
description: The version of Go to install
|
||||
required: false
|
||||
default: '>=1.21.0'
|
||||
workflow_call:
|
||||
inputs:
|
||||
dotnet-version:
|
||||
type: string
|
||||
description: The version of .NET to install
|
||||
required: false
|
||||
default: 9.x
|
||||
go-version:
|
||||
type: string
|
||||
description: The version of Go to install
|
||||
required: false
|
||||
default: '>=1.21.0'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
|
||||
jobs:
|
||||
linux-arm64:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04-arm
|
||||
version: nightly-latest
|
||||
name: Linux Arm64
|
||||
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: Install .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- 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
|
||||
with:
|
||||
languages: ${{ env.LANGUAGES }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Assert databases exist
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/codeql_databases"
|
||||
for lang in ${LANGUAGES//,/ }; do
|
||||
if [[ ! -d "$lang" ]]; then
|
||||
echo "Did not find a database for $lang"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found database for $lang"
|
||||
done
|
||||
env:
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
@@ -191,5 +191,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
|
||||
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
|
||||
|
||||
6
.github/workflows/__swift-custom-build.yml
generated
vendored
6
.github/workflows/__swift-custom-build.yml
generated
vendored
@@ -54,11 +54,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
- os: macos-latest-xlarge
|
||||
version: linked
|
||||
- os: macos-latest
|
||||
- os: macos-latest-xlarge
|
||||
version: default
|
||||
- os: macos-latest
|
||||
- os: macos-latest-xlarge
|
||||
version: nightly-latest
|
||||
name: Swift analysis using a custom build command
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
|
||||
@@ -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
|
||||
|
||||
33
.github/workflows/pr-checks.yml
vendored
33
.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: |
|
||||
@@ -67,7 +73,12 @@ jobs:
|
||||
|
||||
- name: Upload sarif
|
||||
uses: ./upload-sarif
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
|
||||
# The merge queue deletes its `gh-readonly-queue` ref as soon as the queue entry resolves,
|
||||
# so uploading against it races with that deletion. Both the `merge_group` run and the
|
||||
# paired `push` run that the queue branch creates use that ref, so gate on the ref itself
|
||||
# rather than the event. The same results are uploaded by the `pull_request` run and again
|
||||
# by the `push` run on `main`.
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/')
|
||||
with:
|
||||
sarif_file: eslint.sarif
|
||||
category: eslint
|
||||
@@ -90,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
|
||||
@@ -109,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: |
|
||||
@@ -158,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 }}
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -28,7 +28,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: "Prepare release"
|
||||
name: "Release info"
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'github/codeql-action'
|
||||
|
||||
|
||||
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
|
||||
|
||||
12
.github/workflows/rollback-release.yml
vendored
12
.github/workflows/rollback-release.yml
vendored
@@ -10,8 +10,7 @@ on:
|
||||
required: true
|
||||
# Only for dry-runs of changes to the workflow.
|
||||
push:
|
||||
# Don't run dry-run on release branches, to avoid an issue where the
|
||||
# "new" tag determined by the "Prepare release" job already exists.
|
||||
# Don't run dry-run on release branches, since that's unnecessary.
|
||||
branches-ignore:
|
||||
- releases/v*
|
||||
paths:
|
||||
@@ -24,7 +23,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: "Prepare release"
|
||||
name: "Prepare"
|
||||
if: github.repository == 'github/codeql-action'
|
||||
|
||||
permissions:
|
||||
@@ -107,8 +106,10 @@ jobs:
|
||||
# We usually expect to checkout `inputs.rollback-tag` (required for `workflow_dispatch`),
|
||||
# but use `v0.0.0` for testing.
|
||||
ROLLBACK_TAG: ${{ inputs.rollback-tag || 'v0.0.0' }}
|
||||
RELEASE_TAG: ${{ needs.prepare.outputs.version }}
|
||||
MAJOR_VERSION_TAG: ${{ needs.prepare.outputs.major_version }}
|
||||
# Use `needs.prepare.outputs.version` for actual runs and `v0.0.1` for testing.
|
||||
RELEASE_TAG: ${{ case(github.event_name == 'workflow_dispatch', needs.prepare.outputs.version, 'v0.0.1') }}
|
||||
# Use `needs.prepare.outputs.major_version` for actual runs and `v0` for testing.
|
||||
MAJOR_VERSION_TAG: ${{ case(github.event_name == 'workflow_dispatch', needs.prepare.outputs.major_version, 'v0') }}
|
||||
run: |
|
||||
git checkout "refs/tags/${ROLLBACK_TAG}"
|
||||
git tag --annotate "${RELEASE_TAG}" --message "${RELEASE_TAG}"
|
||||
@@ -184,4 +185,3 @@ jobs:
|
||||
# Setting this to `true` for non-workflow_dispatch events will
|
||||
# still push the `branch`, but won't create a corresponding PR
|
||||
dry-run: "${{ github.event_name != 'workflow_dispatch' }}"
|
||||
|
||||
|
||||
5
.github/workflows/update-release-branch.yml
vendored
5
.github/workflows/update-release-branch.yml
vendored
@@ -16,15 +16,15 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
|
||||
prepare:
|
||||
name: "Prepare release"
|
||||
name: "Prepare"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
uses: ./.github/workflows/prepare-release.yml
|
||||
|
||||
update:
|
||||
name: "Update release branch"
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
@@ -77,6 +77,7 @@ jobs:
|
||||
--conductor ${GITHUB_ACTOR}
|
||||
|
||||
backport:
|
||||
name: "Create backport"
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
environment: Automation
|
||||
|
||||
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"
|
||||
},
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -4,7 +4,25 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
- _Upcoming breaking change_: CodeQL version 2.21.2 and earlier were discontinued on 24 September 2026 alongside GitHub Enterprise Server 3.17, and will be unsupported by the next minor release of the CodeQL Action. Added a deprecation warning for customers using these versions of CodeQL. [#4188](https://github.com/github/codeql-action/pull/4188)
|
||||
|
||||
## 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)
|
||||
- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072)
|
||||
- Update default CodeQL bundle version to [2.27.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.0). [#4129](https://github.com/github/codeql-action/pull/4129)
|
||||
|
||||
## 4.37.9 - 26 Aug 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.4](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.4). [#4106](https://github.com/github/codeql-action/pull/4106)
|
||||
|
||||
## 4.37.8 - 21 Aug 2026
|
||||
|
||||
|
||||
@@ -60,10 +60,13 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
This workflow goes through the pull requests that have been merged to `main` since the last release, creates a changelog, then opens a pull request to merge the changes since the last release into the `releases/v3` release branch.
|
||||
|
||||
You can start a release by triggering this workflow via [workflow dispatch](https://github.com/github/codeql-action/actions/workflows/update-release-branch.yml).
|
||||
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it.
|
||||
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it **with a merge commit** (`gh pr merge --merge`).
|
||||
1. When the "Merge main into releases/v3" pull request is merged into the `releases/v3` branch, a mergeback pull request to `main` will be automatically created. This mergeback pull request incorporates the changelog updates into `main`, tags the release using the merge commit of the "Merge main into releases/v3" pull request, and bumps the patch version of the CodeQL Action.
|
||||
1. If a backport to an older major version is required, a pull request targeting that version's branch will also be automatically created.
|
||||
1. Approve the mergeback and backport pull request (if applicable) and automerge them.
|
||||
1. Approve the mergeback and backport pull request (if applicable) and automerge them **with a merge commit** (`gh pr merge --merge`).
|
||||
|
||||
> [!NOTE]
|
||||
> The release, mergeback, and backport pull requests must always be merged with a merge commit — **never squash or rebase**. The mergeback tags the release using the merge commit of the "Merge main into releases/v3" pull request, so squashing or rebasing breaks tagging and the branch linkage the release automation relies on.
|
||||
|
||||
Once the mergeback and backport pull request have been merged, the release is complete.
|
||||
|
||||
|
||||
@@ -72,12 +72,11 @@ We typically release new minor versions of the CodeQL Action and Bundle when a n
|
||||
|
||||
| Minimum CodeQL Action | Minimum CodeQL Bundle Version | GitHub Environment | Notes |
|
||||
|-----------------------|-------------------------------|--------------------|-------|
|
||||
| `v4.36.2` | `2.25.6` | Enterprise Server 3.22 | |
|
||||
| `v4.33.0` | `2.24.3` | Enterprise Server 3.21 | |
|
||||
| `v4.31.10` | `2.23.9` | Enterprise Server 3.20 | |
|
||||
| `v3.29.11` | `2.22.4` | Enterprise Server 3.19 | |
|
||||
| `v3.28.21` | `2.21.3` | Enterprise Server 3.18 | |
|
||||
| `v3.28.12` | `2.20.7` | Enterprise Server 3.17 | |
|
||||
| `v3.28.6` | `2.20.3` | Enterprise Server 3.16 | |
|
||||
|
||||
See the full list of GHES release and deprecation dates at [GitHub Enterprise Server releases](https://docs.github.com/en/enterprise-server/admin/all-releases#releases-of-github-enterprise-server).
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -164,6 +164,13 @@ inputs:
|
||||
[Internal] The ID of the check run, as provided by the Actions runtime environment. Do not set this value manually.
|
||||
default: ${{ job.check_run_id }}
|
||||
required: false
|
||||
job-status:
|
||||
description: >-
|
||||
[Internal] The status of the job, as provided by the Actions runtime environment. This is how the
|
||||
post step learns whether the job as a whole succeeded, failed, or was cancelled. Do not set this
|
||||
value manually.
|
||||
default: ${{ job.status }}
|
||||
required: false
|
||||
outputs:
|
||||
codeql-path:
|
||||
description: The path of the CodeQL binary used for analysis
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
48973
lib/entry-points.js
generated
48973
lib/entry-points.js
generated
File diff suppressed because one or more lines are too long
1450
package-lock.json
generated
1450
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.9",
|
||||
"version": "4.38.3",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -30,28 +30,28 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@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",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.2.3",
|
||||
"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.1",
|
||||
"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": "^16.0.0",
|
||||
"@octokit/types": "^18.0.0",
|
||||
"@types/archiver": "^8.0.0",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
@@ -66,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": "^62.9.0",
|
||||
"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.67.0"
|
||||
"typescript-eslint": "^8.70.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
|
||||
/** The type of the Octokit client. */
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
|
||||
|
||||
/** Constructs an `ApiClient` using `token` for authentication. */
|
||||
export function getApiClient(token: string): ApiClient {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
121
pr-checks/changelog/validate.mts
Normal file
121
pr-checks/changelog/validate.mts
Normal file
@@ -0,0 +1,121 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { matter } from "lite-matter";
|
||||
import type { List, ListItem } from "mdast";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
|
||||
// Regex for filename: YYYY-MM-DD-id.md
|
||||
const VALID_CHANGE_NOTE_FILENAME_PATTERN =
|
||||
/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/;
|
||||
|
||||
export const VALID_CHANGE_NOTE_CATEGORIES = {
|
||||
breaking: "Breaking Changes",
|
||||
feature: "New Features",
|
||||
improvement: "Improvements",
|
||||
securityFix: "Security Fixes",
|
||||
fix: "Bug Fixes",
|
||||
unship: "Removed Features",
|
||||
deprecation: "Deprecations",
|
||||
knownIssue: "Known Issues",
|
||||
misc: "Miscellaneous",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the given Markdown string meets the criteria for a change-note, which is:
|
||||
* - A single unordered list
|
||||
* - Each list item must start with a hyphen (-)
|
||||
* - No other Markdown elements are allowed
|
||||
* @param content The Markdown string to validate
|
||||
* @returns True if the string is a valid change-note, false otherwise
|
||||
*/
|
||||
export function isValidChangenoteContent(content: string): boolean {
|
||||
const ast = fromMarkdown(content);
|
||||
const lines = content.split("\n");
|
||||
|
||||
function listHasHyphenBullets(node: List | ListItem): boolean {
|
||||
if (node.type === "list") {
|
||||
return node.children.every(listHasHyphenBullets);
|
||||
}
|
||||
|
||||
const line = lines[node.position!.start.line - 1].trim();
|
||||
return (
|
||||
line.startsWith("-") &&
|
||||
node.children.every(
|
||||
(child) => child.type !== "list" || listHasHyphenBullets(child),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
ast.children.length === 1 &&
|
||||
ast.children[0].type === "list" &&
|
||||
ast.children[0].ordered === false &&
|
||||
listHasHyphenBullets(ast.children[0])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given filename meets the criteria for a change-note filename.
|
||||
* @param filename The name of the change-note file to validate.
|
||||
* @returns True if the filename is valid, false otherwise.
|
||||
*/
|
||||
export function isValidChangenoteFilename(filename: string): boolean {
|
||||
return filename.match(VALID_CHANGE_NOTE_FILENAME_PATTERN) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given frontmatter has a valid change-note category.
|
||||
* @param frontmatter The frontmatter object to validate.
|
||||
* @returns True if the frontmatter has a valid category, false otherwise.
|
||||
*/
|
||||
export function hasValidChangenoteCategory(
|
||||
frontmatter: Record<string, unknown>,
|
||||
): boolean {
|
||||
const category = frontmatter["category"];
|
||||
return (
|
||||
typeof category === "string" &&
|
||||
Object.hasOwn(VALID_CHANGE_NOTE_CATEGORIES, category)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given change-note file meets all of the criteria for a change-note.
|
||||
* @param filename The name of the change-note file to validate.
|
||||
* @returns True if the file is a valid change-note, false otherwise.
|
||||
*/
|
||||
export function isValidChangenoteFile(filename: string): boolean {
|
||||
let isValid: boolean = true;
|
||||
|
||||
let fileData: string | undefined;
|
||||
try {
|
||||
fileData = fs.readFileSync(filename, "utf8");
|
||||
} catch (error) {
|
||||
console.error(`${filename}: failed to read file`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data: frontmatter, content } = matter(fileData);
|
||||
|
||||
if (!isValidChangenoteFilename(path.basename(filename))) {
|
||||
isValid = false;
|
||||
console.error(
|
||||
`${filename}: invalid filename; must match pattern YYYY-MM-DD-id.md`,
|
||||
);
|
||||
}
|
||||
if (!hasValidChangenoteCategory(frontmatter)) {
|
||||
isValid = false;
|
||||
const categories = Object.keys(VALID_CHANGE_NOTE_CATEGORIES).join(", ");
|
||||
console.error(
|
||||
`${filename}: invalid category; must be one of: ${categories}`,
|
||||
);
|
||||
}
|
||||
if (!isValidChangenoteContent(content)) {
|
||||
isValid = false;
|
||||
console.error(
|
||||
`${filename}: invalid Markdown; content must be a single unordered list with hyphen bullets and no other Markdown elements`,
|
||||
);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
186
pr-checks/changelog/validate.test.mts
Normal file
186
pr-checks/changelog/validate.test.mts
Normal file
@@ -0,0 +1,186 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { withTmpFile } from "../../src/util";
|
||||
|
||||
import {
|
||||
hasValidChangenoteCategory,
|
||||
isValidChangenoteContent,
|
||||
isValidChangenoteFile,
|
||||
isValidChangenoteFilename,
|
||||
VALID_CHANGE_NOTE_CATEGORIES,
|
||||
} from "./validate.mjs";
|
||||
|
||||
await describe("isValidChangenoteContent", async () => {
|
||||
await it("recognizes an unordered Markdown list", () => {
|
||||
const inputs = [
|
||||
"- One changenote entry",
|
||||
"- First item\n- Second item",
|
||||
"\n\n\n\n- Fixed a bug\n- Added a feature",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize non-Markdown text", () => {
|
||||
const inputs = [
|
||||
"This is not a list.",
|
||||
'["this", "is", "JSON"]',
|
||||
"---",
|
||||
"***",
|
||||
"___",
|
||||
"paragraph",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize ordered Markdown lists", () => {
|
||||
const inputs = [
|
||||
"1. First item\n2. Second item",
|
||||
"\n\n\n1. First item\n1. Second item",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
"- Fixed a bug\n* Added feature",
|
||||
"- Fixed a bug\n+ Added feature",
|
||||
"- Fixed a bug\n * Added feature\n + Updated docs",
|
||||
"\n\n\n* Fixed a bug",
|
||||
"\n\n\n+ Fixed a bug",
|
||||
"---\n* Fixed a bug\n* Added feature",
|
||||
] as const;
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
"# Header\n- Fixed a bug",
|
||||
"- Fixed a bug\n## Subheader",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidChangenoteFilename", async () => {
|
||||
await it("accepts valid filenames", () => {
|
||||
const inputs = [
|
||||
"2023-01-01-fix-bug.md",
|
||||
"2023-12-31-add-feature.md",
|
||||
"2023-06-15-update-docs.md",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteFilename(input), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("rejects invalid filenames", () => {
|
||||
const inputs = [
|
||||
"missing-date-from-filename.md",
|
||||
"2021-01-01.md",
|
||||
"2026-12-19-wrong-file-name-extension.txt",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteFilename(input), false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await describe("hasValidChangenoteCategory", 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", () => {
|
||||
const inputs = [
|
||||
"",
|
||||
"invalid-category",
|
||||
"bug-fix",
|
||||
"new-feature",
|
||||
"security-patch",
|
||||
"miscellaneous",
|
||||
"documentation",
|
||||
];
|
||||
|
||||
for (const category of inputs) {
|
||||
const frontmatter = { category };
|
||||
assert.equal(hasValidChangenoteCategory(frontmatter), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("reject missing category", () => {
|
||||
assert.equal(hasValidChangenoteCategory({}), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: null }), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: undefined }), false);
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidChangenoteFile", async () => {
|
||||
await it("accepts a valid change-note file", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n- Fixed a bug\n",
|
||||
(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",
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects missing frontmatter", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"- Fixed a bug\n",
|
||||
(filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects invalid Markdown", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n* Fixed a bug\n",
|
||||
(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;
|
||||
}
|
||||
@@ -2,7 +2,8 @@ name: "All-platform bundle"
|
||||
description: "Tests using an all-platform CodeQL Bundle"
|
||||
operatingSystems:
|
||||
- ubuntu
|
||||
- macos
|
||||
- os: macos
|
||||
runner-image: macos-latest-xlarge
|
||||
- windows
|
||||
versions:
|
||||
- nightly-latest
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -11,6 +11,10 @@ installDotNet: true
|
||||
env:
|
||||
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
|
||||
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
|
||||
# To balance speed and coverage, we analyze only a single language (JavaScript), but use the
|
||||
# combined bundle so we can test that baseline information is reported for each language in the
|
||||
# multi-language source directory.
|
||||
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
id: init
|
||||
|
||||
35
pr-checks/checks/linux-arm64.yml
Normal file
35
pr-checks/checks/linux-arm64.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: "Linux Arm64"
|
||||
description: "An end-to-end integration test running on a Linux Arm64 runner, checking that the native linux-arm64 CodeQL bundle is downloaded and can analyze interpreted and compiled code"
|
||||
operatingSystems:
|
||||
- os: ubuntu
|
||||
runner-image: ubuntu-24.04-arm
|
||||
# The native linux-arm64 CodeQL bundle is only available in recent CLI releases, so we restrict this
|
||||
# check to `nightly-latest`, which is guaranteed to ship it. Older stable versions do not have an
|
||||
# arm64 asset, and `prepare-test` would resolve an x64 bundle URL for them on this runner.
|
||||
versions:
|
||||
- nightly-latest
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
# The set of languages CodeQL supports on this platform, excluding Swift (macOS only).
|
||||
env:
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
with:
|
||||
languages: ${{ env.LANGUAGES }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Assert databases exist
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/codeql_databases"
|
||||
for lang in ${LANGUAGES//,/ }; do
|
||||
if [[ ! -d "$lang" ]]; then
|
||||
echo "Did not find a database for $lang"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found database for $lang"
|
||||
done
|
||||
@@ -15,6 +15,7 @@ operatingSystems:
|
||||
- stable-v2.21.4
|
||||
- stable-v2.22.4
|
||||
env:
|
||||
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
|
||||
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
|
||||
|
||||
@@ -5,7 +5,8 @@ versions:
|
||||
- default
|
||||
- nightly-latest
|
||||
operatingSystems:
|
||||
- macos
|
||||
- os: macos
|
||||
runner-image: macos-latest-xlarge
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
env:
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ contains:
|
||||
- "test-setup-python-scripts"
|
||||
- "update"
|
||||
- "Update"
|
||||
# Matrix-ed job; the name starts with this
|
||||
- "Create backport"
|
||||
is:
|
||||
- "Agent"
|
||||
- "check-expected-release-files"
|
||||
@@ -15,4 +17,6 @@ is:
|
||||
- "Label PR with size"
|
||||
- "Post repo size comment"
|
||||
- "Prepare"
|
||||
- "Release info"
|
||||
- "Upload results"
|
||||
- "Update release branch"
|
||||
|
||||
@@ -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/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@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",
|
||||
"b6effb05e454b25005698d916606bdc6ffcbf961",
|
||||
"v5.7.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"]
|
||||
}
|
||||
|
||||
@@ -21,19 +21,16 @@ inputs:
|
||||
required: false
|
||||
languages:
|
||||
description: >-
|
||||
A comma-separated list of CodeQL languages that will be analyzed in subsequent
|
||||
`github/codeql-action/init` and `github/codeql-action/analyze` invocations. If specified, the
|
||||
Action may use this list to select a CodeQL CLI version that is best suited to analyzing those
|
||||
languages, for example by preferring a version that has a cached overlay-base database for the
|
||||
specified languages. This input is not remembered and must also be passed to
|
||||
`github/codeql-action/init`.
|
||||
A comma-separated list of CodeQL languages that the installed CodeQL CLI will be used to
|
||||
analyze. If specified, the Action may use this list to select a CodeQL CLI version that is
|
||||
best suited to analyzing those languages, for example by preferring a version that has a
|
||||
cached overlay-base database for the specified languages.
|
||||
required: false
|
||||
analysis-kinds:
|
||||
description: >-
|
||||
[Internal] A comma-separated list of analysis kinds that subsequent
|
||||
`github/codeql-action/init` invocations will enable. If specified, the Action may use this
|
||||
list to select a CodeQL CLI version that is best suited to those analysis kinds. This input is
|
||||
not remembered and must also be passed to `github/codeql-action/init`.
|
||||
[Internal] A comma-separated list of analysis kinds that the installed CodeQL CLI will be used
|
||||
for. If specified, the Action may use this list to select a CodeQL CLI version that is best
|
||||
suited to those analysis kinds.
|
||||
|
||||
Available options are the same as for the `analysis-kinds` input on the `init` Action.
|
||||
default: 'code-scanning'
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
@@ -283,6 +284,19 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the job is running on a runner that GitHub hosts, and whose toolcache is therefore thrown
|
||||
* away once the job has finished.
|
||||
*
|
||||
* Unlike `looksLikeHostedRunner`, this is based on what the service reports for the job rather than
|
||||
* on how the runner's filesystem happens to be laid out, so it does not match self-hosted runners
|
||||
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
|
||||
* `/opt/hostedtoolcache`.
|
||||
*/
|
||||
export function isGitHubHostedRunner(env: ReadOnlyEnv = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
|
||||
}
|
||||
|
||||
/** Determines whether the workflow trigger is `dynamic`. */
|
||||
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
||||
return getWorkflowEventName(env) === "dynamic";
|
||||
@@ -399,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
|
||||
|
||||
@@ -7,12 +7,12 @@ import * as sinon from "sinon";
|
||||
import { CodeQuality, CodeScanning, RiskAssessment } from "./analyses";
|
||||
import {
|
||||
runQueries,
|
||||
defaultSuites,
|
||||
resolveQuerySuiteAlias,
|
||||
addSarifExtension,
|
||||
diffRangeExtensionPackContents,
|
||||
} from "./analyze";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { defaultSuites } from "./config/db-config";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
|
||||
@@ -5,10 +5,11 @@ 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";
|
||||
import { defaultSuites } from "./config/db-config";
|
||||
import * as configUtils from "./config-utils";
|
||||
import {
|
||||
getCsharpTempDependencyDir,
|
||||
@@ -233,6 +234,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 +247,6 @@ export async function setupDiffInformedQueryRun(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const checkoutPath = getRequiredInput("checkout_path");
|
||||
const packDir = writeDiffRangeDataExtensionPack(
|
||||
logger,
|
||||
diffRanges,
|
||||
@@ -357,15 +358,6 @@ dataExtensions:
|
||||
return diffRangeDir;
|
||||
}
|
||||
|
||||
// A set of default query suite names that are understood by the CLI.
|
||||
export const defaultSuites: Set<string> = new Set([
|
||||
"security-experimental",
|
||||
"security-extended",
|
||||
"security-and-quality",
|
||||
"code-quality",
|
||||
"code-scanning",
|
||||
]);
|
||||
|
||||
/**
|
||||
* If `maybeSuite` is the name of a default query suite, it is resolved into the corresponding
|
||||
* query suite name for the given `language`. Otherwise, `maybeSuite` is returned as is.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import * as retry from "@octokit/plugin-retry";
|
||||
import { RequestRequestOptions } from "@octokit/types";
|
||||
import {
|
||||
@@ -128,7 +125,7 @@ export function makeProxyRequestOptions(
|
||||
}
|
||||
|
||||
/** The type of GitHub API client we use. */
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
|
||||
|
||||
/** Options for `createApiClientWithDetails`. */
|
||||
interface CreateApiClientOptions {
|
||||
@@ -222,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 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,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;
|
||||
@@ -417,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 (
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion":"3.23","minimumVersion":"3.17"}
|
||||
{"maximumVersion":"3.23","minimumVersion":"3.18"}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as core from "@actions/core";
|
||||
import { getOptionalInput, isDefaultSetup } from "./actions-util";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import { isHostedRunner, tryGetFolderBytes } from "./util";
|
||||
import { looksLikeHostedRunner, tryGetFolderBytes } from "./util";
|
||||
|
||||
/**
|
||||
* Returns the total size of all the specified paths.
|
||||
@@ -109,7 +109,7 @@ export function getDependencyCachingEnabled(): CachingKind {
|
||||
if (dependencyCaching !== undefined) return getCachingKind(dependencyCaching);
|
||||
|
||||
// On self-hosted runners which may have dependencies installed centrally, disable caching by default
|
||||
if (!isHostedRunner()) return CachingKind.None;
|
||||
if (!looksLikeHostedRunner()) return CachingKind.None;
|
||||
|
||||
// Disable in advanced workflows by default.
|
||||
if (!isDefaultSetup()) return CachingKind.None;
|
||||
|
||||
@@ -128,7 +128,6 @@ test("CliError constructor with empty stderr", (t) => {
|
||||
|
||||
for (const [platform, arch] of [
|
||||
["weird_plat", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "arm64"],
|
||||
]) {
|
||||
test.serial(
|
||||
@@ -157,20 +156,34 @@ for (const [platform, arch] of [
|
||||
);
|
||||
}
|
||||
|
||||
test("wrapCliConfigurationError - supported platform", (t) => {
|
||||
const commandError = new CommandInvocationError(
|
||||
"codeql",
|
||||
["version"],
|
||||
1,
|
||||
"Some error",
|
||||
for (const [platform, arch] of [
|
||||
["linux", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "x64"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "arm64"],
|
||||
]) {
|
||||
test.serial(
|
||||
`wrapCliConfigurationError - ${platform}/${arch} supported`,
|
||||
(t) => {
|
||||
sinon.stub(process, "platform").value(platform);
|
||||
sinon.stub(process, "arch").value(arch);
|
||||
const commandError = new CommandInvocationError(
|
||||
"codeql",
|
||||
["version"],
|
||||
1,
|
||||
"Some error",
|
||||
);
|
||||
const cliError = new CliError(commandError);
|
||||
|
||||
const wrappedError = wrapCliConfigurationError(cliError);
|
||||
|
||||
// Should return the original error since the platform is supported, rather
|
||||
// than replacing it with the unsupported-platform ConfigurationError.
|
||||
t.is(wrappedError, cliError);
|
||||
},
|
||||
);
|
||||
const cliError = new CliError(commandError);
|
||||
|
||||
const wrappedError = wrapCliConfigurationError(cliError);
|
||||
|
||||
// Should return the original error since platform is supported
|
||||
t.is(wrappedError, cliError);
|
||||
});
|
||||
}
|
||||
|
||||
test("wrapCliConfigurationError - autobuild error", (t) => {
|
||||
const commandError = new CommandInvocationError(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ConfigurationError } from "./util";
|
||||
|
||||
const SUPPORTED_PLATFORMS = [
|
||||
["linux", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "x64"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "arm64"],
|
||||
|
||||
@@ -3,12 +3,11 @@ import path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { EnvVar } from "../environment";
|
||||
import { getRunnerLogger } from "../logging";
|
||||
import { getTestEnv, setupTests } from "../testing-utils";
|
||||
import { setupTests } from "../testing-utils";
|
||||
import * as util from "../util";
|
||||
|
||||
import * as outputCache from "./output-cache";
|
||||
import { getCachedCodeQlVersion } from "./output-cache";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
@@ -18,18 +17,18 @@ test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "codeql-action-command-cache.json");
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
cacheFilePath,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.deepEqual(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
{
|
||||
version: "2.20.0",
|
||||
},
|
||||
@@ -42,18 +41,17 @@ test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
cacheFilePath,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
@@ -64,11 +62,10 @@ test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
fs.writeFileSync(cacheFile, "not valid json", "utf8");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
fs.writeFileSync(cacheFilePath, "not valid json", "utf8");
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
@@ -79,9 +76,7 @@ test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
const testValues = [
|
||||
{ cmd: "/path/to/codeql" },
|
||||
{ entries: { version: { version: "2.20.0" } } },
|
||||
@@ -104,9 +99,9 @@ test.serial(
|
||||
].map((v) => JSON.stringify(v));
|
||||
|
||||
for (const value of testValues) {
|
||||
fs.writeFileSync(cacheFile, value, "utf8");
|
||||
fs.writeFileSync(cacheFilePath, value, "utf8");
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
value,
|
||||
);
|
||||
@@ -117,10 +112,10 @@ test.serial(
|
||||
|
||||
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
t.notThrows(() => {
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,9 +3,10 @@ import path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import * as json from "../json";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
import type { VersionInfo } from "./types";
|
||||
import { VersionInfo, versionInfoBaseSchema } from "./types";
|
||||
|
||||
/**
|
||||
* The keys of the command cache. Each key corresponds to a command whose output we cache.
|
||||
@@ -13,12 +14,19 @@ import type { VersionInfo } from "./types";
|
||||
export type CommandCacheKey = string;
|
||||
|
||||
/**
|
||||
* The type of the command cache that is persisted to disk.
|
||||
* The JSON schema of the command cache that is persisted to disk.
|
||||
*/
|
||||
export interface OutputCache {
|
||||
cmd: string;
|
||||
entries: Record<CommandCacheKey, unknown>;
|
||||
}
|
||||
const outputCacheSchema = {
|
||||
cmd: json.string,
|
||||
entries: json.object({}),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* The type that describes the command cache that is persisted to disk.
|
||||
*/
|
||||
export type OutputCache = json.FromSchema<typeof outputCacheSchema> & {
|
||||
entries: { version: VersionInfo };
|
||||
};
|
||||
|
||||
/**
|
||||
* The name of the temporary file that backs the on-disk cache of
|
||||
@@ -43,18 +51,18 @@ export function resetCachedCodeQlVersion(): void {
|
||||
* Returns the path to the temporary file that backs the
|
||||
* on-disk cache of CLI responses between workflow steps.
|
||||
*/
|
||||
function getCommandCacheFilePath(env: Env): string {
|
||||
export function getCommandCacheFilePath(env: Env): string {
|
||||
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the CodeQL CLI version both in-memory and on disk.
|
||||
* @param env The environment variables to use.
|
||||
* @param cacheFilePath The path to the cache file.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
* @param version The version information to cache.
|
||||
*/
|
||||
export function cacheCodeQlVersion(
|
||||
env: Env,
|
||||
cacheFilePath: string,
|
||||
cmd: string,
|
||||
version: VersionInfo,
|
||||
): void {
|
||||
@@ -70,22 +78,18 @@ export function cacheCodeQlVersion(
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
fs.writeFileSync(
|
||||
getCommandCacheFilePath(env),
|
||||
JSON.stringify(outputCache),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(cacheFilePath, JSON.stringify(outputCache), "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached CodeQL CLI version, if any.
|
||||
* @param logger The logger to use for logging messages.
|
||||
* @param env The environment variables to use.
|
||||
* @param cacheFilePath The path to the cache file.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
*/
|
||||
export function getCachedCodeQlVersion(
|
||||
logger: Logger,
|
||||
env: Env,
|
||||
cacheFilePath: string,
|
||||
cmd?: string,
|
||||
): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
@@ -96,11 +100,9 @@ export function getCachedCodeQlVersion(
|
||||
// invokes `codeql version` instead.
|
||||
let serialized: string;
|
||||
try {
|
||||
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
|
||||
serialized = fs.readFileSync(cacheFilePath, "utf8");
|
||||
} catch (e) {
|
||||
logger.debug(
|
||||
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`,
|
||||
);
|
||||
logger.debug(`Cannot read CLI-cache file ${cacheFilePath}: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
@@ -127,17 +129,7 @@ export function getCachedCodeQlVersion(
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const candidate = x as Partial<VersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.version === "string" &&
|
||||
(candidate.features === undefined ||
|
||||
(typeof candidate.features === "object" &&
|
||||
candidate.features !== null)) &&
|
||||
(candidate.overlayVersion === undefined ||
|
||||
typeof candidate.overlayVersion === "number")
|
||||
);
|
||||
return json.isObject(x) && json.validateSchema(versionInfoBaseSchema, x);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,12 +137,10 @@ function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isOutputCache(x: unknown): x is OutputCache {
|
||||
const candidate = x as Partial<OutputCache> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
candidate.entries !== undefined &&
|
||||
isVersionInfo(candidate.entries.version)
|
||||
json.isObject(x) &&
|
||||
json.validateSchema(outputCacheSchema, x) &&
|
||||
json.isObject<{ version: unknown }>(x.entries) &&
|
||||
isVersionInfo(x.entries.version)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
import * as json from "../json";
|
||||
|
||||
/**
|
||||
* The JSON schema of the expected output of the `codeql version` command.
|
||||
*/
|
||||
export const versionInfoBaseSchema = {
|
||||
version: json.string,
|
||||
features: json.optional(json.object({})),
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
@@ -9,5 +14,17 @@ export interface VersionInfo {
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
overlayVersion: json.optional(json.number),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* The base type that describes the expected output of the `codeql version` command.
|
||||
*/
|
||||
export type VersionInfoBase = json.FromSchema<typeof versionInfoBaseSchema>;
|
||||
|
||||
/**
|
||||
* The full type that describes the expected output of the `codeql version` command.
|
||||
*/
|
||||
export type VersionInfo = Omit<VersionInfoBase, "features"> & {
|
||||
features?: { [name: string]: boolean };
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -99,6 +99,7 @@ async function installIntoToolcache({
|
||||
? { enabledVersions: [{ cliVersion, tagName }] }
|
||||
: SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
createFeatures([]),
|
||||
getRunnerLogger(true),
|
||||
@@ -172,6 +173,7 @@ test.serial(
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -207,6 +209,7 @@ test.serial(
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -246,6 +249,7 @@ test.serial(
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -259,6 +263,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",
|
||||
@@ -296,6 +359,7 @@ for (const {
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -338,6 +402,7 @@ for (const toolcacheVersion of [
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -382,6 +447,7 @@ test.serial(
|
||||
],
|
||||
},
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -428,6 +494,7 @@ test.serial(
|
||||
],
|
||||
},
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -467,6 +534,7 @@ test.serial(
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
@@ -508,6 +576,7 @@ test.serial(
|
||||
util.GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
undefined, // otherLanguagePacksReason
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
features,
|
||||
getRunnerLogger(true),
|
||||
|
||||
@@ -258,17 +258,17 @@ const CODEQL_MINIMUM_VERSION = "2.19.4";
|
||||
/**
|
||||
* This version will shortly become the oldest version of CodeQL that the Action will run with.
|
||||
*/
|
||||
const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7";
|
||||
const CODEQL_NEXT_MINIMUM_VERSION = "2.21.3";
|
||||
|
||||
/**
|
||||
* This is the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16";
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.17";
|
||||
|
||||
/**
|
||||
* This is the deprecation date for the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-09-24";
|
||||
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
@@ -302,6 +302,9 @@ export function isDiskConfigurationError(e: unknown): boolean {
|
||||
* @param variant
|
||||
* @param defaultCliVersion
|
||||
* @param rawLanguages Raw set of languages.
|
||||
* @param otherLanguagePacksReason Why the CodeQL CLI may need packs for languages other than
|
||||
* `rawLanguages`, or `undefined` if it won't. If defined, the combined bundle is used. See
|
||||
* `PerLanguageBundleOptions.otherLanguagePacksReason`.
|
||||
* @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version.
|
||||
* @param features Information about the features that are enabled.
|
||||
* @param logger
|
||||
@@ -316,6 +319,7 @@ export async function setupCodeQL(
|
||||
variant: util.GitHubVariant,
|
||||
defaultCliVersion: CodeQLDefaultVersionInfo,
|
||||
rawLanguages: string[] | undefined,
|
||||
otherLanguagePacksReason: string | undefined,
|
||||
useOverlayAwareDefaultCliVersion: boolean,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
@@ -339,6 +343,7 @@ export async function setupCodeQL(
|
||||
variant,
|
||||
defaultCliVersion,
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
useOverlayAwareDefaultCliVersion,
|
||||
features,
|
||||
logger,
|
||||
@@ -510,7 +515,12 @@ async function getCodeQLForCmd(
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = outputCache.getCachedCodeQlVersion(logger, getEnv(), cmd);
|
||||
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
|
||||
let result = outputCache.getCachedCodeQlVersion(
|
||||
logger,
|
||||
cacheFilePath,
|
||||
cmd,
|
||||
);
|
||||
if (result === undefined) {
|
||||
result = await runCliJson<VersionInfo>(
|
||||
cmd,
|
||||
@@ -519,7 +529,7 @@ async function getCodeQLForCmd(
|
||||
noStreamStdout: true,
|
||||
},
|
||||
);
|
||||
outputCache.cacheCodeQlVersion(getEnv(), cmd, result);
|
||||
outputCache.cacheCodeQlVersion(cacheFilePath, cmd, result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -932,6 +942,7 @@ async function getCodeQLForCmd(
|
||||
"--format=json",
|
||||
`--language=${language}`,
|
||||
"--extractor-include-aliases",
|
||||
"-J-XX:-UsePerfData",
|
||||
...getExtraOptionsFromEnv(["resolve", "extractor"]),
|
||||
],
|
||||
{
|
||||
@@ -946,7 +957,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 = [
|
||||
|
||||
@@ -473,6 +473,12 @@ const simpleConfigFileContents = `
|
||||
queries:
|
||||
- uses: ./foo_file`;
|
||||
|
||||
/** The configuration in `simpleConfigFileContents`. */
|
||||
const simpleConfigInput: UserConfig = {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
};
|
||||
|
||||
/** A less minimal configuration file. */
|
||||
const otherConfigFileContents = `
|
||||
name: my config
|
||||
@@ -558,16 +564,14 @@ test.serial(
|
||||
tempDir,
|
||||
);
|
||||
|
||||
const configInput = `
|
||||
name: my config
|
||||
queries:
|
||||
- uses: ./foo
|
||||
packs:
|
||||
javascript:
|
||||
- a/b@1.2.3
|
||||
python:
|
||||
- c/d@1.2.3
|
||||
`;
|
||||
const configInput: UserConfig = {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo" }],
|
||||
packs: {
|
||||
javascript: ["a/b@1.2.3"],
|
||||
python: ["c/d@1.2.3"],
|
||||
},
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, "foo"));
|
||||
|
||||
@@ -598,7 +602,16 @@ test.serial(
|
||||
}),
|
||||
);
|
||||
|
||||
t.deepEqual(config.originalUserInput, yaml.load(configInput));
|
||||
// Compare with a separate object rather than `configInput` itself, so that the test catches
|
||||
// changes made to the input in place.
|
||||
t.deepEqual(config.originalUserInput, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo" }],
|
||||
packs: {
|
||||
javascript: ["a/b@1.2.3"],
|
||||
python: ["c/d@1.2.3"],
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -2413,7 +2426,7 @@ test("determineUserConfig - loads config input", async (t) => {
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configInput: simpleConfigInput,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
@@ -2423,17 +2436,15 @@ test("determineUserConfig - loads config input", async (t) => {
|
||||
|
||||
await target
|
||||
// The input source and path of the generated config file should have been logged.
|
||||
.logs(
|
||||
t,
|
||||
"Using config from action input:",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
// The message about no configuration input and
|
||||
// the warning about both inputs should not have been logged.
|
||||
.logs(t, `Using config from action input: ${expectedConfigPath}`)
|
||||
// The message about no configuration input and the warning about both inputs should not have
|
||||
// been logged. The generated config file isn't loaded, since the `config` input has already
|
||||
// been parsed.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
@@ -2452,7 +2463,7 @@ test("determineUserConfig - ignores config file input when both specified", asyn
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configInput: simpleConfigInput,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
@@ -2466,10 +2477,14 @@ test("determineUserConfig - ignores config file input when both specified", asyn
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
// The generated config file isn't loaded, since the `config` input has already been parsed.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
@@ -2482,11 +2497,14 @@ test("determineUserConfig - ignores config file input when both specified", asyn
|
||||
});
|
||||
|
||||
/** A `config` input that we might get from Default Setup. */
|
||||
const defaultSetupConfigInput = `
|
||||
threat-models: [local, remote]
|
||||
default-setup:
|
||||
org:
|
||||
model-packs: [foo, bar]`;
|
||||
const defaultSetupConfigInput: UserConfig = {
|
||||
"threat-models": ["local", "remote"],
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": ["foo", "bar"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
@@ -2555,7 +2573,7 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configInput: simpleConfigInput,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
@@ -2565,10 +2583,14 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
// The generated config file isn't loaded, since the `config` input has already been parsed.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
@@ -2587,7 +2609,7 @@ test("determineUserConfig - ignores config file input outside Default Setup if F
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configInput: simpleConfigInput,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
@@ -2597,10 +2619,14 @@ test("determineUserConfig - ignores config file input outside Default Setup if F
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
// The generated config file isn't loaded, since the `config` input has already been parsed.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
|
||||
@@ -90,9 +90,8 @@ import {
|
||||
Result,
|
||||
Success,
|
||||
Failure,
|
||||
isHostedRunner,
|
||||
looksLikeHostedRunner,
|
||||
} from "./util";
|
||||
|
||||
export { type Config } from "./config/action-config";
|
||||
|
||||
/**
|
||||
@@ -338,7 +337,8 @@ export interface InitConfigInputs {
|
||||
packsInput: string | undefined;
|
||||
configFile: string | undefined;
|
||||
dbLocation: string | undefined;
|
||||
configInput: string | undefined;
|
||||
/** The configuration from the `config` input. */
|
||||
configInput: UserConfig | undefined;
|
||||
buildModeInput: string | undefined;
|
||||
ramInput: string | undefined;
|
||||
dependencyCachingEnabled: string | undefined;
|
||||
@@ -938,7 +938,7 @@ export async function isTrapCachingEnabled(
|
||||
if (trapCaching !== undefined) return trapCaching === "true";
|
||||
|
||||
// On self-hosted runners which may have slow network access, disable TRAP caching by default.
|
||||
if (!isHostedRunner()) return false;
|
||||
if (!looksLikeHostedRunner()) return false;
|
||||
|
||||
// If overlay analysis is enabled, then disable TRAP caching since overlay analysis supersedes it.
|
||||
// This change is gated behind a feature flag.
|
||||
@@ -1058,17 +1058,13 @@ export async function determineUserConfig(
|
||||
tempDir: string,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<UserConfig> {
|
||||
const validateConfig = await action.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
|
||||
// We have the following cases:
|
||||
// 1. A `config` or `config-file` input is provided, but not both: use the provided one.
|
||||
// 2. Both are provided and we are in an advanced workflow: ignore the `config-file` input.
|
||||
// 3. Both are provided and we are in Default Setup: the `config` input uses a limited
|
||||
// set of options, which are supported by `mergeDefaultSetupAndUserConfigs`,
|
||||
// and we merge the two configs.
|
||||
if (inputs.configInput) {
|
||||
if (inputs.configInput !== undefined) {
|
||||
const computedConfigPath = userConfigFromActionPath(tempDir);
|
||||
|
||||
// Get a function which enables us to determine whether the FF that allows us to
|
||||
@@ -1085,12 +1081,6 @@ export async function determineUserConfig(
|
||||
) {
|
||||
// If the FF is enabled and we are in Default Setup, combine the supported
|
||||
// configuration file properties and write the result to disk.
|
||||
const fromConfigInput = parseUserConfig(
|
||||
action.logger,
|
||||
"`config` input",
|
||||
inputs.configInput,
|
||||
validateConfig,
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
action,
|
||||
inputs.configFile,
|
||||
@@ -1103,7 +1093,7 @@ export async function determineUserConfig(
|
||||
// the CLI or other CodeQL Action steps.
|
||||
const mergedConfig = mergeDefaultSetupAndUserConfigs(
|
||||
action.logger,
|
||||
fromConfigInput,
|
||||
inputs.configInput,
|
||||
fromConfigFile,
|
||||
);
|
||||
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
|
||||
@@ -1123,12 +1113,13 @@ export async function determineUserConfig(
|
||||
);
|
||||
}
|
||||
|
||||
// Write the `config` input straight to disk.
|
||||
fs.writeFileSync(computedConfigPath, inputs.configInput);
|
||||
// Write the `config` input to disk without merging it with a configuration file.
|
||||
fs.writeFileSync(computedConfigPath, yaml.dump(inputs.configInput));
|
||||
inputs.configFile = computedConfigPath;
|
||||
action.logger.debug(
|
||||
`Using config from action input: ${inputs.configFile}`,
|
||||
);
|
||||
return inputs.configInput;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1426,7 +1417,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(" "),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -625,3 +625,73 @@ test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Se
|
||||
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
|
||||
]);
|
||||
});
|
||||
|
||||
/** Parses `contents` as a configuration without validating it. */
|
||||
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {
|
||||
return dbConfig.parseUserConfig(
|
||||
getRunnerLogger(true),
|
||||
"test",
|
||||
contents,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
test("matchesDefaultSetupConfigSchema - returns true for configurations that only use Default Setup properties", (t) => {
|
||||
for (const contents of [
|
||||
[
|
||||
"default-setup:",
|
||||
" org:",
|
||||
" model-packs: [ github/immutable-actions-list@0.0.1 ]",
|
||||
"threat-models: [ ]",
|
||||
].join("\n"),
|
||||
"threat-models: [ local ]",
|
||||
"{}",
|
||||
]) {
|
||||
t.true(
|
||||
dbConfig.matchesDefaultSetupConfigSchema(
|
||||
parseUnvalidatedConfig(contents),
|
||||
),
|
||||
contents,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("matchesDefaultSetupConfigSchema - returns false for configurations that use other properties", (t) => {
|
||||
for (const contents of [
|
||||
"queries: [ { uses: ./queries/show_ifs.ql } ]",
|
||||
"paths-ignore: [ tests ]",
|
||||
"default-setup: { org: { model-packs: [], queries: [] } }",
|
||||
]) {
|
||||
t.false(
|
||||
dbConfig.matchesDefaultSetupConfigSchema(
|
||||
parseUnvalidatedConfig(contents),
|
||||
),
|
||||
contents,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("matchesDefaultSetupConfigSchema - returns false for invalid Default Setup properties", (t) => {
|
||||
for (const contents of [
|
||||
"threat-models: local",
|
||||
"default-setup: { org: { model-packs: [ 1 ] } }",
|
||||
]) {
|
||||
t.false(
|
||||
dbConfig.matchesDefaultSetupConfigSchema(
|
||||
parseUnvalidatedConfig(contents),
|
||||
),
|
||||
contents,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("matchesDefaultSetupConfigSchema - returns false for configurations that aren't mappings", (t) => {
|
||||
for (const contents of ["- threat-models", "local", "null"]) {
|
||||
t.false(
|
||||
dbConfig.matchesDefaultSetupConfigSchema(
|
||||
parseUnvalidatedConfig(contents),
|
||||
),
|
||||
contents,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -33,6 +33,15 @@ export interface QuerySpec {
|
||||
uses: string;
|
||||
}
|
||||
|
||||
// A set of default query suite names that are understood by the CLI.
|
||||
export const defaultSuites: Set<string> = new Set([
|
||||
"security-experimental",
|
||||
"security-extended",
|
||||
"security-and-quality",
|
||||
"code-quality",
|
||||
"code-scanning",
|
||||
]);
|
||||
|
||||
const ORG_SCHEMA = {
|
||||
/** An array of model pack names. */
|
||||
"model-packs": json.optional(json.array(json.string)),
|
||||
@@ -77,7 +86,11 @@ export interface UserConfig {
|
||||
"default-setup"?: DefaultSetupConfig;
|
||||
}
|
||||
|
||||
/** A subset of the `UserConfig` schema that is used by Default Setup. */
|
||||
/**
|
||||
* A subset of the `UserConfig` schema that is known to be used by Default Setup. None of these
|
||||
* properties may add queries, since a per-language CodeQL bundle can be used with a `config` input
|
||||
* that only sets them.
|
||||
*/
|
||||
const DEFAULT_SETUP_CONFIG_SCHEMA = {
|
||||
"threat-models": json.optional(json.array(json.string)),
|
||||
"default-setup": json.optional<DefaultSetupConfig>(
|
||||
@@ -85,6 +98,24 @@ const DEFAULT_SETUP_CONFIG_SCHEMA = {
|
||||
),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* Returns whether `config` matches what we expect Default Setup to send in the `config` input: a
|
||||
* mapping that only sets properties in `DEFAULT_SETUP_CONFIG_SCHEMA`, with values of the expected
|
||||
* types.
|
||||
*/
|
||||
export function matchesDefaultSetupConfigSchema(config: UserConfig): boolean {
|
||||
// Unless validation is enabled, `parseUserConfig` doesn't check that the YAML is a mapping.
|
||||
if (!json.isObject(config)) {
|
||||
return false;
|
||||
}
|
||||
const result = json.checkSchema(
|
||||
DEFAULT_SETUP_CONFIG_SCHEMA,
|
||||
config as json.UnvalidatedObject<any>,
|
||||
);
|
||||
// `valid` doesn't account for unknown properties.
|
||||
return result.valid && result.unknownKeys.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges supported properties from two configuration files. This is intended only for
|
||||
* use with merging the `config` input provided by Default Setup with a potentially
|
||||
@@ -437,63 +468,60 @@ export async function calculateAugmentation(
|
||||
languages,
|
||||
packsInputCombines,
|
||||
);
|
||||
const queriesInputCombines = shouldCombine(rawQueriesInput);
|
||||
const queriesInput = parseQueriesFromInput(
|
||||
rawQueriesInput,
|
||||
queriesInputCombines,
|
||||
const queries = parseQueriesFromInput(rawQueriesInput);
|
||||
const repoPropertyQueries = parseQueriesFromInput(
|
||||
repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES],
|
||||
RepositoryPropertyName.EXTRA_QUERIES,
|
||||
);
|
||||
|
||||
const repoExtraQueries =
|
||||
repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES];
|
||||
const repoExtraQueriesCombines = shouldCombine(repoExtraQueries);
|
||||
const repoPropertyQueries = {
|
||||
combines: repoExtraQueriesCombines,
|
||||
input: parseQueriesFromInput(
|
||||
repoExtraQueries,
|
||||
repoExtraQueriesCombines,
|
||||
new ConfigurationError(
|
||||
errorMessages.getRepoPropertyError(
|
||||
RepositoryPropertyName.EXTRA_QUERIES,
|
||||
errorMessages.getEmptyCombinesError(),
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
packsInputCombines,
|
||||
packsInput: packsInput?.[languages[0]],
|
||||
queriesInput,
|
||||
queriesInputCombines,
|
||||
queriesInput: queries.input,
|
||||
queriesInputCombines: queries.combines,
|
||||
repoPropertyQueries,
|
||||
};
|
||||
}
|
||||
|
||||
function parseQueriesFromInput(
|
||||
rawQueriesInput: string | undefined,
|
||||
queriesInputCombines: boolean,
|
||||
errorToThrow?: ConfigurationError,
|
||||
) {
|
||||
if (!rawQueriesInput) {
|
||||
return undefined;
|
||||
/**
|
||||
* Parses a comma-separated list of queries, which may have a '+' prefix. Entries aren't validated,
|
||||
* so an empty entry becomes `{ uses: "" }`.
|
||||
*
|
||||
* @param value The list of queries.
|
||||
* @param repositoryProperty The repository property that `value` comes from, if any. Error messages
|
||||
* refer to this property, or to the `queries` input if no property is given.
|
||||
* @returns An `Augmentation` containing the parsed queries and whether `value` has a '+' prefix.
|
||||
* The queries are `undefined` if `value` is unset or empty.
|
||||
* @throws A `ConfigurationError` if `value` is a '+' with no queries after it.
|
||||
*/
|
||||
export function parseQueriesFromInput(
|
||||
value: string | undefined,
|
||||
repositoryProperty?: RepositoryPropertyName,
|
||||
): Augmentation<QuerySpec[]> {
|
||||
if (!value) {
|
||||
return { combines: false, input: undefined };
|
||||
}
|
||||
|
||||
const trimmedInput = queriesInputCombines
|
||||
? rawQueriesInput.trim().slice(1).trim()
|
||||
: (rawQueriesInput?.trim() ?? "");
|
||||
if (queriesInputCombines && trimmedInput.length === 0) {
|
||||
if (errorToThrow) {
|
||||
throw errorToThrow;
|
||||
}
|
||||
const combines = shouldCombine(value);
|
||||
const trimmedInput = combines ? value.trim().slice(1).trim() : value.trim();
|
||||
if (combines && trimmedInput.length === 0) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFilePropertyError(
|
||||
undefined,
|
||||
"queries",
|
||||
"A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs.",
|
||||
),
|
||||
repositoryProperty !== undefined
|
||||
? errorMessages.getRepoPropertyError(
|
||||
repositoryProperty,
|
||||
errorMessages.getEmptyCombinesError(),
|
||||
)
|
||||
: errorMessages.getConfigFilePropertyError(
|
||||
undefined,
|
||||
"queries",
|
||||
"A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs.",
|
||||
),
|
||||
);
|
||||
}
|
||||
return trimmedInput.split(",").map((query) => ({ uses: query.trim() }));
|
||||
return {
|
||||
combines,
|
||||
input: trimmedInput.split(",").map((query) => ({ uses: query.trim() })),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,11 +638,13 @@ export function generateCodeScanningConfig(
|
||||
* Attempts to parse `contents` into a `UserConfig` value.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param pathInput The path to the file where `contents` was obtained from, for use in error messages.
|
||||
* @param contents The string contents of a YAML file to try and parse as a `UserConfig`.
|
||||
* @param validateConfig Whether to validate the configuration file against the schema.
|
||||
* @returns The `UserConfig` corresponding to `contents`, if parsing was successful.
|
||||
* @throws A `ConfigurationError` if parsing failed.
|
||||
* @param pathInput Where `contents` came from, such as the path to a file, for use in error messages.
|
||||
* @param contents The YAML to try and parse as a `UserConfig`.
|
||||
* @param validateConfig Whether to validate the configuration against the schema.
|
||||
* @returns The `UserConfig` corresponding to `contents`, if parsing was successful. Unless
|
||||
* `validateConfig` is set, the result might not be a mapping.
|
||||
* @throws A `ConfigurationError` if `contents` isn't valid YAML or, when `validateConfig` is set,
|
||||
* isn't a valid configuration.
|
||||
*/
|
||||
export function parseUserConfig(
|
||||
logger: Logger,
|
||||
|
||||
@@ -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.26.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
"bundleVersion": "codeql-bundle-v2.27.1",
|
||||
"cliVersion": "2.27.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.27.0",
|
||||
"priorCliVersion": "2.27.0"
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ export enum EnvVar {
|
||||
/** Whether the CodeQL Action has already warned the user about low disk space. */
|
||||
HAS_WARNED_ABOUT_DISK_SPACE = "CODEQL_ACTION_HAS_WARNED_ABOUT_DISK_SPACE",
|
||||
|
||||
/**
|
||||
* Whether a step in this job has already set up CodeQL. Steps that run afterwards may be holding
|
||||
* a path into the toolcache, so we must not delete anything from it.
|
||||
*/
|
||||
HAS_SET_UP_CODEQL = "CODEQL_ACTION_HAS_SET_UP_CODEQL",
|
||||
|
||||
/** Whether the `setup-codeql` action has been run. */
|
||||
SETUP_CODEQL_ACTION_HAS_RUN = "CODEQL_ACTION_SETUP_CODEQL_HAS_RUN",
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ export enum Feature {
|
||||
AllowMergeConfigFiles = "allow_merge_config_files",
|
||||
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||
/**
|
||||
* Controls whether we delete CodeQL bundles that we are not going to use from the toolcache
|
||||
* before downloading a different bundle, in order to reclaim disk space.
|
||||
*/
|
||||
CleanupToolcacheBundles = "cleanup_toolcache_bundles",
|
||||
CleanupTrapCaches = "cleanup_trap_caches",
|
||||
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
||||
ConfigFileRepositoryProperty = "config_file_repository_property",
|
||||
@@ -159,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_v2",
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
@@ -211,6 +221,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CleanupToolcacheBundles]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CleanupTrapCaches]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||
@@ -424,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);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -15,10 +15,12 @@ import { getRunnerLogger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import * as overlayStatus from "./overlay/status";
|
||||
import { parseRepositoryNwo } from "./repository";
|
||||
import { JobStatus } from "./status-report";
|
||||
import {
|
||||
createFeatures,
|
||||
createTestConfig,
|
||||
DEFAULT_ACTIONS_VARS,
|
||||
getTestEnv,
|
||||
makeMacro,
|
||||
makeVersionInfo,
|
||||
RecordingLogger,
|
||||
@@ -58,6 +60,8 @@ test.serial("init-post action with debug mode off", async (t) => {
|
||||
createTestConfig({ debugMode: false }),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -80,6 +84,8 @@ test.serial("init-post action with debug mode on", async (t) => {
|
||||
createTestConfig({ debugMode: true }),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -375,6 +381,8 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -443,6 +451,8 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -457,8 +467,13 @@ test.serial(
|
||||
test.serial("does not save overlay status when build successful", async (t) => {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
// Mark analyze as having completed successfully.
|
||||
// Mark analyze as having completed successfully. `tryUploadSarifIfRunFailed` reads this from
|
||||
// the process environment, while `recordOverlayStatus` reads it from the environment it is
|
||||
// given.
|
||||
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] = "true";
|
||||
const env = getTestEnv({
|
||||
[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]: "true",
|
||||
});
|
||||
|
||||
sinon.stub(util, "checkDiskUsage").resolves({
|
||||
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
|
||||
@@ -480,6 +495,8 @@ test.serial("does not save overlay status when build successful", async (t) => {
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
"success",
|
||||
env,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -517,6 +534,8 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -528,6 +547,137 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Runs `uploadFailureInfo` for an overlay-base job that did not complete successfully, with the
|
||||
* given job status from the Actions runtime environment.
|
||||
*/
|
||||
async function runOverlayPostStep({
|
||||
jobStatus,
|
||||
codeQlReportedError = false,
|
||||
}: {
|
||||
jobStatus: string | undefined;
|
||||
codeQlReportedError?: boolean;
|
||||
}) {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY];
|
||||
const env = getTestEnv(
|
||||
codeQlReportedError
|
||||
? { [EnvVar.JOB_STATUS]: JobStatus.FailureStatus }
|
||||
: {},
|
||||
);
|
||||
|
||||
sinon.stub(util, "checkDiskUsage").resolves({
|
||||
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
|
||||
numTotalBytes: 200 * NUM_BYTES_PER_GIB,
|
||||
});
|
||||
|
||||
const saveOverlayStatusStub = sinon
|
||||
.stub(overlayStatus, "saveOverlayStatus")
|
||||
.resolves(true);
|
||||
|
||||
await initActionPostHelper.uploadFailureInfo(
|
||||
sinon.spy(),
|
||||
sinon.spy(),
|
||||
codeql.createStubCodeQL({}),
|
||||
createTestConfig({
|
||||
debugMode: false,
|
||||
languages: ["javascript"],
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
jobStatus,
|
||||
env,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
return { saveOverlayStatusStub };
|
||||
});
|
||||
}
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job was cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "cancelled",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"a cancellation tells us nothing about whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job status is not recognised",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "some-new-status",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"a status we do not recognise tells us nothing about whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job status is unavailable",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: undefined,
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"without a job status we cannot tell whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"saves overlay status when the job failed rather than being cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "failure",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"a failed job indicates that the analysis itself failed",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("saves overlay status when the job succeeded", async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "success",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"the analysis did not complete successfully even though the job as a whole succeeded",
|
||||
);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"saves overlay status when a CodeQL Action reported an error before the run was cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "cancelled",
|
||||
codeQlReportedError: true,
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"the analysis genuinely failed, even though the run was later cancelled",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function createTestWorkflow(
|
||||
steps: workflow.WorkflowJobStep[],
|
||||
): workflow.Workflow {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
sanitizeArtifactName,
|
||||
} from "./debug-artifacts";
|
||||
import * as dependencyCaching from "./dependency-caching";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, ReadOnlyEnv } from "./environment";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
@@ -316,6 +316,8 @@ export async function tryUploadSarifIfRunFailed(
|
||||
* @param config The CodeQL Action configuration.
|
||||
* @param repositoryNwo The name and owner of the repository.
|
||||
* @param features Information about enabled features.
|
||||
* @param jobStatus The status of the job, as reported by the Actions runtime environment.
|
||||
* @param env The environment to read variables from.
|
||||
* @param logger The logger to use.
|
||||
* @returns The results of uploading the SARIF file for the failure.
|
||||
*/
|
||||
@@ -331,9 +333,11 @@ export async function uploadFailureInfo(
|
||||
config: Config,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
env: ReadOnlyEnv,
|
||||
logger: Logger,
|
||||
): Promise<UploadFailedSarifResult> {
|
||||
await recordOverlayStatus(codeql, config, features, logger);
|
||||
await recordOverlayStatus(codeql, config, features, jobStatus, env, logger);
|
||||
|
||||
const uploadFailedSarifResult = await tryUploadSarifIfRunFailed(
|
||||
config,
|
||||
@@ -412,6 +416,37 @@ export async function uploadFailureInfo(
|
||||
return uploadFailedSarifResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether one of the CodeQL Actions reported an error for this job, which means the analysis
|
||||
* genuinely failed.
|
||||
*
|
||||
* Note that the converse does not hold: an Action that is terminated abruptly, or that fails before
|
||||
* it can gather telemetry, does not get to report anything.
|
||||
*/
|
||||
function didCodeQlReportError(env: ReadOnlyEnv): boolean {
|
||||
const jobStatus = env.getOptional(EnvVar.JOB_STATUS);
|
||||
return (
|
||||
jobStatus === JobStatus.FailureStatus ||
|
||||
jobStatus === JobStatus.ConfigErrorStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the job status tells us anything about whether the analysis itself would have succeeded.
|
||||
*
|
||||
* We check for the statuses we know to be meaningful rather than excluding the ones that are not,
|
||||
* so that a status we do not recognise is treated as inconclusive.
|
||||
*/
|
||||
function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
|
||||
switch (jobStatus?.trim().toLowerCase()) {
|
||||
case "failure":
|
||||
case "success":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If overlay base database creation was attempted but the analysis did not complete
|
||||
* successfully, save the failure status to the Actions cache so that subsequent runs
|
||||
@@ -421,16 +456,30 @@ async function recordOverlayStatus(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
env: ReadOnlyEnv,
|
||||
logger: Logger,
|
||||
) {
|
||||
if (
|
||||
config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase ||
|
||||
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true" ||
|
||||
env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" ||
|
||||
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only record a failure when the job outcome tells us something about the analysis. A cancelled
|
||||
// job, or a status we do not recognise, says nothing about whether the analysis would have
|
||||
// succeeded, so recording a failure would disable overlay analysis needlessly. We still record
|
||||
// one if a CodeQL Action reported an error before the job ended.
|
||||
if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) {
|
||||
logger.info(
|
||||
"Not recording an improved incremental analysis failure for this job because the job " +
|
||||
`status (${jobStatus ?? "unset"}) does not tell us whether the analysis itself failed.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkRunIdInput = actionsUtil.getOptionalInput("check-run-id");
|
||||
const checkRunId =
|
||||
checkRunIdInput !== undefined ? parseInt(checkRunIdInput, 10) : undefined;
|
||||
|
||||
@@ -8,10 +8,11 @@ import * as core from "@actions/core";
|
||||
|
||||
import {
|
||||
restoreInputs,
|
||||
getOptionalInput,
|
||||
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";
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
DependencyCachingUsageReport,
|
||||
getDependencyCacheUsage,
|
||||
} from "./dependency-caching";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import * as initActionPostHelper from "./init-action-post-helper";
|
||||
@@ -55,10 +56,17 @@ async function run(startedAt: Date) {
|
||||
| undefined;
|
||||
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
|
||||
try {
|
||||
// Restore inputs from `init` Action.
|
||||
restoreInputs();
|
||||
// Read the job status before restoring inputs, since it is provided by the Actions runtime
|
||||
// environment for this step and would otherwise be overwritten by the value that the `init`
|
||||
// Action saw, which is always a success.
|
||||
const jobStatus = getOptionalInput("job-status");
|
||||
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
// Restore inputs from `init` Action.
|
||||
restoreInputs(logger);
|
||||
|
||||
config = await getConfig(getTemporaryDirectory(), logger);
|
||||
|
||||
const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion());
|
||||
checkGitHubVersionInRange(gitHubVersion, logger);
|
||||
|
||||
const repositoryNwo = getRepositoryNwo();
|
||||
@@ -69,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.",
|
||||
@@ -84,6 +91,8 @@ async function run(startedAt: Date) {
|
||||
config,
|
||||
repositoryNwo,
|
||||
features,
|
||||
jobStatus,
|
||||
getEnv(),
|
||||
logger,
|
||||
);
|
||||
|
||||
@@ -99,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(
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
shouldRestoreCache,
|
||||
} from "./caching-utils";
|
||||
import { CodeQL } from "./codeql";
|
||||
import { parseUserConfig } from "./config/db-config";
|
||||
import { getConfigFileInput } from "./config/file";
|
||||
import { ComputedInput, getToolsInput } from "./config/inputs";
|
||||
import * as configUtils from "./config-utils";
|
||||
@@ -38,9 +39,12 @@ 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 {
|
||||
loadRepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "./feature-flags/properties";
|
||||
import {
|
||||
checkInstallPython311,
|
||||
checkPacksForOverlayCompatibility,
|
||||
@@ -58,18 +62,19 @@ import {
|
||||
OverlayBaseDatabaseDownloadStats,
|
||||
} from "./overlay/caching";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { getOtherLanguagePacksReason } from "./per-language-bundles";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
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 +86,6 @@ import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
getCodeQLMemoryLimit,
|
||||
getRequiredEnvParam,
|
||||
getThreadsFlagValue,
|
||||
initializeEnvironment,
|
||||
ConfigurationError,
|
||||
@@ -168,15 +172,10 @@ async function sendCompletedStatusReport(
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
|
||||
const initToolsDownloadFields: InitToolsDownloadFields = {};
|
||||
|
||||
if (toolsDownloadStatusReport?.downloadDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_download_duration_ms =
|
||||
toolsDownloadStatusReport.downloadDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
const initToolsDownloadFields = createInitToolsDownloadFields(
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
);
|
||||
|
||||
if (config !== undefined) {
|
||||
// Append fields that are dependent on `config`
|
||||
@@ -230,8 +229,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();
|
||||
@@ -260,7 +259,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") || "",
|
||||
);
|
||||
|
||||
@@ -308,6 +307,24 @@ async function run(
|
||||
const rawLanguages = configUtils.getRawLanguagesNoAutodetect(
|
||||
getOptionalInput("languages"),
|
||||
);
|
||||
const rawConfigInput = getOptionalInput("config");
|
||||
const configInput =
|
||||
rawConfigInput === undefined
|
||||
? undefined
|
||||
: parseUserConfig(
|
||||
logger,
|
||||
"`config` input",
|
||||
rawConfigInput,
|
||||
await features.getValue(Feature.ValidateDbConfig),
|
||||
);
|
||||
const queriesInput = getOptionalInput("queries");
|
||||
const otherLanguagePacksReason = getOtherLanguagePacksReason({
|
||||
configFile,
|
||||
configInput,
|
||||
queriesInput,
|
||||
extraQueriesProperty:
|
||||
repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES],
|
||||
});
|
||||
const useOverlayAwareDefaultCliVersion =
|
||||
analysisKinds?.length === 1 &&
|
||||
analysisKinds[0] === AnalysisKind.CodeScanning;
|
||||
@@ -318,6 +335,7 @@ async function run(
|
||||
gitHubVersion.type,
|
||||
codeQLDefaultVersionInfo,
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
useOverlayAwareDefaultCliVersion,
|
||||
features,
|
||||
logger,
|
||||
@@ -368,13 +386,13 @@ async function run(
|
||||
config = await initConfig(actionStateWithFeatures, {
|
||||
analysisKinds,
|
||||
languagesInput: getOptionalInput("languages"),
|
||||
queriesInput: getOptionalInput("queries"),
|
||||
queriesInput,
|
||||
packsInput: getOptionalInput("packs"),
|
||||
buildModeInput: getOptionalInput("build-mode"),
|
||||
ramInput: getOptionalInput("ram"),
|
||||
configFile,
|
||||
dbLocation: getOptionalInput("db-location"),
|
||||
configInput: getOptionalInput("config"),
|
||||
configInput,
|
||||
dependencyCachingEnabled: getDependencyCachingEnabled(),
|
||||
// Debug mode is enabled if:
|
||||
// - The `init` Action is passed `debug: true`.
|
||||
@@ -388,7 +406,9 @@ async function run(
|
||||
repository: repositoryNwo,
|
||||
tempDir: getTemporaryDirectory(),
|
||||
codeql,
|
||||
workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
workspacePath: actionState.env.getRequired(
|
||||
ActionsEnvVars.GITHUB_WORKSPACE,
|
||||
),
|
||||
sourceRoot,
|
||||
githubVersion: gitHubVersion,
|
||||
apiDetails,
|
||||
@@ -720,7 +740,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);
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function initCodeQL(
|
||||
variant: util.GitHubVariant,
|
||||
defaultCliVersion: CodeQLDefaultVersionInfo,
|
||||
rawLanguages: string[] | undefined,
|
||||
otherLanguagePacksReason: string | undefined,
|
||||
useOverlayAwareDefaultCliVersion: boolean,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
@@ -58,6 +59,7 @@ export async function initCodeQL(
|
||||
variant,
|
||||
defaultCliVersion,
|
||||
rawLanguages,
|
||||
otherLanguagePacksReason,
|
||||
useOverlayAwareDefaultCliVersion,
|
||||
features,
|
||||
logger,
|
||||
|
||||
@@ -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,
|
||||
|
||||
297
src/per-language-bundles.test.ts
Normal file
297
src/per-language-bundles.test.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import test from "ava";
|
||||
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import {
|
||||
getOtherLanguagePacksReason,
|
||||
getPerLanguageBundleLanguage,
|
||||
MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
|
||||
PerLanguageBundleOptions,
|
||||
QueryConfigInputs,
|
||||
} from "./per-language-bundles";
|
||||
import { BundlePlatform } from "./platform";
|
||||
import {
|
||||
createFeatures,
|
||||
getRecordingLogger,
|
||||
getTestEnv,
|
||||
initAllState,
|
||||
LoggedMessage,
|
||||
} from "./testing-utils";
|
||||
import { ConfigurationError, GitHubVariant } from "./util";
|
||||
|
||||
/** Options for which we would use a per-language bundle. */
|
||||
const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = {
|
||||
rawLanguages: ["java"],
|
||||
otherLanguagePacksReason: undefined,
|
||||
// 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 explains why the CodeQL CLI may need packs for other languages before checking the languages", async (t) => {
|
||||
// Without a language, the explanation would otherwise suggest requesting a single language.
|
||||
for (const rawLanguages of [["java"], undefined]) {
|
||||
const messages: LoggedMessage[] = [];
|
||||
const language = await checkEligibility(
|
||||
{ rawLanguages, otherLanguagePacksReason: "an example reason" },
|
||||
{ logger: getRecordingLogger(messages, { logToConsole: false }) },
|
||||
);
|
||||
|
||||
t.is(language, undefined);
|
||||
t.deepEqual(
|
||||
messages.map((message) => message.message),
|
||||
["Not using a per-language CodeQL bundle since an example reason."],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
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 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"] },
|
||||
{ otherLanguagePacksReason: "an example reason" },
|
||||
{ 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,
|
||||
);
|
||||
});
|
||||
|
||||
/** Query configuration inputs that configure nothing. */
|
||||
const NO_QUERY_CONFIG: QueryConfigInputs = {
|
||||
configFile: undefined,
|
||||
configInput: undefined,
|
||||
queriesInput: undefined,
|
||||
extraQueriesProperty: undefined,
|
||||
};
|
||||
|
||||
test("getOtherLanguagePacksReason returns undefined when no queries are configured", (t) => {
|
||||
t.is(getOtherLanguagePacksReason(NO_QUERY_CONFIG), undefined);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason returns undefined for built-in query suites", (t) => {
|
||||
for (const queries of [
|
||||
"security-extended",
|
||||
"+security-and-quality",
|
||||
" security-extended , code-quality ",
|
||||
]) {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
queriesInput: queries,
|
||||
extraQueriesProperty: queries,
|
||||
}),
|
||||
undefined,
|
||||
queries,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason returns undefined for a config input that only uses default setup properties", (t) => {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
// The configuration from the `config` input that default setup passes.
|
||||
configInput: {
|
||||
"default-setup": {
|
||||
org: { "model-packs": ["github/immutable-actions-list@0.0.1"] },
|
||||
},
|
||||
"threat-models": [],
|
||||
},
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason explains a configuration file", (t) => {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
configFile: "./.github/codeql/codeql-config.yml",
|
||||
}),
|
||||
"the configuration file './.github/codeql/codeql-config.yml' may use queries that need " +
|
||||
"library packs for other languages",
|
||||
);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason explains a config input that uses other properties", (t) => {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
configInput: { queries: [{ uses: "./queries/show_ifs.ql" }] },
|
||||
}),
|
||||
"the 'config' input may use queries that need library packs for other languages",
|
||||
);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason explains the first query in the queries input that isn't a built-in query suite", (t) => {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
queriesInput:
|
||||
"+security-extended, ./queries/show_ifs.ql, octo-org/queries@main",
|
||||
}),
|
||||
"the query './queries/show_ifs.ql' from the 'queries' input may need library packs for " +
|
||||
"other languages",
|
||||
);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason explains a query in the extra queries repository property that isn't a built-in query suite", (t) => {
|
||||
t.is(
|
||||
getOtherLanguagePacksReason({
|
||||
...NO_QUERY_CONFIG,
|
||||
extraQueriesProperty: "+octo-org/queries/show_ifs.ql@main",
|
||||
}),
|
||||
"the query 'octo-org/queries/show_ifs.ql@main' from the 'github-codeql-extra-queries' " +
|
||||
"repository property may need library packs for other languages",
|
||||
);
|
||||
});
|
||||
|
||||
test("getOtherLanguagePacksReason throws a ConfigurationError for a '+' with no queries after it", (t) => {
|
||||
// Loading the configuration would throw the same errors.
|
||||
for (const inputs of [
|
||||
{ queriesInput: "+" },
|
||||
{ extraQueriesProperty: " + " },
|
||||
]) {
|
||||
t.throws(
|
||||
() => getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, ...inputs }),
|
||||
{ instanceOf: ConfigurationError },
|
||||
);
|
||||
}
|
||||
});
|
||||
247
src/per-language-bundles.ts
Normal file
247
src/per-language-bundles.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import { isGitHubHostedRunner } from "./actions-util";
|
||||
import {
|
||||
defaultSuites,
|
||||
matchesDefaultSetupConfigSchema,
|
||||
parseQueriesFromInput,
|
||||
QuerySpec,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { RepositoryPropertyName } from "./feature-flags/properties";
|
||||
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
|
||||
import { BundlePlatform } from "./platform";
|
||||
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(),
|
||||
};
|
||||
|
||||
/** Query configuration that is known before CodeQL is set up. */
|
||||
export interface QueryConfigInputs {
|
||||
/** The configuration file from the `config-file` input or repository property. */
|
||||
configFile: string | undefined;
|
||||
/** The configuration from the `config` input. */
|
||||
configInput: UserConfig | undefined;
|
||||
/** The `queries` input. */
|
||||
queriesInput: string | undefined;
|
||||
/** The `github-codeql-extra-queries` repository property. */
|
||||
extraQueriesProperty: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explains why the configured queries may need library packs for languages other than the one
|
||||
* being analyzed, which a per-language bundle doesn't contain. Returns `undefined` if the only
|
||||
* queries that these inputs add are built-in query suites. The `packs` input doesn't matter, since
|
||||
* query packs are downloaded together with their dependencies.
|
||||
*
|
||||
* Any configuration file is assumed to configure such queries, since reading it may need file or API
|
||||
* access. So is the `config` input, unless it only sets the properties that default setup is known
|
||||
* to set (see `matchesDefaultSetupConfigSchema`).
|
||||
*
|
||||
* @throws A `ConfigurationError` if the `queries` input or the `github-codeql-extra-queries`
|
||||
* repository property is a '+' with no queries after it, unless an input that's checked earlier
|
||||
* already gives a reason.
|
||||
*/
|
||||
export function getOtherLanguagePacksReason(
|
||||
inputs: QueryConfigInputs,
|
||||
): string | undefined {
|
||||
if (inputs.configFile !== undefined) {
|
||||
return (
|
||||
`the configuration file '${inputs.configFile}' may use queries that need library packs ` +
|
||||
"for other languages"
|
||||
);
|
||||
}
|
||||
|
||||
// The `config` input can configure queries in the same way as a configuration file. The
|
||||
// properties that default setup is known to set, listed in `DEFAULT_SETUP_CONFIG_SCHEMA` in
|
||||
// `config/db-config.ts`, don't add queries.
|
||||
if (
|
||||
inputs.configInput !== undefined &&
|
||||
!matchesDefaultSetupConfigSchema(inputs.configInput)
|
||||
) {
|
||||
return "the 'config' input may use queries that need library packs for other languages";
|
||||
}
|
||||
|
||||
// We can't tell which language a local query or a query from another repository is for without
|
||||
// loading it, and CodeQL resolves the library packs for every configured query, including those
|
||||
// for languages that aren't being analyzed.
|
||||
const query = findNonBuiltInQuery(
|
||||
parseQueriesFromInput(inputs.queriesInput).input,
|
||||
);
|
||||
if (query !== undefined) {
|
||||
return `the query '${query}' from the 'queries' input may need library packs for other languages`;
|
||||
}
|
||||
const extraQuery = findNonBuiltInQuery(
|
||||
parseQueriesFromInput(
|
||||
inputs.extraQueriesProperty,
|
||||
RepositoryPropertyName.EXTRA_QUERIES,
|
||||
).input,
|
||||
);
|
||||
if (extraQuery !== undefined) {
|
||||
return (
|
||||
`the query '${extraQuery}' from the '${RepositoryPropertyName.EXTRA_QUERIES}' repository ` +
|
||||
"property may need library packs for other languages"
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Returns the `uses` value of the first of `queries` that isn't a built-in query suite. */
|
||||
function findNonBuiltInQuery(
|
||||
queries: QuerySpec[] | undefined,
|
||||
): string | undefined {
|
||||
return queries?.find((query) => !defaultSuites.has(query.uses))?.uses;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
/**
|
||||
* Why the CodeQL CLI may need packs for other languages, for example because of the configured
|
||||
* queries, or `undefined` if it won't. If defined, the combined bundle is used, and this reason is
|
||||
* logged to complete the sentence "Not using a per-language CodeQL bundle since ...". See
|
||||
* `getOtherLanguagePacksReason`.
|
||||
*/
|
||||
otherLanguagePacksReason: 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,
|
||||
otherLanguagePacksReason,
|
||||
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 is disabled");
|
||||
}
|
||||
|
||||
// A defined reason means the CodeQL CLI may need packs for other languages, for example because
|
||||
// of the configured queries. That applies whichever languages were requested, so check it first
|
||||
// to avoid suggesting that requesting a single language would be enough.
|
||||
if (otherLanguagePacksReason !== undefined) {
|
||||
return explain(otherLanguagePacksReason);
|
||||
}
|
||||
|
||||
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,15 +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 (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
const initToolsDownloadFields = createInitToolsDownloadFields(
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
);
|
||||
|
||||
await sendStatusReport({ ...initStatusReport, ...initToolsDownloadFields });
|
||||
}
|
||||
@@ -113,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();
|
||||
@@ -171,6 +165,12 @@ async function run(
|
||||
gitHubVersion.type,
|
||||
codeQLDefaultVersionInfo,
|
||||
rawLanguages,
|
||||
// CodeQL resolves the dependencies of queries that aren't in compiled packs from the bundle,
|
||||
// so the queries that the workflow runs with this CLI may need library packs for languages
|
||||
// other than those in the `languages` input. That input therefore only informs the choice of
|
||||
// CLI version.
|
||||
"the 'setup-codeql' Action can't tell whether the queries that the workflow runs will need " +
|
||||
"library packs for other languages",
|
||||
analysisKinds.length === 1 &&
|
||||
analysisKinds[0] === AnalysisKind.CodeScanning,
|
||||
features,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user