mirror of
https://github.com/github/codeql-action.git
synced 2026-11-05 04:29:40 +00:00
Compare commits
193 Commits
mergeback/
...
mario-camp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a35a8ac9ef | ||
|
|
ecf4e52a5e | ||
|
|
0daa052859 | ||
|
|
ec96b5271e | ||
|
|
8f1212fe5c | ||
|
|
f133996fb5 | ||
|
|
0c2cea31ba | ||
|
|
3f1e4da48d | ||
|
|
89343ee1aa | ||
|
|
ba5fbf159a | ||
|
|
9a1225df61 | ||
|
|
aec5441507 | ||
|
|
f8ed33e067 | ||
|
|
ec0b8ae45f | ||
|
|
36ef889329 | ||
|
|
50d76e2fa3 | ||
|
|
6a62e99aa7 | ||
|
|
fb5048ee88 | ||
|
|
8096e4f957 | ||
|
|
85830a2768 | ||
|
|
29c712e0b7 | ||
|
|
ff973a1072 | ||
|
|
ed1bc3d57d | ||
|
|
ac331c28db | ||
|
|
9a9ab4cc7d | ||
|
|
d8866c2578 | ||
|
|
d4b99ad098 | ||
|
|
5110948a58 | ||
|
|
355aa4a77f | ||
|
|
81d0f34a44 | ||
|
|
d0a69d77dc | ||
|
|
a1e3f70a6f | ||
|
|
6b3ba17c47 | ||
|
|
da75ca58fa | ||
|
|
ab6fd520bd | ||
|
|
c77051d879 | ||
|
|
1a57d7ae11 | ||
|
|
6a782906dc | ||
|
|
a4319e3365 | ||
|
|
a8ac1e029c | ||
|
|
b571644f21 | ||
|
|
31842a7e06 | ||
|
|
c23977f57b | ||
|
|
0c7580ae89 | ||
|
|
37ea21c1fc | ||
|
|
4b164dbc17 | ||
|
|
97b52d19da | ||
|
|
3f2a29899f | ||
|
|
80dcb7ccdf | ||
|
|
a440e35eda | ||
|
|
be67a7be93 | ||
|
|
732911a2fa | ||
|
|
1d0e1b20e5 | ||
|
|
4c868b3e7e | ||
|
|
8d24507592 | ||
|
|
c7d9083db4 | ||
|
|
2831d9a0e9 | ||
|
|
258af3df60 | ||
|
|
d8168ab655 | ||
|
|
ac36b7d725 | ||
|
|
9fdfd3e4c8 | ||
|
|
1c42f21061 | ||
|
|
ef79560bc8 | ||
|
|
4581e395f3 | ||
|
|
76d37c509a | ||
|
|
0e974e6288 | ||
|
|
721b977126 | ||
|
|
329fc9cde0 | ||
|
|
f27c81bb1c | ||
|
|
99df26d4f1 | ||
|
|
31c27074fd | ||
|
|
72df2181aa | ||
|
|
c7c896d71b | ||
|
|
3f34ff0ea3 | ||
|
|
43bec09f1d | ||
|
|
f58f0d11eb | ||
|
|
7dc37cbb5b | ||
|
|
8e22350a7e | ||
|
|
69c9e8c7d9 | ||
|
|
73f4741039 | ||
|
|
5094019011 | ||
|
|
006d029c8d | ||
|
|
d309c0e332 | ||
|
|
e34d7fe5bf | ||
|
|
5fa9c0c3e2 | ||
|
|
eba9d55e1f | ||
|
|
ae4c8d057d | ||
|
|
73b0850a89 | ||
|
|
5e3e6b81b9 | ||
|
|
3cf0a529d8 | ||
|
|
de65da38e5 | ||
|
|
b16b719b76 | ||
|
|
37ae669b89 | ||
|
|
b987514f02 | ||
|
|
a04a87c9c9 | ||
|
|
54f647b7e1 | ||
|
|
e78819e055 | ||
|
|
c1e7c7a0de | ||
|
|
50bd53bfbc | ||
|
|
289efcad49 | ||
|
|
17a28bc4c5 | ||
|
|
cbf67da2fa | ||
|
|
4c18ab020b | ||
|
|
252ad66b0d | ||
|
|
d5b8046386 | ||
|
|
a8958a9883 | ||
|
|
1ba744977b | ||
|
|
e0f825ed66 | ||
|
|
fbcae9afa6 | ||
|
|
8f47ee793f | ||
|
|
67ec1e3914 | ||
|
|
1578ba0b4f | ||
|
|
ef29bf2e43 | ||
|
|
ceac4af134 | ||
|
|
772e8e9310 | ||
|
|
fcd9f6cbac | ||
|
|
a9ceac150d | ||
|
|
6136b93088 | ||
|
|
6ab762cada | ||
|
|
a3a1cdd43a | ||
|
|
9c16c3b47b | ||
|
|
b009eac559 | ||
|
|
26e9e9e932 | ||
|
|
9b35ab94e1 | ||
|
|
e1905475d1 | ||
|
|
2399df7427 | ||
|
|
2c9d3d63eb | ||
|
|
1f34ec1643 | ||
|
|
d5f0145480 | ||
|
|
f27f56386a | ||
|
|
0025d0f2b5 | ||
|
|
f7fa18f05d | ||
|
|
7f4f27e7e6 | ||
|
|
7d1c6e90aa | ||
|
|
6cdc6063c0 | ||
|
|
bba0d6c67c | ||
|
|
ffb3423848 | ||
|
|
da149812f1 | ||
|
|
0d698171d3 | ||
|
|
628fc3f124 | ||
|
|
708e106533 | ||
|
|
9cfb67bab9 | ||
|
|
b6be81d5b9 | ||
|
|
7b2af8940d | ||
|
|
f86aa521ce | ||
|
|
e446d55e5a | ||
|
|
c2f43799c5 | ||
|
|
41965b0e7b | ||
|
|
6f8864c8b0 | ||
|
|
d8d1d6d754 | ||
|
|
f77cf557cd | ||
|
|
e90a71aaea | ||
|
|
ad159d34f6 | ||
|
|
812b882dee | ||
|
|
3fe7ef97d3 | ||
|
|
8d69da902e | ||
|
|
00e5a58139 | ||
|
|
8102fa6675 | ||
|
|
81ad479e13 | ||
|
|
12821cff0c | ||
|
|
e537ff20a4 | ||
|
|
598d00854a | ||
|
|
85c8a8cebe | ||
|
|
c7a94c979d | ||
|
|
82e5ca6c55 | ||
|
|
9ef8be7176 | ||
|
|
07b6b1eac5 | ||
|
|
9c4ee01a28 | ||
|
|
652296eb9e | ||
|
|
c05a5b71ad | ||
|
|
b3c7aa7372 | ||
|
|
649bababe7 | ||
|
|
895ffe7eb5 | ||
|
|
805b59092e | ||
|
|
d1bd5ec7b0 | ||
|
|
94b12602ac | ||
|
|
126166c212 | ||
|
|
dd7ff30205 | ||
|
|
420c97eadd | ||
|
|
defcf1bff6 | ||
|
|
c8e32e423d | ||
|
|
553eef0d3f | ||
|
|
b18df17ee7 | ||
|
|
a602287f73 | ||
|
|
dc8e1e9aa0 | ||
|
|
889ae42672 | ||
|
|
587fcb33c7 | ||
|
|
445107e23a | ||
|
|
6010f85d81 | ||
|
|
311292c28f | ||
|
|
1023397ae4 | ||
|
|
68ec727966 | ||
|
|
cb4720a101 |
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__analyze-ref-input.yml
generated
vendored
2
.github/workflows/__analyze-ref-input.yml
generated
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__autobuild-action.yml
generated
vendored
2
.github/workflows/__autobuild-action.yml
generated
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Prepare test
|
||||
|
||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
2
.github/workflows/__build-mode-manual.yml
generated
vendored
2
.github/workflows/__build-mode-manual.yml
generated
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__export-file-baseline-information.yml
generated
vendored
2
.github/workflows/__export-file-baseline-information.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__go-custom-queries.yml
generated
vendored
2
.github/workflows/__go-custom-queries.yml
generated
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__local-bundle.yml
generated
vendored
2
.github/workflows/__local-bundle.yml
generated
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__multi-language-autodetect.yml
generated
vendored
2
.github/workflows/__multi-language-autodetect.yml
generated
vendored
@@ -101,7 +101,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__packaging-config-js.yml
generated
vendored
2
.github/workflows/__packaging-config-js.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__remote-config.yml
generated
vendored
2
.github/workflows/__remote-config.yml
generated
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
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@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
||||
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
2
.github/workflows/__split-workflow.yml
generated
vendored
2
.github/workflows/__split-workflow.yml
generated
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__swift-custom-build.yml
generated
vendored
2
.github/workflows/__swift-custom-build.yml
generated
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__unset-environment.yml
generated
vendored
2
.github/workflows/__unset-environment.yml
generated
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__upload-ref-sha-input.yml
generated
vendored
2
.github/workflows/__upload-ref-sha-input.yml
generated
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__upload-sarif.yml
generated
vendored
2
.github/workflows/__upload-sarif.yml
generated
vendored
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
2
.github/workflows/__with-checkout-path.yml
generated
vendored
2
.github/workflows/__with-checkout-path.yml
generated
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
with:
|
||||
go-version: ^1.13.1
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Assert best-effort artifact scan completed
|
||||
|
||||
2
.github/workflows/debug-artifacts-safe.yml
vendored
2
.github/workflows/debug-artifacts-safe.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
with:
|
||||
go-version: ^1.13.1
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Assert best-effort artifact scan completed
|
||||
|
||||
2
.github/workflows/test-codeql-bundle-all.yml
vendored
2
.github/workflows/test-codeql-bundle-all.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: true
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- id: init
|
||||
|
||||
@@ -4,6 +4,15 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956)
|
||||
|
||||
## 4.37.0 - 08 Jul 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
|
||||
- In addition to the existing input format, the `config-file` input for the `codeql-action/init` step will soon support a new `[owner/]repo[@ref][:path]` format. All components except the repository name are optional. If omitted, `owner` defaults to the same owner as the repository the analysis is running for, `ref` to `main`, and `path` to `.github/codeql-action.yaml`. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. [#3973](https://github.com/github/codeql-action/pull/3973)
|
||||
|
||||
## 4.36.3 - 01 Jul 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.36.2 - 04 Jun 2026
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
}
|
||||
|
||||
6856
lib/entry-points.js
generated
6856
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
212
package-lock.json
generated
212
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.36.3",
|
||||
"version": "4.37.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codeql",
|
||||
"version": "4.36.3",
|
||||
"version": "4.37.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks"
|
||||
@@ -28,12 +28,12 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.0",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.4",
|
||||
"uuid": "^14.0.0"
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -47,21 +47,21 @@
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^21.0.1",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.6.0",
|
||||
"nock": "^14.0.15",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.1"
|
||||
"typescript-eslint": "^8.62.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2274,13 +2274,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@package-json/types": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz",
|
||||
"integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
|
||||
@@ -2579,9 +2572,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sinon": {
|
||||
"version": "21.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz",
|
||||
"integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz",
|
||||
"integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2594,17 +2587,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
|
||||
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
|
||||
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/type-utils": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/type-utils": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2617,7 +2610,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.61.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -2633,16 +2626,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
|
||||
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
|
||||
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2676,14 +2669,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
|
||||
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
|
||||
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.61.1",
|
||||
"@typescript-eslint/types": "^8.61.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.62.1",
|
||||
"@typescript-eslint/types": "^8.62.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2716,14 +2709,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
|
||||
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
|
||||
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1"
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2734,9 +2727,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
|
||||
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2751,15 +2744,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
|
||||
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2794,9 +2787,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
|
||||
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
|
||||
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2808,16 +2801,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
|
||||
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
|
||||
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.61.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/visitor-keys": "8.61.1",
|
||||
"@typescript-eslint/project-service": "8.62.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2846,9 +2839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2893,16 +2886,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
|
||||
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
|
||||
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.61.1",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1"
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2917,13 +2910,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
|
||||
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
|
||||
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5132,13 +5125,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x": {
|
||||
"version": "4.16.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz",
|
||||
"integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==",
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz",
|
||||
"integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@package-json/types": "^0.0.12",
|
||||
"@typescript-eslint/types": "^8.56.0",
|
||||
"comment-parser": "^1.4.1",
|
||||
"debug": "^4.4.1",
|
||||
@@ -6143,9 +6135,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.6.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
|
||||
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
|
||||
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -6986,9 +6978,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz",
|
||||
"integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.0.tgz",
|
||||
"integrity": "sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7451,9 +7443,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "14.0.15",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.15.tgz",
|
||||
"integrity": "sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==",
|
||||
"version": "14.0.16",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.16.tgz",
|
||||
"integrity": "sha512-8r4KEc6nT1D/fdLD/R1BO1CPaVEL8o40u/guFRJlXabN7vr3RmMqyjsY5Krt0nMwhsOAwXQ/mtN5vy5Jh3aErg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -8367,9 +8359,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -8904,9 +8896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/supertap/node_modules/js-yaml": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
|
||||
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9325,16 +9317,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
|
||||
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
|
||||
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.61.1",
|
||||
"@typescript-eslint/parser": "8.61.1",
|
||||
"@typescript-eslint/typescript-estree": "8.61.1",
|
||||
"@typescript-eslint/utils": "8.61.1"
|
||||
"@typescript-eslint/eslint-plugin": "8.62.1",
|
||||
"@typescript-eslint/parser": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -9368,9 +9360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -9498,9 +9490,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
|
||||
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
|
||||
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
@@ -9818,7 +9810,7 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.36.3",
|
||||
"version": "4.37.1",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -36,12 +36,12 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.0.0",
|
||||
"js-yaml": "^5.2.0",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.4",
|
||||
"uuid": "^14.0.0"
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -55,21 +55,21 @@
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^21.0.1",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.0.0",
|
||||
"eslint-plugin-import-x": "^4.16.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.6.0",
|
||||
"nock": "^14.0.15",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.1"
|
||||
"typescript-eslint": "^8.62.1"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
|
||||
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.0",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"ad2b38190b15e4d6bdf0c97fb4fca8412226d287",
|
||||
"v5.3.0",
|
||||
"0f481fcb613427c0f801b606911222b5b6f3083a",
|
||||
"v5.5.0",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
@@ -288,8 +288,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install .NET",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-dotnet",
|
||||
"9a946fdbd5fb07b82b2f5a4466058b876ab72bb2",
|
||||
"v5.3.0",
|
||||
"26b0ec14cb23fa6904739307f278c14f94c95bf1",
|
||||
"v5.4.0",
|
||||
),
|
||||
with: {
|
||||
"dotnet-version": `\${{ inputs.dotnet-version || '${defaultLanguageVersions.csharp}' }}`,
|
||||
|
||||
95
src/action-common.ts
Normal file
95
src/action-common.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ActionsEnv, getActionsEnv } from "./actions-util";
|
||||
import { Env } from "./environment";
|
||||
import { FeatureEnablement } from "./feature-flags";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import {
|
||||
ActionName,
|
||||
getDisplayActionName,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage } from "./util";
|
||||
|
||||
/** Common state that is always available in `ActionState`. */
|
||||
export interface BaseState {
|
||||
/** The name of the Action. */
|
||||
name: ActionName;
|
||||
/** When the Action was started. */
|
||||
startedAt: Date;
|
||||
}
|
||||
|
||||
/** Describes different state features that an Action may have. */
|
||||
export interface FeatureState {
|
||||
Logger: {
|
||||
/** The logger that is in use. */
|
||||
logger: Logger;
|
||||
};
|
||||
Env: {
|
||||
/** Information about environment variables. */
|
||||
env: Env;
|
||||
};
|
||||
Actions: {
|
||||
/** Access to Actions-related functionality. */
|
||||
actions: ActionsEnv;
|
||||
};
|
||||
FeatureFlags: {
|
||||
/** Information about enabled feature flags. */
|
||||
features: FeatureEnablement;
|
||||
};
|
||||
}
|
||||
|
||||
/** Identifies a type of state an Action may have. */
|
||||
export type StateFeature = keyof FeatureState;
|
||||
|
||||
/** Constructs the intersection of all state types identifies by `Fs`. */
|
||||
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
|
||||
? BaseState
|
||||
: Fs extends [
|
||||
infer Head extends StateFeature,
|
||||
...infer Tail extends readonly StateFeature[],
|
||||
]
|
||||
? FeatureState[Head] & FieldsOf<Tail>
|
||||
: never;
|
||||
|
||||
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
|
||||
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
|
||||
|
||||
/** The type of an Action's main entry point. This is a function that is provided
|
||||
* with a basic `ActionState` object with features that are always available.
|
||||
* Each Action can then augment the `state` further if additional features are required.
|
||||
*/
|
||||
export type ActionMain = (
|
||||
state: ActionState<["Logger", "Env", "Actions"]>,
|
||||
) => Promise<void>;
|
||||
|
||||
/** A specification for a CodeQL Action step. */
|
||||
export interface Action {
|
||||
/** The name of the Action. */
|
||||
name: ActionName;
|
||||
/** The entry point for the Action. */
|
||||
run: ActionMain;
|
||||
}
|
||||
|
||||
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
|
||||
export async function runInActions(action: Action) {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
const env = getEnv();
|
||||
const actionsEnv = getActionsEnv();
|
||||
|
||||
try {
|
||||
await action.run({
|
||||
name: action.name,
|
||||
startedAt,
|
||||
logger,
|
||||
env,
|
||||
actions: actionsEnv,
|
||||
});
|
||||
} catch (error) {
|
||||
core.setFailed(
|
||||
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ 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 { Logger } from "./logging";
|
||||
import {
|
||||
doesDirectoryExist,
|
||||
getCodeQLDatabasePath,
|
||||
getRequiredEnvParam,
|
||||
ConfigurationError,
|
||||
getEnv,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -61,17 +62,21 @@ export const getOptionalInput = function (name: string): string | undefined {
|
||||
return value.length > 0 ? value : undefined;
|
||||
};
|
||||
|
||||
export function getTemporaryDirectory(): string {
|
||||
const value = process.env["CODEQL_ACTION_TEMP"];
|
||||
return value !== undefined && value !== ""
|
||||
? value
|
||||
: getRequiredEnvParam("RUNNER_TEMP");
|
||||
/**
|
||||
* Gets the temporary directory used by the CodeQL Action. This will either be the temporary
|
||||
* directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the
|
||||
* value of `RUNNER_TEMP` otherwise.
|
||||
*/
|
||||
export function getTemporaryDirectory(env: Env = getEnv()): string {
|
||||
return (
|
||||
env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP)
|
||||
);
|
||||
}
|
||||
|
||||
const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json";
|
||||
|
||||
export function getDiffRangesJsonFilePath(): string {
|
||||
return path.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME);
|
||||
export function getDiffRangesJsonFilePath(env: Env = getEnv()): string {
|
||||
return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME);
|
||||
}
|
||||
|
||||
export function getActionVersion(): string {
|
||||
@@ -83,16 +88,16 @@ export function getActionVersion(): string {
|
||||
*
|
||||
* This will be "dynamic" for default setup workflow runs.
|
||||
*/
|
||||
export function getWorkflowEventName() {
|
||||
return getRequiredEnvParam("GITHUB_EVENT_NAME");
|
||||
export function getWorkflowEventName(env: Env = getEnv()) {
|
||||
return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
|
||||
* a workflow on the codeql-action repo itself.
|
||||
*/
|
||||
export function isRunningLocalAction(): boolean {
|
||||
const relativeScriptPath = getRelativeScriptPath();
|
||||
export function isRunningLocalAction(env: Env = getEnv()): boolean {
|
||||
const relativeScriptPath = getRelativeScriptPath(env);
|
||||
return (
|
||||
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
|
||||
);
|
||||
@@ -103,15 +108,15 @@ export function isRunningLocalAction(): boolean {
|
||||
*
|
||||
* This can be used to get the Action's name or tell if we're running a local Action.
|
||||
*/
|
||||
function getRelativeScriptPath(): string {
|
||||
const runnerTemp = getRequiredEnvParam("RUNNER_TEMP");
|
||||
function getRelativeScriptPath(env: Env): string {
|
||||
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
|
||||
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
|
||||
return path.relative(actionsDirectory, __filename);
|
||||
}
|
||||
|
||||
/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
|
||||
export function getWorkflowEvent(): any {
|
||||
const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH");
|
||||
export function getWorkflowEvent(env: Env = getEnv()): any {
|
||||
const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH);
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));
|
||||
} catch (e) {
|
||||
@@ -180,17 +185,17 @@ export function getUploadValue(input: string | undefined): UploadKind {
|
||||
/**
|
||||
* Get the workflow run ID.
|
||||
*/
|
||||
export function getWorkflowRunID(): number {
|
||||
const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID");
|
||||
export function getWorkflowRunID(env: Env = getEnv()): number {
|
||||
const workflowRunIdString = env.getRequired(ActionsEnvVars.GITHUB_RUN_ID);
|
||||
const workflowRunID = parseInt(workflowRunIdString, 10);
|
||||
if (Number.isNaN(workflowRunID)) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ID} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
|
||||
);
|
||||
}
|
||||
if (workflowRunID < 0) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ID} must be a non-negative integer. Current value is ${workflowRunIdString}`,
|
||||
);
|
||||
}
|
||||
return workflowRunID;
|
||||
@@ -199,17 +204,19 @@ export function getWorkflowRunID(): number {
|
||||
/**
|
||||
* Get the workflow run attempt number.
|
||||
*/
|
||||
export function getWorkflowRunAttempt(): number {
|
||||
const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT");
|
||||
export function getWorkflowRunAttempt(env: Env = getEnv()): number {
|
||||
const workflowRunAttemptString = env.getRequired(
|
||||
ActionsEnvVars.GITHUB_RUN_ATTEMPT,
|
||||
);
|
||||
const workflowRunAttempt = parseInt(workflowRunAttemptString, 10);
|
||||
if (Number.isNaN(workflowRunAttempt)) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
|
||||
);
|
||||
}
|
||||
if (workflowRunAttempt <= 0) {
|
||||
throw new Error(
|
||||
`GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}`,
|
||||
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must be a positive integer. Current value is ${workflowRunAttemptString}`,
|
||||
);
|
||||
}
|
||||
return workflowRunAttempt;
|
||||
@@ -266,18 +273,18 @@ export const getFileType = async (filePath: string): Promise<string> => {
|
||||
}
|
||||
};
|
||||
|
||||
export function isSelfHostedRunner() {
|
||||
return process.env.RUNNER_ENVIRONMENT === "self-hosted";
|
||||
export function isSelfHostedRunner(env: Env = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
||||
}
|
||||
|
||||
/** Determines whether the workflow trigger is `dynamic`. */
|
||||
export function isDynamicWorkflow(): boolean {
|
||||
return getWorkflowEventName() === "dynamic";
|
||||
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
||||
return getWorkflowEventName(env) === "dynamic";
|
||||
}
|
||||
|
||||
/** Determines whether we are running in default setup. */
|
||||
export function isDefaultSetup(): boolean {
|
||||
return isDynamicWorkflow();
|
||||
export function isDefaultSetup(env: Env = getEnv()): boolean {
|
||||
return isDynamicWorkflow(env);
|
||||
}
|
||||
|
||||
export function prettyPrintInvocation(cmd: string, args: string[]): string {
|
||||
@@ -375,9 +382,10 @@ const persistedInputsKey = "persisted_inputs";
|
||||
* This would be simplified if actions/runner#3514 is addressed.
|
||||
* https://github.com/actions/runner/issues/3514
|
||||
*/
|
||||
export const persistInputs = function () {
|
||||
const inputEnvironmentVariables = Object.entries(process.env).filter(
|
||||
([name]) => name.startsWith("INPUT_"),
|
||||
export const persistInputs = function (env: Env = getEnv()) {
|
||||
const entries = env.entries();
|
||||
const inputEnvironmentVariables = entries.filter(([name]) =>
|
||||
name.startsWith("INPUT_"),
|
||||
);
|
||||
core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables));
|
||||
};
|
||||
@@ -405,7 +413,9 @@ export interface PullRequestBranches {
|
||||
* @returns the base and head branches of the pull request, or undefined if
|
||||
* we are not analyzing a pull request.
|
||||
*/
|
||||
export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
export function getPullRequestBranches(
|
||||
env: Env = getEnv(),
|
||||
): PullRequestBranches | undefined {
|
||||
const pullRequest = github.context.payload.pull_request;
|
||||
if (pullRequest) {
|
||||
return {
|
||||
@@ -419,8 +429,10 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
|
||||
// PR analysis under Default Setup does not have the pull_request context,
|
||||
// but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH.
|
||||
const codeScanningRef = process.env.CODE_SCANNING_REF;
|
||||
const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH;
|
||||
const codeScanningRef = env.getOptional(EnvVar.CODE_SCANNING_REF);
|
||||
const codeScanningBaseBranch = env.getOptional(
|
||||
EnvVar.CODE_SCANNING_BASE_BRANCH,
|
||||
);
|
||||
if (codeScanningRef && codeScanningBaseBranch) {
|
||||
return {
|
||||
base: codeScanningBaseBranch,
|
||||
@@ -435,8 +447,8 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
|
||||
/**
|
||||
* Returns whether we are analyzing a pull request.
|
||||
*/
|
||||
export function isAnalyzingPullRequest(): boolean {
|
||||
return getPullRequestBranches() !== undefined;
|
||||
export function isAnalyzingPullRequest(env: Env = getEnv()): boolean {
|
||||
return getPullRequestBranches(env) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,13 +472,14 @@ const qualityCategoryMapping: Record<string, string> = {
|
||||
export function fixCodeQualityCategory(
|
||||
logger: Logger,
|
||||
category?: string,
|
||||
env: Env = getEnv(),
|
||||
): string | undefined {
|
||||
// The `category` should always be set by Default Setup. We perform this check
|
||||
// to avoid potential issues if Code Quality supports Advanced Setup in the future
|
||||
// and before this workaround is removed.
|
||||
if (
|
||||
category !== undefined &&
|
||||
isDefaultSetup() &&
|
||||
isDefaultSetup(env) &&
|
||||
category.startsWith("/language:")
|
||||
) {
|
||||
const language = category.substring("/language:".length);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { performance } from "perf_hooks";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import {
|
||||
@@ -40,7 +41,6 @@ import {
|
||||
createStatusReportBase,
|
||||
DatabaseCreationTimings,
|
||||
getActionsStatus,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
} from "./status-report";
|
||||
import {
|
||||
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
|
||||
await runAutobuild(config, BuiltInLanguage.go, logger);
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
@@ -228,7 +228,6 @@ async function run(startedAt: Date) {
|
||||
let didUploadTrapCaches = false;
|
||||
let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined;
|
||||
let databaseUploadResults: DatabaseUploadResult[] = [];
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
util.initializeEnvironment(actionsUtil.getActionVersion());
|
||||
@@ -523,19 +522,13 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the `analyze` Action. */
|
||||
const analyze: Action = {
|
||||
name: ActionName.Analyze,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`analyze action failed: ${util.getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Analyze,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(analyze);
|
||||
await util.checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ extensions:
|
||||
// characters are escaped, and that the path is always rendered as a
|
||||
// quoted string on a single line.
|
||||
return (
|
||||
` - [${yaml.dump(filename, { quoteStyle: "single" }).trim()}, ` +
|
||||
` - [${yaml.dump(filename, { forceQuotes: true, quoteStyle: "single" }).trim()}, ` +
|
||||
`${range.startLine}, ${range.endLine}]\n`
|
||||
);
|
||||
})
|
||||
|
||||
@@ -6,7 +6,8 @@ import * as sinon from "sinon";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { DO_NOT_RETRY_STATUSES } from "./api-client";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { getTestEnv, setupTests } from "./testing-utils";
|
||||
import * as util from "./util";
|
||||
|
||||
setupTests(test);
|
||||
@@ -20,16 +21,13 @@ test.serial("getApiClient", async (t) => {
|
||||
const githubStub: sinon.SinonStub = sinon.stub();
|
||||
pluginStub.returns(githubStub);
|
||||
|
||||
sinon.stub(actionsUtil, "getRequiredInput").withArgs("token").returns("xyz");
|
||||
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
|
||||
requiredEnvParamStub
|
||||
.withArgs("GITHUB_SERVER_URL")
|
||||
.returns("http://github.localhost");
|
||||
requiredEnvParamStub
|
||||
.withArgs("GITHUB_API_URL")
|
||||
.returns("http://api.github.localhost");
|
||||
const env = getTestEnv();
|
||||
env.set(ActionsEnvVars.GITHUB_SERVER_URL, "http://github.localhost");
|
||||
env.set(ActionsEnvVars.GITHUB_API_URL, "http://api.github.localhost");
|
||||
|
||||
api.getApiClient();
|
||||
sinon.stub(actionsUtil, "getRequiredInput").withArgs("token").returns("xyz");
|
||||
|
||||
api.getApiClient(env);
|
||||
|
||||
t.assert(
|
||||
githubStub.calledOnceWithExactly({
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as githubUtils from "@actions/github/lib/utils";
|
||||
import * as retry from "@octokit/plugin-retry";
|
||||
|
||||
import { getActionVersion, getRequiredInput } from "./actions-util";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, ReadOnlyEnv, ActionsEnvVars, getEnv } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo, RepositoryNwo } from "./repository";
|
||||
import {
|
||||
@@ -67,16 +67,16 @@ function createApiClientWithDetails(
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiDetails(): GitHubApiDetails {
|
||||
export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
|
||||
return {
|
||||
auth: getRequiredInput("token"),
|
||||
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
|
||||
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
|
||||
url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
|
||||
apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL),
|
||||
};
|
||||
}
|
||||
|
||||
export function getApiClient() {
|
||||
return createApiClientWithDetails(getApiDetails());
|
||||
export function getApiClient(env: ReadOnlyEnv = getEnv()) {
|
||||
return createApiClientWithDetails(getApiDetails(env));
|
||||
}
|
||||
|
||||
export function getApiClientWithExternalAuth(
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.16"}
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -11,13 +12,12 @@ import { getCodeQL } from "./codeql";
|
||||
import { Config, getConfig } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Language } from "./languages";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import {
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
ActionName,
|
||||
} from "./status-report";
|
||||
import { endTracingForCluster } from "./tracer-config";
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
checkDiskUsage,
|
||||
checkGitHubVersionInRange,
|
||||
ConfigurationError,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
wrapError,
|
||||
} from "./util";
|
||||
@@ -69,11 +68,10 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
let config: Config | undefined;
|
||||
let currentLanguage: Language | undefined;
|
||||
let languages: Language[] | undefined;
|
||||
@@ -142,18 +140,12 @@ async function run(startedAt: Date) {
|
||||
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
|
||||
}
|
||||
|
||||
/** Defines the `autobuild` Action. */
|
||||
const autobuild: Action = {
|
||||
name: ActionName.Autobuild,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`autobuild action failed. ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Autobuild,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(autobuild);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getGitHubVersion } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
import * as configUtils from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { ActionsEnvVars, EnvVar } from "./environment";
|
||||
import { Feature, featureConfig, initFeatures } from "./feature-flags";
|
||||
import { BuiltInLanguage, Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
@@ -126,7 +126,7 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
|
||||
if (await features.getValue(Feature.CppDependencyInstallation, codeql)) {
|
||||
// disable autoinstall on self-hosted runners unless explicitly requested
|
||||
if (
|
||||
process.env["RUNNER_ENVIRONMENT"] === "self-hosted" &&
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] === "self-hosted" &&
|
||||
process.env[envVar] !== "true"
|
||||
) {
|
||||
logger.info(
|
||||
|
||||
220
src/cli/output-cache.test.ts
Normal file
220
src/cli/output-cache.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { setupTests } from "../testing-utils";
|
||||
|
||||
import {
|
||||
cacheCommandOutput,
|
||||
CommandCacheKey,
|
||||
getCachedCommandOutput,
|
||||
resetCachedCommandOutputs,
|
||||
type VersionInfo,
|
||||
} from "./output-cache";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
||||
|
||||
/**
|
||||
* Runs `body` with a temporary directory configured as the cache's backing
|
||||
* store (`RUNNER_TEMP`). `CODEQL_ACTION_TEMP` is cleared so that
|
||||
* `getTemporaryDirectory()` falls back to `RUNNER_TEMP`.
|
||||
*
|
||||
* `setupTests` snapshots and restores `process.env` around every test, so we
|
||||
* don't restore the environment variables we set here ourselves.
|
||||
*/
|
||||
async function withCacheDir(
|
||||
body: (cacheFilePath: string) => Promise<void> | void,
|
||||
): Promise<void> {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cache-test-"));
|
||||
process.env["RUNNER_TEMP"] = tmpDir;
|
||||
delete process.env["CODEQL_ACTION_TEMP"];
|
||||
resetCachedCommandOutputs();
|
||||
try {
|
||||
await body(path.join(tmpDir, COMMAND_CACHE_FILENAME));
|
||||
} finally {
|
||||
await fs.promises.rm(tmpDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function writeCacheFile(
|
||||
cacheFilePath: string,
|
||||
contents: Record<string, unknown>,
|
||||
): void {
|
||||
fs.writeFileSync(cacheFilePath, JSON.stringify(contents));
|
||||
}
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput reuses an output persisted by an earlier step",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: {
|
||||
cmd: "/path/to/codeql",
|
||||
output: { version: "2.20.0" },
|
||||
},
|
||||
});
|
||||
t.deepEqual(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
{ version: "2.20.0" },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput ignores an output persisted from a different CLI",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: {
|
||||
cmd: "/path/to/other-codeql",
|
||||
output: { version: "2.20.0" },
|
||||
},
|
||||
});
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput ignores a malformed cache file",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
fs.writeFileSync(cacheFilePath, "not valid json");
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput returns undefined when there is no cache file",
|
||||
async (t) => {
|
||||
await withCacheDir(() => {
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput ignores an output that fails validation",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
for (const output of [
|
||||
{},
|
||||
{ version: 2 },
|
||||
{ version: "2.20.0", overlayVersion: "1" },
|
||||
{ version: "2.20.0", features: "nope" },
|
||||
]) {
|
||||
resetCachedCommandOutputs();
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: { cmd: "/path/to/codeql", output },
|
||||
});
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
JSON.stringify(output),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput ignores an entry missing the cmd field",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: { output: { version: "2.20.0" } },
|
||||
});
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("cacheCommandOutput persists the output to the memo", async (t) => {
|
||||
await withCacheDir(() => {
|
||||
const output: VersionInfo = { version: "2.20.0" };
|
||||
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
|
||||
|
||||
// Tier 1: the value is immediately available from the memo.
|
||||
t.deepEqual(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
output,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput treats a memoized output from another CLI as a miss",
|
||||
async (t) => {
|
||||
await withCacheDir(() => {
|
||||
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
|
||||
version: "2.20.0",
|
||||
});
|
||||
t.is(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput prefers the in-memory memo over the file",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
const output: VersionInfo = { version: "2.20.0", overlayVersion: 1 };
|
||||
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
|
||||
|
||||
// Overwrite the file with a different value; the memo (tier 1) should win.
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: {
|
||||
cmd: "/path/to/codeql",
|
||||
output: { version: "2.21.0" },
|
||||
},
|
||||
});
|
||||
t.deepEqual(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
output,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCommandOutput falls back to file when memoized output comes from another CLI",
|
||||
async (t) => {
|
||||
await withCacheDir((cacheFilePath) => {
|
||||
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
|
||||
version: "2.19.0",
|
||||
});
|
||||
writeCacheFile(cacheFilePath, {
|
||||
[CommandCacheKey.Version]: {
|
||||
cmd: "/path/to/codeql",
|
||||
output: { version: "2.20.0", overlayVersion: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
|
||||
{ version: "2.20.0", overlayVersion: 1 },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
238
src/cli/output-cache.ts
Normal file
238
src/cli/output-cache.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import * as json from "../json";
|
||||
import { getActionsLogger } from "../logging";
|
||||
|
||||
/**
|
||||
* The name of the temporary file that backs the on-disk cache of
|
||||
* CLI responses between workflow steps.
|
||||
*/
|
||||
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
||||
|
||||
/** A key used to identify cached command output. */
|
||||
export enum CommandCacheKey {
|
||||
Version = "version",
|
||||
ResolveLanguages = "resolve languages",
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
|
||||
/** Returns true if `x` is a {@link VersionInfo}. */
|
||||
export function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const isBooleanRecord = (obj: unknown): obj is Record<string, boolean> =>
|
||||
json.isObject(obj) &&
|
||||
Object.values(obj).every((val) => typeof val === "boolean");
|
||||
|
||||
return (
|
||||
json.isObject(x) &&
|
||||
json.validateSchema(
|
||||
{
|
||||
version: json.string,
|
||||
features: {
|
||||
validate: isBooleanRecord,
|
||||
check: (obj) => ({
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
valid: isBooleanRecord(obj),
|
||||
}),
|
||||
required: false,
|
||||
},
|
||||
overlayVersion: json.optional(json.number),
|
||||
},
|
||||
x,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export interface ResolveLanguagesOutput {
|
||||
aliases?: {
|
||||
[alias: string]: string;
|
||||
};
|
||||
extractors: {
|
||||
[language: string]: Array<{
|
||||
extractor_root: string;
|
||||
extractor_options?: any;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns true if `x` is a {@link ResolveLanguagesOutput}. */
|
||||
export function isResolveLanguagesOutput(
|
||||
x: unknown,
|
||||
): x is ResolveLanguagesOutput {
|
||||
return (
|
||||
json.isObject<ResolveLanguagesOutput>(x) &&
|
||||
json.isObject(x.extractors) &&
|
||||
Object.values(x.extractors).every(
|
||||
(extractorList) =>
|
||||
json.isArray(extractorList) &&
|
||||
extractorList.every(
|
||||
(extractor) =>
|
||||
json.isObject<{ extractor_root: unknown }>(extractor) &&
|
||||
json.isString(extractor.extractor_root),
|
||||
),
|
||||
) &&
|
||||
(x.aliases === undefined ||
|
||||
(json.isObject(x.aliases) &&
|
||||
Object.values(x.aliases).every((alias) => json.isString(alias))))
|
||||
);
|
||||
}
|
||||
|
||||
export type CommandCacheKeyOutputMap = {
|
||||
[CommandCacheKey.Version]: VersionInfo;
|
||||
[CommandCacheKey.ResolveLanguages]: ResolveLanguagesOutput;
|
||||
};
|
||||
|
||||
const commandCacheValidators: {
|
||||
[K in CommandCacheKey]: (
|
||||
output: unknown,
|
||||
) => output is CommandCacheKeyOutputMap[K];
|
||||
} = {
|
||||
[CommandCacheKey.Version]: isVersionInfo,
|
||||
[CommandCacheKey.ResolveLanguages]: isResolveLanguagesOutput,
|
||||
};
|
||||
|
||||
interface StoredCommandCacheEntry {
|
||||
cmd: string;
|
||||
output: unknown;
|
||||
}
|
||||
|
||||
/** A single cached command output together with the CLI path it came from. */
|
||||
export interface CommandCacheEntry<K extends CommandCacheKey> {
|
||||
/**
|
||||
* The path to the CodeQL CLI that produced `output`. Persisted so that a
|
||||
* different step using a different CodeQL bundle doesn't pick up a stale
|
||||
* value.
|
||||
*/
|
||||
cmd: string;
|
||||
output: CommandCacheKeyOutputMap[K];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 1: the in-process memo. Consulted first on every lookup and populated
|
||||
* whenever a value is read from the file (tier 2) or computed via the CLI
|
||||
* (tier 3).
|
||||
*/
|
||||
const inMemoryCache = new Map<
|
||||
CommandCacheKey,
|
||||
CommandCacheEntry<CommandCacheKey>
|
||||
>();
|
||||
const logger = getActionsLogger();
|
||||
|
||||
function getCommandCacheFilePath(): string {
|
||||
return path.join(getTemporaryDirectory(), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the temporary cache file. Best-effort: a missing, malformed,
|
||||
* or otherwise unreadable file is treated as an empty cache.
|
||||
*/
|
||||
function readCommandCacheFile(): Record<string, StoredCommandCacheEntry> {
|
||||
if (!fs.existsSync(getCommandCacheFilePath())) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const contents = fs.readFileSync(getCommandCacheFilePath(), "utf8");
|
||||
const parsed = json.parseString(contents);
|
||||
if (json.isObject(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warning(`Failed to read or parse command cache file: ${e}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the in-memory cache to the temporary file. Best-effort: a failure to write
|
||||
* just means a later step re-runs the CLI.
|
||||
*/
|
||||
export function writeCommandCacheFile(): void {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
getCommandCacheFilePath(),
|
||||
JSON.stringify(Object.fromEntries(inMemoryCache)),
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warning(`Failed to write command cache file: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the output of a CLI command under `key` in a module-global object.
|
||||
*/
|
||||
export function cacheCommandOutput<K extends CommandCacheKey>(
|
||||
key: K,
|
||||
cmd: string,
|
||||
output: CommandCacheKeyOutputMap[K],
|
||||
): void {
|
||||
const entry: CommandCacheEntry<K> = { cmd, output };
|
||||
inMemoryCache.set(key, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached output for `key`, or `undefined` if it isn't cached.
|
||||
*
|
||||
* Resolves tier 1 (in-memory memo) first, then tier 2 (temporary file). A value
|
||||
* loaded from the file is ignored unless its `cmd` matches the optional `cmd`
|
||||
* argument and its output satisfies the internal validator for `key`; valid
|
||||
* values are memoized into tier 1 before being returned.
|
||||
*
|
||||
* A return value of `undefined` signals the caller to fall back to tier 3 (the
|
||||
* CLI).
|
||||
*/
|
||||
export function getCachedCommandOutput<K extends CommandCacheKey>(
|
||||
key: K,
|
||||
cmd?: string,
|
||||
): CommandCacheKeyOutputMap[K] | undefined {
|
||||
// Tier 1: the in-memory variable.
|
||||
const memoized = inMemoryCache.get(key);
|
||||
if (memoized !== undefined) {
|
||||
if (cmd === undefined || memoized.cmd === cmd) {
|
||||
return memoized.output as CommandCacheKeyOutputMap[K];
|
||||
}
|
||||
// If the memoized entry doesn't match the requested CLI,
|
||||
// treat it as a miss and fall back to tier 2, the file.
|
||||
inMemoryCache.delete(key);
|
||||
}
|
||||
|
||||
// Tier 2: the temporary file persisted by an earlier step, if any.
|
||||
const entry = readCommandCacheFile()[key] as unknown;
|
||||
if (
|
||||
!json.isObject<StoredCommandCacheEntry>(entry) ||
|
||||
!json.isString(entry.cmd) ||
|
||||
(cmd !== undefined && entry.cmd !== cmd) ||
|
||||
!commandCacheValidators[key](entry.output)
|
||||
) {
|
||||
logger.warning("Received invalid data from the command-cache file.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Memoize so subsequent lookups in this process hit tier 1.
|
||||
const cachedEntry = entry as StoredCommandCacheEntry;
|
||||
const output = cachedEntry.output as CommandCacheKeyOutputMap[K];
|
||||
cacheCommandOutput(key, cachedEntry.cmd, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the in-process memo (tier 1). Only for use in tests, which exercise
|
||||
* multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCommandOutputs(): void {
|
||||
inMemoryCache.clear();
|
||||
}
|
||||
153
src/codeql.ts
153
src/codeql.ts
@@ -12,6 +12,14 @@ import {
|
||||
runTool,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import {
|
||||
cacheCommandOutput,
|
||||
CommandCacheKey,
|
||||
getCachedCommandOutput,
|
||||
type CommandCacheKeyOutputMap,
|
||||
type ResolveLanguagesOutput,
|
||||
type VersionInfo,
|
||||
} from "./cli/output-cache";
|
||||
import { CliError, wrapCliConfigurationError } from "./cli-errors";
|
||||
import { appendExtraQueryExclusions, type Config } from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
@@ -216,36 +224,10 @@ export interface CodeQL {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
|
||||
export interface ResolveDatabaseOutput {
|
||||
overlayBaseSpecifier?: string;
|
||||
}
|
||||
|
||||
export interface ResolveLanguagesOutput {
|
||||
aliases?: {
|
||||
[alias: string]: string;
|
||||
};
|
||||
extractors: {
|
||||
[language: string]: Array<{
|
||||
extractor_root: string;
|
||||
extractor_options?: any;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResolveBuildEnvironmentOutput {
|
||||
configuration?: {
|
||||
[language: string]: {
|
||||
@@ -272,17 +254,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.19.4";
|
||||
const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7";
|
||||
|
||||
/**
|
||||
* This is the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15";
|
||||
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16";
|
||||
|
||||
/**
|
||||
* This is the deprecation date for the version of GHES that was most recently deprecated.
|
||||
*/
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09";
|
||||
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
@@ -495,6 +477,29 @@ export async function getCodeQLForTesting(
|
||||
return getCodeQLForCmd(cmd, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached output for a `codeql` command, resolving through the three
|
||||
* caching tiers in order: the in-memory memo, then the temporary file (both via
|
||||
* {@link getCachedCommandOutput}), and finally the CLI itself by invoking `run`
|
||||
* on a miss and persisting its result back into the first two tiers.
|
||||
*
|
||||
* @param key The cache key identifying the command's output.
|
||||
* @param cmd The path to the CodeQL CLI the output is obtained from.
|
||||
* @param run Invokes the CLI to compute the output when it isn't cached.
|
||||
*/
|
||||
async function getCachedOrRun<K extends CommandCacheKey>(
|
||||
key: K,
|
||||
cmd: string,
|
||||
run: () => Promise<CommandCacheKeyOutputMap[K]>,
|
||||
): Promise<CommandCacheKeyOutputMap[K]> {
|
||||
let result = getCachedCommandOutput(key, cmd);
|
||||
if (result === undefined) {
|
||||
result = await run();
|
||||
cacheCommandOutput(key, cmd, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a CodeQL object for CodeQL CLI access.
|
||||
*
|
||||
@@ -512,21 +517,11 @@ async function getCodeQLForCmd(
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = util.getCachedCodeQlVersion(cmd);
|
||||
if (result === undefined) {
|
||||
const output = await runCli(cmd, ["version", "--format=json"], {
|
||||
return getCachedOrRun(CommandCacheKey.Version, cmd, () => {
|
||||
return runCliJson<VersionInfo>(cmd, ["version", "--format=json"], {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
try {
|
||||
result = JSON.parse(output) as VersionInfo;
|
||||
} catch {
|
||||
throw Error(
|
||||
`Invalid JSON output from \`version --format=json\`: ${output}`,
|
||||
);
|
||||
}
|
||||
util.cacheCodeQlVersion(cmd, result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
async printVersion() {
|
||||
// Reuse the cached version information rather than invoking the CLI again.
|
||||
@@ -731,26 +726,20 @@ async function getCodeQLForCmd(
|
||||
filterToLanguagesWithQueries: boolean;
|
||||
} = { filterToLanguagesWithQueries: false },
|
||||
) {
|
||||
const codeqlArgs = [
|
||||
return runCliJson<ResolveLanguagesOutput>(cmd, [
|
||||
"resolve",
|
||||
"languages",
|
||||
"--format=betterjson",
|
||||
"--extractor-options-verbosity=4",
|
||||
"--extractor-include-aliases",
|
||||
// TODO: Unconditionally include `--filter-to-languages-with-queries`
|
||||
// once CODEQL_MINIMUM_VERSION is at least v2.23.0
|
||||
// — the first version to support this flag.
|
||||
...(filterToLanguagesWithQueries
|
||||
? ["--filter-to-languages-with-queries"]
|
||||
: []),
|
||||
...getExtraOptionsFromEnv(["resolve", "languages"]),
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveLanguagesOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
|
||||
);
|
||||
}
|
||||
]);
|
||||
},
|
||||
async resolveBuildEnvironment(
|
||||
workingDir: string | undefined,
|
||||
@@ -766,15 +755,7 @@ async function getCodeQLForCmd(
|
||||
if (workingDir !== undefined) {
|
||||
codeqlArgs.push("--working-dir", workingDir);
|
||||
}
|
||||
const output = await runCli(cmd, codeqlArgs);
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveBuildEnvironmentOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve build-environment: ${e} in\n${output}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<ResolveBuildEnvironmentOutput>(cmd, codeqlArgs);
|
||||
},
|
||||
async databaseRunQueries(
|
||||
databasePath: string,
|
||||
@@ -976,15 +957,9 @@ async function getCodeQLForCmd(
|
||||
...getExtraOptionsFromEnv(["resolve", "queries"]),
|
||||
...queries,
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as string[];
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve queries --format=startingpacks: ${e}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<string[]>(cmd, codeqlArgs, {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
},
|
||||
async resolveDatabase(
|
||||
databasePath: string,
|
||||
@@ -996,15 +971,9 @@ async function getCodeQLForCmd(
|
||||
"--format=json",
|
||||
...getExtraOptionsFromEnv(["resolve", "database"]),
|
||||
];
|
||||
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
|
||||
|
||||
try {
|
||||
return JSON.parse(output) as ResolveDatabaseOutput;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unexpected output from codeql resolve database --format=json: ${e}`,
|
||||
);
|
||||
}
|
||||
return await runCliJson<ResolveDatabaseOutput>(cmd, codeqlArgs, {
|
||||
noStreamStdout: true,
|
||||
});
|
||||
},
|
||||
async mergeResults(
|
||||
sarifFiles: string[],
|
||||
@@ -1160,6 +1129,30 @@ async function runCli(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the command executor {@link runCli} and tries to parse the output as JSON.
|
||||
* @param cmd The command to run.
|
||||
* @param args The arguments to pass to the command.
|
||||
* @param opts The options for running the command.
|
||||
* @param opts.stdin Optional string to pass to the command's standard input.
|
||||
* @param opts.noStreamStdout Optional boolean to indicate whether to stream the command's standard output.
|
||||
* @returns The parsed JSON output from the command.
|
||||
*/
|
||||
async function runCliJson<T>(
|
||||
cmd: string,
|
||||
args: string[] = [],
|
||||
opts: { stdin?: string; noStreamStdout?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const output = await runCli(cmd, args, opts);
|
||||
try {
|
||||
return JSON.parse(output) as T;
|
||||
} catch (e) {
|
||||
throw Error(
|
||||
`Unexpected output from codeql ${args.join(" ")}: ${getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the code scanning configuration that is to be used by the CLI.
|
||||
*
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
mockCodeQLVersion,
|
||||
createTestConfig,
|
||||
makeMacro,
|
||||
initAllState,
|
||||
} from "./testing-utils";
|
||||
import {
|
||||
GitHubVariant,
|
||||
@@ -160,8 +161,9 @@ test.serial("load empty config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
@@ -202,8 +204,9 @@ test.serial("load code quality config", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -280,9 +283,10 @@ test.serial(
|
||||
repositoryProperties,
|
||||
});
|
||||
|
||||
const state = initAllState({ logger });
|
||||
await t.notThrowsAsync(async () => {
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languagesInput: languages,
|
||||
@@ -321,8 +325,9 @@ test.serial("loading a saved config produces the same config", async (t) => {
|
||||
// Sanity check that getConfig returns undefined before we have called initConfig
|
||||
t.deepEqual(await configUtils.getConfig(tempDir, logger), undefined);
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config1 = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -373,8 +378,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
.stub(actionsUtil, "getActionVersion")
|
||||
.returns("does-not-exist");
|
||||
|
||||
const state = initAllState({ logger });
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput: "javascript,python",
|
||||
tempDir,
|
||||
@@ -402,8 +408,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
|
||||
test.serial("load input outside of workspace", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: "../input",
|
||||
tempDir,
|
||||
@@ -424,34 +431,6 @@ test.serial("load input outside of workspace", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("load non-local input with invalid repo syntax", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
// no filename given, just a repo
|
||||
const configFile = "octo-org/codeql-config@main";
|
||||
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
createTestInitConfigInputs({
|
||||
configFile,
|
||||
tempDir,
|
||||
workspacePath: tempDir,
|
||||
}),
|
||||
);
|
||||
throw new Error("initConfig did not throw error");
|
||||
} catch (err) {
|
||||
t.deepEqual(
|
||||
err,
|
||||
new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(
|
||||
"octo-org/codeql-config@main",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("load non-existent input", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const languagesInput = "javascript";
|
||||
@@ -459,8 +438,9 @@ test.serial("load non-existent input", async (t) => {
|
||||
t.false(fs.existsSync(path.join(tempDir, configFile)));
|
||||
|
||||
try {
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -536,8 +516,9 @@ test.serial("load non-empty input", async (t) => {
|
||||
const languagesInput = "javascript";
|
||||
const configFilePath = createConfigFile(inputFileContents, tempDir);
|
||||
|
||||
const state = initAllState();
|
||||
const actualConfig = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
buildModeInput: "none",
|
||||
@@ -595,8 +576,9 @@ test.serial(
|
||||
// Only JS, python packs will be ignored
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState();
|
||||
const config = await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile: configFilePath,
|
||||
@@ -647,8 +629,9 @@ test.serial("API client used when reading remote config", async (t) => {
|
||||
const configFile = "octo-org/codeql-config/config.yaml@main";
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState();
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
configFile,
|
||||
@@ -669,9 +652,10 @@ test.serial(
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -699,9 +683,10 @@ test.serial("Invalid format of remote config handled correctly", async (t) => {
|
||||
mockGetContents(dummyResponse);
|
||||
|
||||
const repoReference = "octo-org/codeql-config/config.yaml@main";
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
configFile: repoReference,
|
||||
tempDir,
|
||||
@@ -729,9 +714,10 @@ test.serial("No detected languages", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
tempDir,
|
||||
codeql,
|
||||
@@ -752,9 +738,10 @@ test.serial("Unknown languages", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const languagesInput = "rubbish,english";
|
||||
|
||||
const state = initAllState();
|
||||
try {
|
||||
await configUtils.initConfig(
|
||||
createFeatures([]),
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
languagesInput,
|
||||
tempDir,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { performance } from "perf_hooks";
|
||||
import * as core from "@actions/core";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -18,8 +19,9 @@ import {
|
||||
getAnalysisConfig,
|
||||
} from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind, getCachingKind } from "./caching-utils";
|
||||
import { getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config/action-config";
|
||||
import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
@@ -27,6 +29,12 @@ import {
|
||||
parseUserConfig,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import { getRemoteConfig } from "./config/file";
|
||||
import {
|
||||
parseRegistries,
|
||||
type RegistryConfigNoCredentials,
|
||||
type RegistryConfigWithCredentials,
|
||||
} from "./config/pack-registries";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
@@ -79,6 +87,8 @@ import {
|
||||
isHostedRunner,
|
||||
} from "./util";
|
||||
|
||||
export { type Config } from "./config/action-config";
|
||||
|
||||
/**
|
||||
* The minimum available disk space (in MB) required to perform overlay analysis.
|
||||
* If the available disk space on the runner is below the threshold when deciding
|
||||
@@ -117,148 +127,6 @@ const OVERLAY_MINIMUM_MEMORY_MB = 5 * 1024;
|
||||
*/
|
||||
const CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE = "2.24.3";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format of the parsed config file.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
|
||||
async function getSupportedLanguageMap(
|
||||
codeql: CodeQL,
|
||||
logger: Logger,
|
||||
@@ -599,12 +467,11 @@ async function downloadCacheWithTime(
|
||||
}
|
||||
|
||||
async function loadUserConfig(
|
||||
logger: Logger,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
workspacePath: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
tempDir: string,
|
||||
validateConfig: boolean,
|
||||
): Promise<UserConfig> {
|
||||
if (isLocal(configFile)) {
|
||||
if (configFile !== userConfigFromActionPath(tempDir)) {
|
||||
@@ -617,14 +484,12 @@ async function loadUserConfig(
|
||||
);
|
||||
}
|
||||
}
|
||||
return getLocalConfig(logger, configFile, validateConfig);
|
||||
} else {
|
||||
return await getRemoteConfig(
|
||||
logger,
|
||||
configFile,
|
||||
apiDetails,
|
||||
validateConfig,
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return getLocalConfig(actionState.logger, configFile, validateConfig);
|
||||
} else {
|
||||
return await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1138,10 +1003,11 @@ export async function applyIncrementalAnalysisSettings(
|
||||
* a default config. The parsed config is then stored to a known location.
|
||||
*/
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<Config> {
|
||||
const { logger, tempDir } = inputs;
|
||||
const { logger, features } = actionState;
|
||||
const { tempDir } = inputs;
|
||||
|
||||
// if configInput is set, it takes precedence over configFile
|
||||
if (inputs.configInput) {
|
||||
@@ -1160,14 +1026,12 @@ export async function initConfig(
|
||||
logger.debug("No configuration file was provided");
|
||||
} else {
|
||||
logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
|
||||
userConfig = await loadUserConfig(
|
||||
logger,
|
||||
actionState,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1317,29 +1181,6 @@ export async function initConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
}
|
||||
|
||||
function isLocal(configPath: string): boolean {
|
||||
// If the path starts with ./, look locally
|
||||
if (configPath.indexOf("./") === 0) {
|
||||
@@ -1369,54 +1210,6 @@ function getLocalConfig(
|
||||
);
|
||||
}
|
||||
|
||||
async function getRemoteConfig(
|
||||
logger: Logger,
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
validateConfig: boolean,
|
||||
): Promise<UserConfig> {
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
|
||||
);
|
||||
const pieces = format.exec(configFile);
|
||||
// 5 = 4 groups + the whole expression
|
||||
if (pieces?.groups === undefined || pieces.length < 5) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const response = await api
|
||||
.getApiClientWithExternalAuth(apiDetails)
|
||||
.rest.repos.getContent({
|
||||
owner: pieces.groups.owner,
|
||||
repo: pieces.groups.repo,
|
||||
path: pieces.groups.path,
|
||||
ref: pieces.groups.ref,
|
||||
});
|
||||
|
||||
let fileContents: string;
|
||||
if ("content" in response.data && response.data.content !== undefined) {
|
||||
fileContents = response.data.content;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
|
||||
);
|
||||
} else {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
return parseUserConfig(
|
||||
logger,
|
||||
configFile,
|
||||
Buffer.from(fileContents, "base64").toString("binary"),
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path where the parsed config will be stored.
|
||||
*/
|
||||
|
||||
128
src/config/action-config.ts
Normal file
128
src/config/action-config.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import type { AnalysisKind } from "../analyses";
|
||||
import type { CachingKind } from "../caching-utils";
|
||||
import type { RepositoryProperties } from "../feature-flags/properties";
|
||||
import type { Language } from "../languages";
|
||||
import type { OverlayDatabaseMode } from "../overlay/overlay-database-mode";
|
||||
import type { BuildMode, GitHubVersion } from "../util";
|
||||
|
||||
import type { ExcludeQueryFilter, UserConfig } from "./db-config";
|
||||
|
||||
/**
|
||||
* Format of the CodeQL Action configuration state that is persisted
|
||||
* between steps of the CodeQL Action in a CodeQL workflow.
|
||||
*/
|
||||
export interface Config {
|
||||
/**
|
||||
* The version of the CodeQL Action that the configuration is for.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Set of analysis kinds that are enabled.
|
||||
*/
|
||||
analysisKinds: AnalysisKind[];
|
||||
/**
|
||||
* Set of languages to run analysis for.
|
||||
*/
|
||||
languages: Language[];
|
||||
/**
|
||||
* Build mode, if set. Currently only a single build mode is supported per job.
|
||||
*/
|
||||
buildMode: BuildMode | undefined;
|
||||
/**
|
||||
* A unaltered copy of the original user input.
|
||||
* Mainly intended to be used for status reporting.
|
||||
* If any field is useful for the actual processing
|
||||
* of the action then consider pulling it out to a
|
||||
* top-level field above.
|
||||
*/
|
||||
originalUserInput: UserConfig;
|
||||
/**
|
||||
* Directory to use for temporary files that should be
|
||||
* deleted at the end of the job.
|
||||
*/
|
||||
tempDir: string;
|
||||
/**
|
||||
* Path of the CodeQL executable.
|
||||
*/
|
||||
codeQLCmd: string;
|
||||
/**
|
||||
* Version of GitHub we are talking to.
|
||||
*/
|
||||
gitHubVersion: GitHubVersion;
|
||||
/**
|
||||
* The location where CodeQL databases should be stored.
|
||||
*/
|
||||
dbLocation: string;
|
||||
/**
|
||||
* Specifies whether we are debugging mode and should try to produce extra
|
||||
* output for debugging purposes when possible.
|
||||
*/
|
||||
debugMode: boolean;
|
||||
/**
|
||||
* Specifies the name of the debugging artifact if we are in debug mode.
|
||||
*/
|
||||
debugArtifactName: string;
|
||||
/**
|
||||
* Specifies the name of the database in the debugging artifact.
|
||||
*/
|
||||
debugDatabaseName: string;
|
||||
/**
|
||||
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
|
||||
* as well as adjustments made to it based on unsupported or required options.
|
||||
*/
|
||||
computedConfig: UserConfig;
|
||||
|
||||
/**
|
||||
* Partial map from languages to locations of TRAP caches for that language.
|
||||
* If a key is omitted, then TRAP caching should not be used for that language.
|
||||
*/
|
||||
trapCaches: { [language: Language]: string };
|
||||
|
||||
/**
|
||||
* Time taken to download TRAP caches. Used for status reporting.
|
||||
*/
|
||||
trapCacheDownloadTime: number;
|
||||
|
||||
/** A value indicating how dependency caching should be used. */
|
||||
dependencyCachingEnabled: CachingKind;
|
||||
|
||||
/** The keys of caches that we restored, if any. */
|
||||
dependencyCachingRestoredKeys: string[];
|
||||
|
||||
/**
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
extraQueryExclusions: ExcludeQueryFilter[];
|
||||
|
||||
/**
|
||||
* The overlay database mode to use.
|
||||
*/
|
||||
overlayDatabaseMode: OverlayDatabaseMode;
|
||||
|
||||
/**
|
||||
* Whether to use caching for overlay databases. If it is true, the action
|
||||
* will upload the created overlay-base database to the actions cache, and
|
||||
* download an overlay-base database from the actions cache before it creates
|
||||
* a new overlay database. If it is false, the action assumes that the
|
||||
* workflow will be responsible for managing database storage and retrieval.
|
||||
*
|
||||
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
|
||||
* `OverlayBase`.
|
||||
*/
|
||||
useOverlayDatabaseCaching: boolean;
|
||||
|
||||
/**
|
||||
* Whether the overlay database mode was set explicitly.
|
||||
*/
|
||||
overlayModeSetExplicitly: boolean;
|
||||
|
||||
/**
|
||||
* A partial mapping from repository properties that affect us to their values.
|
||||
*/
|
||||
repositoryProperties: RepositoryProperties;
|
||||
|
||||
/**
|
||||
* Whether to enable file coverage information.
|
||||
*/
|
||||
enableFileCoverageInformation: boolean;
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { Feature } from "../feature-flags";
|
||||
import { RepositoryPropertyName } from "../feature-flags/properties";
|
||||
import {
|
||||
getTestActionsEnv,
|
||||
RecordingLogger,
|
||||
setupTests,
|
||||
} from "../testing-utils";
|
||||
import { callee, setupTests } from "../testing-utils";
|
||||
|
||||
import { getConfigFileInput } from "./file";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const result = getConfigFileInput(logger, actionsEnv, {});
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
const repositoryProperties = {
|
||||
@@ -24,46 +21,49 @@ const repositoryProperties = {
|
||||
};
|
||||
|
||||
test("getConfigFileInput returns input value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
const testInput = "/some/path";
|
||||
sinon
|
||||
.stub(actionsEnv, "getOptionalInput")
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
|
||||
// Even though both an input and repository property are configured,
|
||||
// we prefer the direct input to the Action.
|
||||
const result = getConfigFileInput(logger, actionsEnv, repositoryProperties);
|
||||
t.is(result, testInput);
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(logger.hasMessage("Using configuration file input from workflow"));
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withActions((actionsEnv) => {
|
||||
sinon
|
||||
.stub(actionsEnv, "getOptionalInput")
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
})
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using configuration file input from workflow")
|
||||
.passes(t.is, testInput);
|
||||
});
|
||||
|
||||
test("getConfigFileInput returns repository property value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since there is no direct input, we should use the repository property.
|
||||
const result = getConfigFileInput(logger, actionsEnv, repositoryProperties);
|
||||
t.is(result, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"Using configuration file input from repository property",
|
||||
),
|
||||
);
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using configuration file input from repository property")
|
||||
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores empty repository property value", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const actionsEnv = getTestActionsEnv();
|
||||
|
||||
// Since the repository property value is an empty/whitespace string, we should ignore it.
|
||||
const result = getConfigFileInput(logger, actionsEnv, {
|
||||
[RepositoryPropertyName.CONFIG_FILE]: " ",
|
||||
});
|
||||
t.is(result, undefined);
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
|
||||
// Since the FF is off, we should ignore the repository property value.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties)
|
||||
.notLogs(t, "Using configuration file input from repository property")
|
||||
.logs(
|
||||
t,
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { ActionsEnv } from "../actions-util";
|
||||
import { ActionState } from "../action-common";
|
||||
import * as api from "../api-client";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import { Logger } from "../logging";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
export function getConfigFileInput(
|
||||
logger: Logger,
|
||||
actions: ActionsEnv,
|
||||
export async function getConfigFileInput(
|
||||
{
|
||||
logger,
|
||||
actions,
|
||||
features,
|
||||
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): string | undefined {
|
||||
): Promise<string | undefined> {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
|
||||
if (input !== undefined) {
|
||||
@@ -24,11 +33,71 @@ export function getConfigFileInput(
|
||||
repositoryProperties[RepositoryPropertyName.CONFIG_FILE];
|
||||
|
||||
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`,
|
||||
// Only use the repository property value if the FF is enabled.
|
||||
const useRepositoryProperty = await features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
return propertyValue;
|
||||
|
||||
if (useRepositoryProperty) {
|
||||
logger.info(
|
||||
`Using configuration file input from repository property: ${propertyValue}`,
|
||||
);
|
||||
return propertyValue;
|
||||
} else {
|
||||
logger.info(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch a `UserConfig` from a remote `address`.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The remote address of the configuration file.
|
||||
* @param apiDetails Information about how to connect to the API.
|
||||
*
|
||||
* @returns The `UserConfig`, if it could be fetched and parsed successfully.
|
||||
*/
|
||||
export async function getRemoteConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
): Promise<UserConfig> {
|
||||
const address = await parseRemoteFileAddress(actionState, configFile);
|
||||
|
||||
const response = await api
|
||||
.getApiClientWithExternalAuth(apiDetails)
|
||||
.rest.repos.getContent({
|
||||
owner: address.owner,
|
||||
repo: address.repo,
|
||||
path: address.path,
|
||||
ref: address.ref,
|
||||
});
|
||||
|
||||
let fileContents: string;
|
||||
if ("content" in response.data && response.data.content !== undefined) {
|
||||
fileContents = response.data.content;
|
||||
} else if (Array.isArray(response.data)) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
|
||||
);
|
||||
} else {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return parseUserConfig(
|
||||
actionState.logger,
|
||||
configFile,
|
||||
Buffer.from(fileContents, "base64").toString("binary"),
|
||||
validateConfig,
|
||||
);
|
||||
}
|
||||
|
||||
49
src/config/pack-registries.ts
Normal file
49
src/config/pack-registries.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
|
||||
// Token to use when downloading packs from this registry.
|
||||
token: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of registries and the associated pack globs that determine where each
|
||||
* pack can be downloaded from.
|
||||
*/
|
||||
export interface RegistryConfigNoCredentials {
|
||||
// URL of a package registry, eg- https://ghcr.io/v2/
|
||||
url: string;
|
||||
|
||||
// List of globs that determine which packs are associated with this registry.
|
||||
packages: string[] | string;
|
||||
|
||||
// Kind of registry, either "github" or "docker". Default is "docker".
|
||||
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
|
||||
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
|
||||
// where GHCR is not available, such as certain GHES environments.
|
||||
kind?: "github" | "docker";
|
||||
}
|
||||
|
||||
export function parseRegistries(
|
||||
registriesInput: string | undefined,
|
||||
): RegistryConfigWithCredentials[] | undefined {
|
||||
try {
|
||||
return registriesInput
|
||||
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
|
||||
: undefined;
|
||||
} catch {
|
||||
throw new ConfigurationError(
|
||||
"Invalid registries input. Must be a YAML string.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRegistriesWithoutCredentials(
|
||||
registriesInput?: string,
|
||||
): RegistryConfigNoCredentials[] | undefined {
|
||||
return parseRegistries(registriesInput)?.map((r) => {
|
||||
const { url, packages, kind } = r;
|
||||
return { url, packages, kind };
|
||||
});
|
||||
}
|
||||
268
src/config/remote-file.test.ts
Normal file
268
src/config/remote-file.test.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import * as errors from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { callee } from "../testing-utils";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import {
|
||||
DEFAULT_CONFIG_FILE_NAME,
|
||||
DEFAULT_CONFIG_FILE_REF,
|
||||
parseRemoteFileAddress,
|
||||
RemoteFileAddress,
|
||||
} from "./remote-file";
|
||||
|
||||
type ParseRemoteFileAddressTest = {
|
||||
input: string;
|
||||
expected: RemoteFileAddress;
|
||||
};
|
||||
|
||||
test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const expected: RemoteFileAddress = {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: "ref",
|
||||
};
|
||||
|
||||
const oldFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo/path@ref", expected },
|
||||
{ input: "owner /repo/path@ref", expected },
|
||||
{ input: "owner/ repo/path@ref", expected },
|
||||
{ input: "owner/repo /path@ref", expected },
|
||||
{ input: "owner/repo/ path@ref", expected },
|
||||
{ input: "owner/repo/path @ref", expected },
|
||||
{ input: "owner/repo/path@ ref", expected },
|
||||
{
|
||||
input: "owner/repo/path/to/codeql.yml@ref/feature",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo/path/to/codeql.yml@ref/feature ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const oldFormatInput of oldFormatInputs) {
|
||||
await target
|
||||
.withArgs(oldFormatInput.input)
|
||||
.passes(t.deepEqual, oldFormatInput.expected);
|
||||
}
|
||||
|
||||
// New format.
|
||||
const newFormatInputs: ParseRemoteFileAddressTest[] = [
|
||||
{ input: "owner/repo@ref:path", expected },
|
||||
{ input: "owner /repo@ref:path", expected },
|
||||
{ input: "owner/ repo@ref:path", expected },
|
||||
{ input: "owner/repo @ref:path", expected },
|
||||
{ input: "owner/repo@ ref:path", expected },
|
||||
{ input: "owner/repo@ref :path", expected },
|
||||
{ input: "owner/repo@ref: path", expected },
|
||||
{
|
||||
input: "owner/repo@ref/feature:path/to/codeql.yml",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
{
|
||||
input: " owner/repo@ref/feature:path/to/codeql.yml ",
|
||||
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
|
||||
},
|
||||
];
|
||||
|
||||
for (const newFormatInput of newFormatInputs) {
|
||||
const targetWithArgs = target.withArgs(newFormatInput.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(t.deepEqual, newFormatInput.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
|
||||
const owner = "test-owner";
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "repo@ref:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo@ref",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo:path.yml",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: "path.yml",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "repo",
|
||||
expected: {
|
||||
owner,
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
|
||||
const getRequired: sinon.SinonStub = sinon.stub();
|
||||
getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`);
|
||||
|
||||
const target = callee(parseRemoteFileAddress)
|
||||
.withArgs("repo@ref")
|
||||
.withEnv((env) => {
|
||||
sinon.define(env, "getRequired", getRequired);
|
||||
});
|
||||
|
||||
await target
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.throws(t, { instanceOf: Error });
|
||||
|
||||
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a path", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
input: "owner/repo@ref",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: "ref",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "owner/repo",
|
||||
expected: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
|
||||
|
||||
// Should only accept the input if the FF is enabled.
|
||||
await target.withFeatures([]).throws(t);
|
||||
await target
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(t.deepEqual, {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
} satisfies RemoteFileAddress);
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress rejects invalid values", async (t) => {
|
||||
const owner = "owner";
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testInputs = [
|
||||
" ",
|
||||
"repo//absolute",
|
||||
"repo:/absolute",
|
||||
"/repo@ref",
|
||||
" /repo@ref",
|
||||
"repo@",
|
||||
"repo:",
|
||||
"repo/",
|
||||
"/repo",
|
||||
":path",
|
||||
"@ref",
|
||||
"@ref:path",
|
||||
"owner/@ref:path",
|
||||
"owner/@ref",
|
||||
"owner/:path",
|
||||
];
|
||||
|
||||
for (const testInput of testInputs) {
|
||||
const targetWithArgs = target.withArgs(testInput);
|
||||
|
||||
// Should throw both when the new format is and isn't accepted.
|
||||
await targetWithArgs.withFeatures([]).throws(t, {
|
||||
instanceOf: ConfigurationError,
|
||||
message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
|
||||
});
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.throws(t, {
|
||||
// When the new format is accepted, there are some more specific
|
||||
// errors in some cases. It is sufficient for us to check that
|
||||
// an exception is thrown.
|
||||
instanceOf: ConfigurationError,
|
||||
});
|
||||
}
|
||||
});
|
||||
138
src/config/remote-file.ts
Normal file
138
src/config/remote-file.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { Env, ActionsEnvVars } from "../environment";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { ConfigurationError, Failure, Result, Success } from "../util";
|
||||
|
||||
/** Represents remote file addresses. */
|
||||
export interface RemoteFileAddress {
|
||||
/** The owner of the repository. */
|
||||
owner: string;
|
||||
/** The repository name. */
|
||||
repo: string;
|
||||
/** The path of the file. */
|
||||
path: string;
|
||||
/** The ref of the repository. */
|
||||
ref: string;
|
||||
}
|
||||
|
||||
/** The default file path to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
|
||||
/** The default ref to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */
|
||||
function getDefaultOwner(env: Env): string {
|
||||
const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY);
|
||||
const nwoParts = currentRepoNwo.split("/");
|
||||
|
||||
if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) {
|
||||
// This shouldn't happen, so we should throw if `GITHUB_REPOSITORY` doesn't match
|
||||
// our expectations.
|
||||
throw new Error(
|
||||
`Expected ${ActionsEnvVars.GITHUB_REPOSITORY} to contain a name with owner, but got '${currentRepoNwo}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return nwoParts[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* The old remote address format that's always been supported for the `config-file` input.
|
||||
* All the components are required. Unchanged from the previous implementation.
|
||||
*/
|
||||
const OLD_REMOTE_ADDRESS_FORMAT = new RegExp(
|
||||
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
|
||||
);
|
||||
|
||||
/**
|
||||
* Attempts to parse `input` as a `RemoteFileAddress` using the old format.
|
||||
*
|
||||
* @param input The input to try and parse.
|
||||
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
|
||||
*/
|
||||
function parseOldRemoteFileAddress(
|
||||
input: string,
|
||||
): Result<RemoteFileAddress, undefined> {
|
||||
const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input);
|
||||
|
||||
// 5 = 4 groups + the whole expression
|
||||
if (pieces?.groups === undefined || pieces.length < 5) {
|
||||
return new Failure(undefined);
|
||||
}
|
||||
|
||||
return new Success({
|
||||
owner: pieces.groups.owner.trim(),
|
||||
repo: pieces.groups.repo.trim(),
|
||||
path: pieces.groups.path.trim(),
|
||||
ref: pieces.groups.ref.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
|
||||
*
|
||||
* @param actionState The current Action state.
|
||||
* @param configFile The string to try and parse.
|
||||
* @returns The successful result of executing the regex.
|
||||
* @throws `ConfigurationError` if the format of `configFile` is not valid.
|
||||
*/
|
||||
export async function parseRemoteFileAddress(
|
||||
actionState: ActionState<["FeatureFlags", "Env"]>,
|
||||
configFile: string,
|
||||
): Promise<RemoteFileAddress> {
|
||||
// Try to parse the input using the old format. If successful, return the
|
||||
// resulting `RemoteFileAddress`. Otherwise, continue using the new format.
|
||||
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
|
||||
|
||||
if (oldFormatAddressResult.isSuccess()) {
|
||||
return oldFormatAddressResult.value;
|
||||
}
|
||||
|
||||
// If the FF for the new format is not enabled, throw the old format error.
|
||||
const allowNewFormat = await actionState.features.getValue(
|
||||
Feature.NewRemoteFileAddresses,
|
||||
);
|
||||
if (!allowNewFormat) {
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoOldFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
|
||||
);
|
||||
const pieces = format.exec(configFile.trim());
|
||||
|
||||
const repo: string | undefined = pieces?.groups?.repo?.trim();
|
||||
|
||||
// Check that the regular expression matched and that we have at least the repo name.
|
||||
if (!pieces?.groups || !repo || repo.length === 0) {
|
||||
// Neither the old format nor the new format worked. Throw an error that
|
||||
// explains the format we accept. We only mention the new format, since that's
|
||||
// what we want to be used going forward.
|
||||
throw new ConfigurationError(
|
||||
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
|
||||
);
|
||||
}
|
||||
|
||||
const owner: string | undefined = pieces.groups.owner?.trim();
|
||||
const path: string | undefined = pieces.groups.path?.trim();
|
||||
const ref: string | undefined = pieces.groups.ref?.trim();
|
||||
|
||||
// Ensure that the path is a relative path.
|
||||
if (path?.startsWith("/")) {
|
||||
throw new ConfigurationError(
|
||||
`The path component of '${configFile}' cannot be an absolute path.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
owner: owner || getDefaultOwner(actionState.env),
|
||||
repo,
|
||||
path: path || DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: ref || DEFAULT_CONFIG_FILE_REF,
|
||||
};
|
||||
}
|
||||
@@ -11,8 +11,10 @@ import * as apiClient from "./api-client";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import { cleanupAndUploadDatabases } from "./database-upload";
|
||||
import { Feature } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { RepositoryNwo } from "./repository";
|
||||
import {
|
||||
checkExpectedLogMessages,
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
import {
|
||||
CleanupLevel,
|
||||
GitHubVariant,
|
||||
HTTPError,
|
||||
initializeEnvironment,
|
||||
@@ -335,3 +338,191 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"Records overlay and clear cleanup sizes when uploading an overlay-base database",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("true");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
// Track the cleanup level passed to each cleanup so that the database
|
||||
// bundle stub can write a differently-sized bundle for each level.
|
||||
const cleanupLevels: CleanupLevel[] = [];
|
||||
let lastCleanupLevel: CleanupLevel | undefined;
|
||||
const overlaySizeBytes = 100;
|
||||
const clearSizeBytes = 50;
|
||||
const codeql = createStubCodeQL({
|
||||
async databaseCleanupCluster(_config, cleanupLevel) {
|
||||
cleanupLevels.push(cleanupLevel);
|
||||
lastCleanupLevel = cleanupLevel;
|
||||
},
|
||||
async databaseBundle(_databasePath, outputFilePath) {
|
||||
const sizeBytes =
|
||||
lastCleanupLevel === CleanupLevel.Overlay
|
||||
? overlaySizeBytes
|
||||
: clearSizeBytes;
|
||||
fs.writeFileSync(outputFilePath, "x".repeat(sizeBytes));
|
||||
},
|
||||
});
|
||||
|
||||
const config = getTestConfig(tmpDir);
|
||||
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
|
||||
const loggedMessages: LoggedMessage[] = [];
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger(loggedMessages),
|
||||
);
|
||||
|
||||
// The database should be cleaned up at the `overlay` level for the upload
|
||||
// and then re-cleaned at the `clear` level to measure its size.
|
||||
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay, CleanupLevel.Clear]);
|
||||
|
||||
t.is(results.length, 1);
|
||||
t.is(results[0].is_overlay_base, true);
|
||||
t.is(results[0].zipped_upload_size_bytes, overlaySizeBytes);
|
||||
t.is(results[0].clear_cleanup_zipped_size_bytes, clearSizeBytes);
|
||||
t.is(typeof results[0].clear_cleanup_measurement_duration_ms, "number");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"Does not measure clear cleanup size for a regular (non-overlay-base) upload",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("true");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
const cleanupLevels: CleanupLevel[] = [];
|
||||
const codeql = createStubCodeQL({
|
||||
async databaseCleanupCluster(_config, cleanupLevel) {
|
||||
cleanupLevels.push(cleanupLevel);
|
||||
},
|
||||
async databaseBundle(_databasePath, outputFilePath) {
|
||||
fs.writeFileSync(outputFilePath, "");
|
||||
},
|
||||
});
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
testRepoName,
|
||||
codeql,
|
||||
getTestConfig(tmpDir),
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
);
|
||||
|
||||
// A regular upload is cleaned only once, at the `clear` level.
|
||||
t.deepEqual(cleanupLevels, [CleanupLevel.Clear]);
|
||||
t.is(results[0].is_overlay_base, false);
|
||||
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
|
||||
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("true");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
const cleanupLevels: CleanupLevel[] = [];
|
||||
const codeql = createStubCodeQL({
|
||||
async databaseCleanupCluster(_config, cleanupLevel) {
|
||||
cleanupLevels.push(cleanupLevel);
|
||||
},
|
||||
async databaseBundle(_databasePath, outputFilePath) {
|
||||
fs.writeFileSync(outputFilePath, "");
|
||||
},
|
||||
});
|
||||
|
||||
const config = getTestConfig(tmpDir);
|
||||
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
config.debugMode = true;
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
);
|
||||
|
||||
// In debug mode we clean up at the `overlay` level for the upload but skip
|
||||
// the additional `clear` cleanup, to preserve the database for debugging.
|
||||
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay]);
|
||||
t.is(results[0].is_overlay_base, true);
|
||||
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
|
||||
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"Does not record a clear cleanup duration when the clear cleanup fails",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
sinon
|
||||
.stub(actionsUtil, "getRequiredInput")
|
||||
.withArgs("upload-database")
|
||||
.returns("true");
|
||||
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
|
||||
|
||||
await mockHttpRequests(201);
|
||||
|
||||
const codeql = createStubCodeQL({
|
||||
async databaseCleanupCluster(_config, cleanupLevel) {
|
||||
if (cleanupLevel === CleanupLevel.Clear) {
|
||||
throw new Error("clear cleanup failed");
|
||||
}
|
||||
},
|
||||
async databaseBundle(_databasePath, outputFilePath) {
|
||||
fs.writeFileSync(outputFilePath, "x".repeat(100));
|
||||
},
|
||||
});
|
||||
|
||||
const config = getTestConfig(tmpDir);
|
||||
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
|
||||
|
||||
const results = await cleanupAndUploadDatabases(
|
||||
testRepoName,
|
||||
codeql,
|
||||
config,
|
||||
testApiDetails,
|
||||
createFeatures([Feature.UploadOverlayDbToApi]),
|
||||
getRecordingLogger([]),
|
||||
);
|
||||
|
||||
// When the `clear` cleanup fails, no size is measured, so we should not
|
||||
// report a measurement duration either.
|
||||
t.is(results[0].is_overlay_base, true);
|
||||
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
|
||||
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -25,6 +25,19 @@ export interface DatabaseUploadResult {
|
||||
zipped_upload_size_bytes?: number;
|
||||
/** Whether the uploaded database is an overlay base. */
|
||||
is_overlay_base?: boolean;
|
||||
/**
|
||||
* For overlay-base uploads only: the size in bytes that the zipped database
|
||||
* would have been if it had been cleaned at the `clear` cleanup level instead
|
||||
* of the `overlay` level.
|
||||
*/
|
||||
clear_cleanup_zipped_size_bytes?: number;
|
||||
/**
|
||||
* For overlay-base uploads only: the time in milliseconds spent measuring the
|
||||
* `clear` cleanup size (cleaning up the cluster at the `clear` level and
|
||||
* bundling each database). This is a cluster-wide measurement, so it is the
|
||||
* same for every language in a run.
|
||||
*/
|
||||
clear_cleanup_measurement_duration_ms?: number;
|
||||
/** Time taken to upload database in milliseconds. */
|
||||
upload_duration_ms?: number;
|
||||
/** If there was an error during database upload, this is its message. */
|
||||
@@ -156,9 +169,89 @@ export async function cleanupAndUploadDatabases(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When we upload an overlay-base database, we cleaned the databases at the `overlay` level, which
|
||||
// retains more data than the `clear` level used for regular uploads. Measure what the zipped size
|
||||
// would have been at the `clear` level too, so we can compare the storage cost of overlay-base
|
||||
// databases against regular databases for the same repository.
|
||||
//
|
||||
// We skip this in debug mode, where the databases are preserved and uploaded as debug artifacts,
|
||||
// since cleaning them up at the `clear` level would discard data that is useful for debugging.
|
||||
if (shouldUploadOverlayBase && !config.debugMode) {
|
||||
await withGroupAsync(
|
||||
"Measuring database size at the clear cleanup level",
|
||||
() => recordClearCleanupSizes(codeql, config, reports, logger),
|
||||
);
|
||||
}
|
||||
|
||||
return reports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the databases at the `clear` cleanup level and records the resulting zipped size for
|
||||
* each language in `clear_cleanup_zipped_size_bytes`. If the cleanup succeeds, also records the
|
||||
* time spent taking the measurement in `clear_cleanup_measurement_duration_ms`.
|
||||
*
|
||||
* This mutates the entries of `reports` in place. It must run only after all overlay-base uploads
|
||||
* have completed, since the `clear` cleanup discards overlay data that the uploaded database
|
||||
* depends on.
|
||||
*
|
||||
* Failures here are non-fatal: this is telemetry-only, so we log and move on rather than failing
|
||||
* the workflow.
|
||||
*/
|
||||
async function recordClearCleanupSizes(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
reports: DatabaseUploadResult[],
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
// Include both the cleanup and the re-bundling to record how much time taking this measurement adds
|
||||
// to the run.
|
||||
const startTime = performance.now();
|
||||
|
||||
try {
|
||||
await codeql.databaseCleanupCluster(config, CleanupLevel.Clear);
|
||||
} catch (e) {
|
||||
// The cleanup didn't run, so there are no sizes to measure. Return without recording a
|
||||
// duration, so that we don't report a measurement duration with no accompanying sizes.
|
||||
logger.warning(
|
||||
`Failed to clean up databases at the '${CleanupLevel.Clear}' level for ` +
|
||||
`size measurement: ${util.getErrorMessage(e)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const language of config.languages) {
|
||||
const report = reports.find((r) => r.language === language);
|
||||
if (report === undefined) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const bundledDb = await bundleDb(config, language, codeql, language, {
|
||||
includeDiagnostics: false,
|
||||
});
|
||||
report.clear_cleanup_zipped_size_bytes = fs.statSync(bundledDb).size;
|
||||
logger.debug(
|
||||
`Database for ${language} is ` +
|
||||
`${report.clear_cleanup_zipped_size_bytes} bytes zipped at the ` +
|
||||
`'${CleanupLevel.Clear}' cleanup level ` +
|
||||
`(vs. ${report.zipped_upload_size_bytes ?? "unknown"} bytes at the ` +
|
||||
`'${CleanupLevel.Overlay}' level).`,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warning(
|
||||
`Failed to measure the '${CleanupLevel.Clear}' cleanup database size ` +
|
||||
`for ${language}: ${util.getErrorMessage(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const durationMs = performance.now() - startTime;
|
||||
for (const report of reports) {
|
||||
report.clear_cleanup_measurement_duration_ms = durationMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a bundled database to the GitHub API.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.25.6",
|
||||
"cliVersion": "2.25.6",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.5",
|
||||
"priorCliVersion": "2.25.5"
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
}
|
||||
|
||||
@@ -18,10 +18,16 @@ export enum EnvVar {
|
||||
CLI_VERBOSITY = "CODEQL_VERBOSITY",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
* Set by Default Setup to the base branch of the PR being analysed, if analysing a PR.
|
||||
* This is needed because the `pull_request` context is not available for `dynamic` events.
|
||||
*/
|
||||
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
|
||||
CODE_SCANNING_BASE_BRANCH = "CODE_SCANNING_BASE_BRANCH",
|
||||
|
||||
/**
|
||||
* Set by Default Setup to the full ref being analysed, if analysing a PR.
|
||||
* This is needed because the `pull_request` context is not available for `dynamic` events.
|
||||
*/
|
||||
CODE_SCANNING_REF = "CODE_SCANNING_REF",
|
||||
|
||||
/** Whether the CodeQL Action has invoked the Go autobuilder. */
|
||||
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
|
||||
@@ -83,6 +89,9 @@ export enum EnvVar {
|
||||
/** Whether to suppress the warning if the current CLI will soon be unsupported. */
|
||||
SUPPRESS_DEPRECATED_SOON_WARNING = "CODEQL_ACTION_SUPPRESS_DEPRECATED_SOON_WARNING",
|
||||
|
||||
/** Used to dictate or persist the temporary directory used by the CodeQL Action. */
|
||||
TEMP = "CODEQL_ACTION_TEMP",
|
||||
|
||||
/** Whether to disable uploading SARIF results or status reports to the GitHub API */
|
||||
TEST_MODE = "CODEQL_ACTION_TEST_MODE",
|
||||
|
||||
@@ -160,3 +169,125 @@ export enum EnvVar {
|
||||
/** Used by Code Scanning Risk Assessment to communicate the assessment ID to the CodeQL Action. */
|
||||
RISK_ASSESSMENT_ID = "CODEQL_ACTION_RISK_ASSESSMENT_ID",
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates known GitHub Actions environment variables that we expect
|
||||
* to be set in a GitHub Actions environment.
|
||||
*/
|
||||
export enum ActionsEnvVars {
|
||||
GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY",
|
||||
GITHUB_API_URL = "GITHUB_API_URL",
|
||||
GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME",
|
||||
GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH",
|
||||
GITHUB_JOB = "GITHUB_JOB",
|
||||
GITHUB_REF = "GITHUB_REF",
|
||||
GITHUB_REPOSITORY = "GITHUB_REPOSITORY",
|
||||
GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT",
|
||||
GITHUB_RUN_ID = "GITHUB_RUN_ID",
|
||||
GITHUB_SERVER_URL = "GITHUB_SERVER_URL",
|
||||
GITHUB_SHA = "GITHUB_SHA",
|
||||
GITHUB_WORKFLOW = "GITHUB_WORKFLOW",
|
||||
GITHUB_WORKSPACE = "GITHUB_WORKSPACE",
|
||||
RUNNER_ENVIRONMENT = "RUNNER_ENVIRONMENT",
|
||||
RUNNER_NAME = "RUNNER_NAME",
|
||||
RUNNER_OS = "RUNNER_OS",
|
||||
RUNNER_TEMP = "RUNNER_TEMP",
|
||||
RUNNER_TOOL_CACHE = "RUNNER_TOOL_CACHE",
|
||||
}
|
||||
|
||||
/** A type representing all known environment variables. */
|
||||
export type KnownEnvVar = EnvVar | ActionsEnvVars;
|
||||
|
||||
/**
|
||||
* Gets an environment variable, but throws an error if it is not set.
|
||||
*/
|
||||
function getRequiredEnvVar(env: NodeJS.ProcessEnv, paramName: string): string {
|
||||
const value = env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*
|
||||
* @deprecated Use `getRequired` of a `ReadOnlyEnv` or `Env` instance instead.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
return getRequiredEnvVar(process.env, paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an environment variable, but returns `undefined` if it is not set or empty.
|
||||
*/
|
||||
function getOptionalEnvVarFrom(
|
||||
env: NodeJS.ProcessEnv,
|
||||
paramName: string,
|
||||
): string | undefined {
|
||||
const value = env[paramName];
|
||||
if (value?.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment variable, but return `undefined` if it is not set or empty.
|
||||
*
|
||||
* @deprecated Use `getOptional` of a `ReadOnlyEnv` or `Env` instance instead.
|
||||
*/
|
||||
export function getOptionalEnvVar(paramName: string): string | undefined {
|
||||
return getOptionalEnvVarFrom(process.env, paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstraction around read-only environment variables, to allow abstracting away from `process.env`
|
||||
* in tests, while clearly signalling in regular code that the consumer of the `ReadOnlyEnv` instance
|
||||
* will only read from it.
|
||||
*/
|
||||
export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
|
||||
constructor(protected readonly vars: Record<string, T>) {}
|
||||
|
||||
/** Tries to get the value for `name` and throws if there isn't one. */
|
||||
public getRequired(name: string): string {
|
||||
return getRequiredEnvVar(this.vars, name);
|
||||
}
|
||||
|
||||
/** Gets the value for `name`, or `undefined` if it isn't set or empty. */
|
||||
public getOptional(name: string): string | undefined {
|
||||
return getOptionalEnvVarFrom(this.vars, name);
|
||||
}
|
||||
|
||||
/** Gets the entries of the underlying `ProcessEnv`. */
|
||||
public entries(): Array<[string, T]> {
|
||||
return Object.entries(this.vars);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around an environment, to allow abstracting away from `process.env` in tests.
|
||||
* Use `ReadOnlyEnv` instead if you only plan to read from the environment.
|
||||
* This type allows writing to the environment.
|
||||
*/
|
||||
export class Env<
|
||||
T extends string | undefined = string | undefined,
|
||||
> extends ReadOnlyEnv<T> {
|
||||
private changed: boolean = false;
|
||||
|
||||
/** Sets an environment variable. */
|
||||
public set(name: string, value: T): void {
|
||||
this.vars[name] = value;
|
||||
this.changed = true;
|
||||
}
|
||||
|
||||
/** Gets a value indicating whether `set` was called at least once. */
|
||||
public hasChanged(): boolean {
|
||||
return this.changed;
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets an `Env` instance for `env`, which is `process.env` by default. */
|
||||
export function getEnv(env: NodeJS.ProcessEnv = process.env): Env {
|
||||
return new Env(env);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function getInvalidConfigFileMessage(
|
||||
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
export function getConfigFileRepoOldFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
@@ -39,6 +39,15 @@ export function getConfigFileRepoFormatInvalidMessage(
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileRepoFormatInvalidMessage(
|
||||
configFile: string,
|
||||
): string {
|
||||
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
|
||||
error += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getConfigFileFormatInvalidMessage(configFile: string): string {
|
||||
return `The configuration file "${configFile}" could not be read`;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@ export enum Feature {
|
||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||
AllowToolcacheInput = "allow_toolcache_input",
|
||||
CleanupTrapCaches = "cleanup_trap_caches",
|
||||
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
||||
ConfigFileRepositoryProperty = "config_file_repository_property",
|
||||
CppDependencyInstallation = "cpp_dependency_installation_enabled",
|
||||
CsharpCacheBuildModeNone = "csharp_cache_bmn",
|
||||
CsharpNewCacheKey = "csharp_new_cache_key",
|
||||
@@ -90,6 +92,8 @@ export enum Feature {
|
||||
ForceNightly = "force_nightly",
|
||||
IgnoreGeneratedFiles = "ignore_generated_files",
|
||||
JavaNetworkDebugging = "java_network_debugging",
|
||||
/** Allow the new remote file address format. */
|
||||
NewRemoteFileAddresses = "new_remote_file_addresses",
|
||||
OverlayAnalysis = "overlay_analysis",
|
||||
OverlayAnalysisCodeScanningCpp = "overlay_analysis_code_scanning_cpp",
|
||||
OverlayAnalysisCodeScanningCsharp = "overlay_analysis_code_scanning_csharp",
|
||||
@@ -184,6 +188,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.ConfigFileRepositoryProperty]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CONFIG_FILE_REPOSITORY_PROPERTY",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CppDependencyInstallation]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_EXTRACTOR_CPP_AUTOINSTALL_DEPENDENCIES",
|
||||
@@ -248,6 +257,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_JAVA_NETWORK_DEBUGGING",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.NewRemoteFileAddresses]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_NEW_REMOTE_FILE_ADDRESSES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysis]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",
|
||||
|
||||
@@ -7,9 +7,9 @@ import * as io from "@actions/io";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
FileCmdNotFoundError,
|
||||
getActionsEnv,
|
||||
getActionVersion,
|
||||
getFileType,
|
||||
getOptionalInput,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getTotalCacheSize,
|
||||
shouldRestoreCache,
|
||||
} from "./caching-utils";
|
||||
import { writeCommandCacheFile } from "./cli/output-cache";
|
||||
import { CodeQL } from "./codeql";
|
||||
import { getConfigFileInput } from "./config/file";
|
||||
import * as configUtils from "./config-utils";
|
||||
@@ -56,7 +57,7 @@ import {
|
||||
runDatabaseInitCluster,
|
||||
} from "./init";
|
||||
import { JavaEnvVars, BuiltInLanguage } from "./languages";
|
||||
import { getActionsLogger, Logger, withGroupAsync } from "./logging";
|
||||
import { Logger, withGroupAsync } from "./logging";
|
||||
import {
|
||||
downloadOverlayBaseDatabaseFromCache,
|
||||
OverlayBaseDatabaseDownloadStats,
|
||||
@@ -73,7 +74,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ZstdAvailability } from "./tar";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
@@ -204,12 +204,12 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
const actionsEnv = getActionsEnv();
|
||||
const startedAt = actionState.startedAt;
|
||||
const logger = actionState.logger;
|
||||
|
||||
let apiDetails: GitHubApiCombinedDetails;
|
||||
let config: configUtils.Config | undefined;
|
||||
@@ -263,7 +263,11 @@ async function run(startedAt: Date) {
|
||||
|
||||
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
|
||||
|
||||
configFile = getConfigFileInput(logger, actionsEnv, repositoryProperties);
|
||||
const actionStateWithFeatures = { ...actionState, features };
|
||||
configFile = await getConfigFileInput(
|
||||
actionStateWithFeatures,
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
// path.resolve() respects the intended semantics of source-root. If
|
||||
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
|
||||
@@ -323,6 +327,10 @@ async function run(startedAt: Date) {
|
||||
toolsSource = initCodeQLResult.toolsSource;
|
||||
zstdAvailability = initCodeQLResult.zstdAvailability;
|
||||
|
||||
// Populate the in-memory command cache with CLI version.
|
||||
// This result will be persisted to disk at the end of the init action.
|
||||
await codeql.getVersion();
|
||||
|
||||
// Check the workflow for problems. If there are any problems, they are reported
|
||||
// to the workflow log. No exceptions are thrown.
|
||||
await checkWorkflow(logger, codeql);
|
||||
@@ -361,7 +369,7 @@ async function run(startedAt: Date) {
|
||||
repositoryProperties,
|
||||
);
|
||||
|
||||
config = await initConfig(features, {
|
||||
config = await initConfig(actionStateWithFeatures, {
|
||||
analysisKinds,
|
||||
languagesInput: getOptionalInput("languages"),
|
||||
queriesInput: getOptionalInput("queries"),
|
||||
@@ -766,6 +774,9 @@ async function run(startedAt: Date) {
|
||||
|
||||
core.setOutput("codeql-path", config.codeQLCmd);
|
||||
core.setOutput("codeql-version", (await codeql.getVersion()).version);
|
||||
|
||||
// Persist the command cache to disk at the end of a successful init.
|
||||
writeCommandCacheFile();
|
||||
} catch (unwrappedError) {
|
||||
const error = wrapError(unwrappedError);
|
||||
core.setFailed(error.message);
|
||||
@@ -846,19 +857,13 @@ async function recordZstdAvailability(
|
||||
);
|
||||
}
|
||||
|
||||
/** Defines the `init` Action. */
|
||||
const init: Action = {
|
||||
name: ActionName.Init,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`init action failed: ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.Init,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(init);
|
||||
await checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { ActionsEnvVars } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import {
|
||||
checkPacksForOverlayCompatibility,
|
||||
@@ -84,7 +85,7 @@ for (const { runnerEnv, ErrorConstructor, message } of [
|
||||
`cleanupDatabaseClusterDirectory throws a ${ErrorConstructor.name} when cleanup fails on ${runnerEnv} runner`,
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir: string) => {
|
||||
process.env["RUNNER_ENVIRONMENT"] = runnerEnv;
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnv;
|
||||
|
||||
const dbLocation = path.resolve(tmpDir, "dbs");
|
||||
fs.mkdirSync(dbLocation, { recursive: true });
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as github from "@actions/github";
|
||||
import * as io from "@actions/io";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
getOptionalInput,
|
||||
isAnalyzingPullRequest,
|
||||
@@ -81,11 +82,11 @@ export async function initCodeQL(
|
||||
}
|
||||
|
||||
export async function initConfig(
|
||||
features: FeatureEnablement,
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
inputs: configUtils.InitConfigInputs,
|
||||
): Promise<configUtils.Config> {
|
||||
return await withGroupAsync("Load language configuration", async () => {
|
||||
return await configUtils.initConfig(features, inputs);
|
||||
return await configUtils.initConfig(actionState, inputs);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ const testSchema = {
|
||||
requiredKey: json.string,
|
||||
};
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
const optionalOrNullSchema = {
|
||||
optionalKey: json.optionalOrNull(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - required properties are required", async (t) => {
|
||||
@@ -28,13 +28,36 @@ test("validateSchema - required properties are required", async (t) => {
|
||||
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
|
||||
});
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
test("validateSchema - optionalOrNullSchema properties are optional or null", async (t) => {
|
||||
// Optional fields may be absent
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, {}));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: undefined }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: null }));
|
||||
|
||||
// But, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: false }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: true }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: [] }));
|
||||
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: {} }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const optionalSchema = {
|
||||
optionalKey: json.optional(json.string),
|
||||
};
|
||||
|
||||
test("validateSchema - optional properties are optional", async (t) => {
|
||||
// Optional fields may be absent or explicitly undefined
|
||||
t.true(json.validateSchema(optionalSchema, {}));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
|
||||
|
||||
// But should reject null
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: null }));
|
||||
|
||||
// And, if present, should have the expected type
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
|
||||
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
|
||||
@@ -44,3 +67,76 @@ test("validateSchema - optional properties are optional", async (t) => {
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
|
||||
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
|
||||
});
|
||||
|
||||
const arraySchema = {
|
||||
arrayKey: json.array(json.number),
|
||||
};
|
||||
|
||||
test("validateSchema - validates arrays", async (t) => {
|
||||
// Arrays of numeric elements are accepted.
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8] }));
|
||||
t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15] }));
|
||||
|
||||
// Other array elements are not accepted.
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, "bar"] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, undefined] }));
|
||||
t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] }));
|
||||
});
|
||||
|
||||
const objectSchema = {
|
||||
objectKey: json.object(arraySchema),
|
||||
};
|
||||
|
||||
test("validateSchema - validates objects", async (t) => {
|
||||
// Objects of the given schema are accepted.
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [] } }));
|
||||
t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [4] } }));
|
||||
|
||||
// Other values are not accepted.
|
||||
t.false(json.validateSchema(objectSchema, {}));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: [] }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: undefined }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: null }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: "foo" }));
|
||||
t.false(json.validateSchema(objectSchema, { objectKey: 123 }));
|
||||
});
|
||||
|
||||
const checkSchemaTestSchema = {
|
||||
rootKey: json.object(objectSchema),
|
||||
};
|
||||
|
||||
test("checkSchema - reports unknown keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: [],
|
||||
},
|
||||
nestedExtraKey: "foo",
|
||||
},
|
||||
extraKey: "bar",
|
||||
});
|
||||
|
||||
t.true(result.valid);
|
||||
t.deepEqual(
|
||||
result.unknownKeys.sort(),
|
||||
[".extraKey", ".rootKey.nestedExtraKey"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test("checkSchema - reports invalid keys", async (t) => {
|
||||
const result = json.checkSchema(checkSchemaTestSchema, {
|
||||
rootKey: {
|
||||
objectKey: {
|
||||
arrayKey: ["foo"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.false(result.valid);
|
||||
t.deepEqual(
|
||||
result.invalidKeys.sort(),
|
||||
[".rootKey.objectKey.arrayKey[0]"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -30,6 +30,11 @@ export function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is a number. */
|
||||
export function isNumber(value: unknown): value is number {
|
||||
return typeof value === "number";
|
||||
}
|
||||
|
||||
/** Asserts that `value` is either a string or undefined. */
|
||||
export function isStringOrUndefined(
|
||||
value: unknown,
|
||||
@@ -43,28 +48,146 @@ export function isStringOrUndefined(
|
||||
*/
|
||||
export type Validator<T> = {
|
||||
validate: (val: unknown) => val is T;
|
||||
check: (
|
||||
val: unknown,
|
||||
opts: CheckSchemaOptions,
|
||||
path: string,
|
||||
) => CheckSchemaResult;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
function defaultCheck(
|
||||
validate: (val: unknown) => val is any,
|
||||
): (arg: unknown) => CheckSchemaResult {
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
|
||||
function makeValidator<T>(
|
||||
validate: (arg: unknown) => arg is T,
|
||||
required: boolean = true,
|
||||
) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/** Extracts `T` from `Validator<T>`. */
|
||||
export type UnwrapValidator<V> = V extends Validator<infer A> ? A : never;
|
||||
|
||||
/** A validator for string fields in schemas. */
|
||||
export const string = {
|
||||
validate: isString,
|
||||
required: true,
|
||||
} as const satisfies Validator<string>;
|
||||
export const string = makeValidator(isString);
|
||||
|
||||
/** Transforms a validator to be optional. */
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
/** A validator for number fields in schemas. */
|
||||
export const number = makeValidator(isNumber);
|
||||
|
||||
/** A validator for arrays. */
|
||||
export function array<T>(validator: Validator<T>) {
|
||||
const validate = (val: unknown) => {
|
||||
return isArray(val) && val.every((e) => validator.validate(e));
|
||||
};
|
||||
return {
|
||||
validate,
|
||||
check: (val: unknown, opts: CheckSchemaOptions, path: string) => {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// The value must be an array.
|
||||
if (!isArray(val)) {
|
||||
result.valid = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Validate all elements of the array.
|
||||
let index = 0;
|
||||
for (const e of val) {
|
||||
const elementPath = `${path}[${index}]`;
|
||||
const eResult = validator.check(e, opts, `${elementPath}`);
|
||||
|
||||
result.invalidKeys.push(...eResult.invalidKeys);
|
||||
result.unknownKeys.push(...eResult.unknownKeys);
|
||||
index++;
|
||||
|
||||
if (!eResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
// Add the element path to `invalidKeys` if we didn't get
|
||||
// any more specific ones from the element validator.
|
||||
if (eResult.invalidKeys.length === 0) {
|
||||
result.invalidKeys.push(elementPath);
|
||||
}
|
||||
|
||||
if (opts.failFast) {
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T[]>;
|
||||
}
|
||||
|
||||
/** A validator for objects. */
|
||||
export function object<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return isObject(val) && validateSchema<S, T>(schema, val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (!isObject(val)) {
|
||||
return invalidCheckSchema();
|
||||
}
|
||||
return checkSchema(schema, val, opts, path);
|
||||
},
|
||||
required: true,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` or `null` for an
|
||||
* absent value.
|
||||
*/
|
||||
export function optionalOrNull<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown) => {
|
||||
return val === undefined || val === null || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined || val === null) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a validator to be optional, accepting `undefined` for an absent
|
||||
* value but, unlike `optionalOrNull`, rejecting `null`.
|
||||
*/
|
||||
export function optional<T>(validator: Validator<T>) {
|
||||
return {
|
||||
validate: (val: unknown): val is T | undefined => {
|
||||
return val === undefined || validator.validate(val);
|
||||
},
|
||||
check: (val, opts, path) => {
|
||||
if (val === undefined) {
|
||||
return successfulCheckSchema();
|
||||
}
|
||||
return validator.check(val, opts, path);
|
||||
},
|
||||
required: false,
|
||||
} as const satisfies Validator<T | undefined>;
|
||||
}
|
||||
|
||||
/** Represents an arbitrary object schema. */
|
||||
export type Schema = Record<string, Validator<any>>;
|
||||
|
||||
@@ -90,28 +213,133 @@ export type FromSchema<S extends Schema> = {
|
||||
* @param obj The object to validate.
|
||||
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
|
||||
*/
|
||||
export function validateSchema<S extends Schema>(
|
||||
export function validateSchema<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedObject<any> = FromSchema<S>,
|
||||
>(schema: S, obj: UnvalidatedObject<any>): obj is T {
|
||||
const result = checkSchema(schema, obj, { failFast: true });
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
export interface CheckSchemaOptions {
|
||||
/** Whether to stop validation after the first error. */
|
||||
failFast?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckSchemaResult {
|
||||
/** Whether the `obj` satisfies the schema. */
|
||||
valid: boolean;
|
||||
/** Unknown keys that were found during validation. */
|
||||
unknownKeys: string[];
|
||||
/** Known keys that failed validation. */
|
||||
invalidKeys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: true`.
|
||||
*/
|
||||
function successfulCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: true,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to produce a `CheckSchemaResult` where `valid: false`.
|
||||
*/
|
||||
function invalidCheckSchema(): CheckSchemaResult {
|
||||
return {
|
||||
valid: false,
|
||||
unknownKeys: [],
|
||||
invalidKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function checkSchema<S extends Schema>(
|
||||
schema: S,
|
||||
obj: UnvalidatedObject<any>,
|
||||
): obj is FromSchema<S> {
|
||||
options: CheckSchemaOptions = {},
|
||||
path: string = "",
|
||||
): CheckSchemaResult {
|
||||
const result: CheckSchemaResult = successfulCheckSchema();
|
||||
|
||||
// Track the set of input keys. We remove keys from this set as we recognise them
|
||||
// during validation.
|
||||
const inputKeys = new Set(Object.keys(obj));
|
||||
|
||||
// Track keys that have failed validation, starting with the empty set.
|
||||
const invalidKeys = new Set();
|
||||
|
||||
// Loop through all keys in the object schema and validate that the given object
|
||||
// satisfies the schema key.
|
||||
for (const [key, validator] of Object.entries(schema)) {
|
||||
const hasKey = key in obj;
|
||||
|
||||
// Remove key from set of unrecognised keys.
|
||||
inputKeys.delete(key);
|
||||
|
||||
// Add the key to the set of invalid keys. We remove it later once
|
||||
// it passes validation.
|
||||
invalidKeys.add(key);
|
||||
|
||||
// If the property is required, but absent, fail.
|
||||
if (validator.required && !hasKey) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is required, but undefined or null, fail.
|
||||
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
|
||||
return false;
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the property is present, validate it.
|
||||
if (hasKey && !validator.validate(obj[key])) {
|
||||
return false;
|
||||
if (hasKey) {
|
||||
const checkResult = validator.check(obj[key], options, `${path}.${key}`);
|
||||
|
||||
result.unknownKeys.push(...checkResult.unknownKeys);
|
||||
result.invalidKeys.push(...checkResult.invalidKeys);
|
||||
|
||||
// If we have invalid keys from the validator, then that means that
|
||||
// we have a more specific key than `key`. Remove `key` from the results.
|
||||
if (checkResult.invalidKeys.length > 0) {
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
if (!checkResult.valid) {
|
||||
result.valid = false;
|
||||
|
||||
if (options.failFast) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this point, the key has been successfully validated.
|
||||
invalidKeys.delete(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
// If there are any remaining keys in `inputKeys`, add them to `unknownKeys`.
|
||||
for (const remainingKey of inputKeys) {
|
||||
result.unknownKeys.push(`${path}.${remainingKey}`);
|
||||
}
|
||||
|
||||
// If there are any remaining keys in `invalidKeys`, add them to the result.
|
||||
for (const invalidKey of invalidKeys) {
|
||||
result.invalidKeys.push(`${path}.${invalidKey}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
@@ -14,7 +15,7 @@ import { getRawLanguagesNoAutodetect } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { initCodeQL } from "./init";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
createStatusReportBase,
|
||||
getActionsStatus,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { ToolsDownloadStatusReport } from "./tools-download";
|
||||
import {
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
ConfigurationError,
|
||||
wrapError,
|
||||
checkActionVersion,
|
||||
getErrorMessage,
|
||||
} from "./util";
|
||||
|
||||
/**
|
||||
@@ -88,12 +87,13 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
|
||||
/** The main behaviour of this action. */
|
||||
async function run(startedAt: Date): Promise<void> {
|
||||
async function run({
|
||||
startedAt,
|
||||
logger,
|
||||
}: ActionState<["Logger"]>): Promise<void> {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
let codeql: CodeQL;
|
||||
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
|
||||
let toolsFeatureFlagsValid: boolean | undefined;
|
||||
@@ -195,20 +195,14 @@ async function run(startedAt: Date): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/** Defines the `setup-codeql` Action. */
|
||||
const setupCodeQL: Action = {
|
||||
name: ActionName.SetupCodeQL,
|
||||
run,
|
||||
};
|
||||
|
||||
/** Run the action and catch any unhandled errors. */
|
||||
export async function runWrapper(): Promise<void> {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(`setup-codeql action failed: ${getErrorMessage(error)}`);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.SetupCodeQL,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(setupCodeQL);
|
||||
await checkForTimeout();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
|
||||
@@ -637,8 +638,8 @@ test.serial(
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env["CODE_SCANNING_REF"] = "refs/heads/feature-branch";
|
||||
process.env["CODE_SCANNING_BASE_BRANCH"] = "main";
|
||||
process.env[EnvVar.CODE_SCANNING_REF] = "refs/heads/feature-branch";
|
||||
process.env[EnvVar.CODE_SCANNING_BASE_BRANCH] = "main";
|
||||
|
||||
sinon.stub(api, "getAutomationID").resolves("test/");
|
||||
const listStub = sinon.stub(api, "listActionsCaches").resolves([
|
||||
|
||||
@@ -120,9 +120,14 @@ const mixedCredentials = [
|
||||
{ type: "maven_repository", host: "maven.pkg.github.com", token: "def" },
|
||||
{ type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" },
|
||||
{ type: "goproxy_server", host: "goproxy.example.com", token: "jkl" },
|
||||
{ type: "git_source", host: "github.com/github", token: "mno" },
|
||||
];
|
||||
|
||||
const gitSourceCredential = {
|
||||
type: "git_source",
|
||||
host: "github.com/github",
|
||||
token: "mno",
|
||||
};
|
||||
|
||||
test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([
|
||||
@@ -241,7 +246,7 @@ test("getCredentials returns all for a language when specified", async (t) => {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
toEncodedJSON(mixedCredentials),
|
||||
toEncodedJSON([...mixedCredentials, gitSourceCredential]),
|
||||
BuiltInLanguage.go,
|
||||
);
|
||||
t.is(credentials.length, 2);
|
||||
@@ -284,7 +289,7 @@ test("getCredentials returns all maven_repositories for Java when specified", as
|
||||
host: "maven2.pkg.github.com",
|
||||
token: "token2",
|
||||
},
|
||||
{ type: "git_source", host: "github.com/github", token: "mno" },
|
||||
{ type: "goproxy_server", host: "github.com/github", token: "mno" },
|
||||
];
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
@@ -624,8 +629,11 @@ test("getCredentials validates 'replaces-base' correctly", async (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("getCredentials returns no credentials for Actions", async (t) => {
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
test("getCredentials returns only ALWAYS_ENABLED_REGISTRY_TYPE credentials for Actions", async (t) => {
|
||||
const credentialsInput = toEncodedJSON([
|
||||
...mixedCredentials,
|
||||
gitSourceCredential,
|
||||
]);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
@@ -633,7 +641,41 @@ test("getCredentials returns no credentials for Actions", async (t) => {
|
||||
credentialsInput,
|
||||
BuiltInLanguage.actions,
|
||||
);
|
||||
t.deepEqual(credentials, []);
|
||||
|
||||
for (const credential of credentials) {
|
||||
t.true(
|
||||
startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE.some(
|
||||
(ty) => ty === credential.type,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials always returns ALWAYS_ENABLED_REGISTRY_TYPE credentials for all languages", async (t) => {
|
||||
const alwaysEnabledCredentials: startProxyExports.Credential[] = [];
|
||||
|
||||
for (const alwaysEnabled of startProxyExports.ALWAYS_ENABLED_REGISTRY_TYPE) {
|
||||
alwaysEnabledCredentials.push({
|
||||
type: alwaysEnabled,
|
||||
host: `host-${alwaysEnabled}`,
|
||||
token: `bar-${alwaysEnabled}`,
|
||||
url: `url-${alwaysEnabled}`,
|
||||
});
|
||||
}
|
||||
|
||||
const credentialsInput = toEncodedJSON(alwaysEnabledCredentials);
|
||||
|
||||
// Test all languages.
|
||||
for (const language of Object.values(BuiltInLanguage)) {
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
credentialsInput,
|
||||
language,
|
||||
);
|
||||
|
||||
t.deepEqual(credentials, alwaysEnabledCredentials);
|
||||
}
|
||||
});
|
||||
|
||||
function mockGetApiClient(endpoints: any) {
|
||||
|
||||
@@ -187,9 +187,16 @@ function isPAT(value: string) {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of always-enabled registry types. The registry types in this list are always
|
||||
* enabled, because generic CodeQL workflow components may use them rather than just
|
||||
* language-specific components.
|
||||
*/
|
||||
export const ALWAYS_ENABLED_REGISTRY_TYPE = ["git_source"] as const;
|
||||
|
||||
type RegistryMapping = Partial<Record<BuiltInLanguage, string[]>>;
|
||||
|
||||
const LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
|
||||
export const LANGUAGE_TO_REGISTRY_TYPE: Required<RegistryMapping> = {
|
||||
actions: [],
|
||||
cpp: [],
|
||||
java: ["maven_repository"],
|
||||
@@ -233,9 +240,11 @@ function getRegistryAddress(
|
||||
}
|
||||
}
|
||||
|
||||
// getCredentials returns registry credentials from action inputs.
|
||||
// It prefers `registries_credentials` over `registry_secrets`.
|
||||
// If neither is set, it returns an empty array.
|
||||
/**
|
||||
* Returns registry credentials from action inputs.
|
||||
* It prefers `registriesCredentials` over `registrySecrets`.
|
||||
* If neither is set, it returns an empty array.
|
||||
*/
|
||||
export function getCredentials(
|
||||
logger: Logger,
|
||||
registrySecrets: string | undefined,
|
||||
@@ -291,8 +300,11 @@ export function getCredentials(
|
||||
const address = getRegistryAddress(e);
|
||||
|
||||
// Filter credentials based on language if specified. `type` is the registry type.
|
||||
// E.g., "maven_feed" for Java/Kotlin, "nuget_repository" for C#.
|
||||
// E.g., "maven_repository" for Java/Kotlin, "nuget_feed" for C#.
|
||||
// We always allow types in `ALWAYS_ENABLED_REGISTRY_TYPE` since they can be used by
|
||||
// other parts of the workflow.
|
||||
if (
|
||||
!ALWAYS_ENABLED_REGISTRY_TYPE.some((t) => t === e.type) &&
|
||||
registryTypeForLanguage &&
|
||||
!registryTypeForLanguage.some((t) => t === e.type)
|
||||
) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export type RawCredential = UnvalidatedObject<Credential>;
|
||||
/** A schema for credential objects with a username. */
|
||||
export const usernameSchema = {
|
||||
/** The username needed to authenticate to the package registry, if any. */
|
||||
username: json.optional(json.string),
|
||||
username: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Usernames may be present for both authentication with tokens or passwords. */
|
||||
@@ -29,7 +29,7 @@ export function hasUsername(config: AuthConfig): config is Username {
|
||||
/** A schema for credential objects with a username and password. */
|
||||
export const usernamePasswordSchema = {
|
||||
/** The password needed to authenticate to the package registry, if any. */
|
||||
password: json.optional(json.string),
|
||||
password: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -52,7 +52,7 @@ export function hasUsernameAndPassword(
|
||||
/** A schema for credential objects for token-based authentication. */
|
||||
export const tokenSchema = {
|
||||
/** The token needed to authenticate to the package registry, if any. */
|
||||
token: json.optional(json.string),
|
||||
token: json.optionalOrNull(json.string),
|
||||
...usernameSchema,
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
@@ -100,7 +100,7 @@ export const awsConfigSchema = {
|
||||
"role-name": json.string,
|
||||
domain: json.string,
|
||||
"domain-owner": json.string,
|
||||
audience: json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for AWS OIDC. */
|
||||
@@ -116,8 +116,8 @@ export function isAWSConfig(
|
||||
/** A schema for JFrog OIDC configurations. */
|
||||
export const jfrogConfigSchema = {
|
||||
"jfrog-oidc-provider-name": json.string,
|
||||
audience: json.optional(json.string),
|
||||
"identity-mapping-name": json.optional(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
"identity-mapping-name": json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for JFrog OIDC. */
|
||||
@@ -150,8 +150,8 @@ export function isCloudsmithConfig(
|
||||
/** A schema for GCP OIDC configurations. */
|
||||
export const gcpConfigSchema = {
|
||||
"workload-identity-provider": json.string,
|
||||
"service-account": json.optional(json.string),
|
||||
audience: json.optional(json.string),
|
||||
"service-account": json.optionalOrNull(json.string),
|
||||
audience: json.optionalOrNull(json.string),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Configuration for GCP OIDC. */
|
||||
|
||||
@@ -12,19 +12,20 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import { parseRegistriesWithoutCredentials, type Config } from "./config-utils";
|
||||
import { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import { CommandCacheKey, getCachedCommandOutput } from "./cli/output-cache";
|
||||
import type { Config } from "./config/action-config";
|
||||
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
||||
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { getRef } from "./git-utils";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import type { Logger } from "./logging";
|
||||
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import type { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
ConfigurationError,
|
||||
getRequiredEnvParam,
|
||||
getCachedCodeQlVersion,
|
||||
isInTestMode,
|
||||
GITHUB_DOTCOM_URL,
|
||||
DiskUsage,
|
||||
@@ -46,6 +47,17 @@ export enum ActionName {
|
||||
UploadSarif = "upload-sarif",
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an `ActionName` to its display name. Usually that is the same, except
|
||||
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
|
||||
*/
|
||||
export function getDisplayActionName(actionName: ActionName): string {
|
||||
if (actionName === ActionName.Analyze) {
|
||||
return "analyze";
|
||||
}
|
||||
return actionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a boolean indicating whether the analysis is considered to be first party.
|
||||
*
|
||||
@@ -283,7 +295,10 @@ export async function createStatusReportBase(
|
||||
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
|
||||
}
|
||||
const runnerOs = getRequiredEnvParam("RUNNER_OS");
|
||||
const codeQlCliVersion = getCachedCodeQlVersion();
|
||||
const codeQlCliVersion = getCachedCommandOutput(
|
||||
CommandCacheKey.Version,
|
||||
undefined,
|
||||
);
|
||||
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
|
||||
const testingEnvironment = getTestingEnvironment();
|
||||
// re-export the testing environment variable so that it is available to subsequent steps,
|
||||
|
||||
@@ -3,6 +3,8 @@ import path from "path";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
import test, {
|
||||
type ThrownError,
|
||||
type ThrowsExpectation,
|
||||
type ExecutionContext,
|
||||
type MacroDeclarationOptions,
|
||||
type TestFn,
|
||||
@@ -10,14 +12,18 @@ import test, {
|
||||
import nock from "nock";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState, StateFeature } from "./action-common";
|
||||
import { ActionsEnv, getActionVersion } from "./actions-util";
|
||||
import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
import { GitHubApiDetails } from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import type { VersionInfo } from "./cli/output-cache";
|
||||
import { resetCachedCommandOutputs } from "./cli/output-cache";
|
||||
import * as codeql from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { Env, ActionsEnvVars } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -26,13 +32,14 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import { ActionName } from "./status-report";
|
||||
import {
|
||||
DEFAULT_DEBUG_ARTIFACT_NAME,
|
||||
DEFAULT_DEBUG_DATABASE_NAME,
|
||||
getEnv,
|
||||
GitHubVariant,
|
||||
GitHubVersion,
|
||||
HTTPError,
|
||||
resetCachedCodeQlVersion,
|
||||
} from "./util";
|
||||
|
||||
export const SAMPLE_DOTCOM_API_DETAILS = {
|
||||
@@ -102,9 +109,9 @@ export function setupTests(testFn: TestFn<any>) {
|
||||
// unless the test explicitly sets one up.
|
||||
codeql.setCodeQL({});
|
||||
|
||||
// Reset the in-process CodeQL version cache so that it doesn't leak between
|
||||
// tests, which each represent a separate Actions step in production.
|
||||
resetCachedCodeQlVersion();
|
||||
// Reset the in-process CodeQL command-output cache so that it doesn't leak
|
||||
// between tests, which each represent a separate Actions step in production.
|
||||
resetCachedCommandOutputs();
|
||||
|
||||
// Replace stdout and stderr so we can record output during tests
|
||||
t.context.testOutput = "";
|
||||
@@ -172,6 +179,11 @@ export function makeMacro<Args extends unknown[]>(
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export function getTestEnv(): Env {
|
||||
const testEnv: NodeJS.ProcessEnv = {};
|
||||
return getEnv(testEnv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an `ActionsEnv` instance for use in tests.
|
||||
*/
|
||||
@@ -181,6 +193,264 @@ export function getTestActionsEnv(): ActionsEnv {
|
||||
};
|
||||
}
|
||||
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
type AllState = ["Logger", "Env", "Actions", "FeatureFlags"];
|
||||
|
||||
/** Initialise a fresh `ActionState<AllState>` value. */
|
||||
export function initAllState(
|
||||
overrides?: Partial<ActionState<AllState>>,
|
||||
): ActionState<AllState> {
|
||||
return {
|
||||
name: ActionName.Init,
|
||||
startedAt: new Date(),
|
||||
logger: new RecordingLogger(),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
features: createFeatures([]),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
type DelayedCheck<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
|
||||
|
||||
export type ValueOrMutation<T> = T | ((val: T) => void);
|
||||
|
||||
/**
|
||||
* Wraps a function that accepts an `ActionState` for testing in different environments.
|
||||
*/
|
||||
abstract class BaseEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> {
|
||||
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
|
||||
private logger: RecordingLogger;
|
||||
protected state: ActionState<AllState>;
|
||||
protected checks: Array<DelayedCheck<Args, R, Fs>>;
|
||||
|
||||
constructor(
|
||||
fn: (state: ActionState<Fs>, ...args: Args) => R,
|
||||
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.logger = new RecordingLogger();
|
||||
this.state =
|
||||
cloneFrom !== undefined
|
||||
? ({
|
||||
...cloneFrom.state,
|
||||
env: Object.create(cloneFrom.state.env),
|
||||
actions: Object.create(cloneFrom.state.actions),
|
||||
logger: this.logger,
|
||||
} satisfies ActionState<AllState>)
|
||||
: initAllState({ logger: this.logger });
|
||||
this.checks = [...(cloneFrom?.checks ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone of this object. Used internally.
|
||||
* Must be overridden by subclasses.
|
||||
*/
|
||||
protected abstract clone(): this;
|
||||
|
||||
public getLogger(): RecordingLogger {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
public getState(): ActionState<AllState> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public withArgs(...args: Args): CallableEnvBuilder<Args, R, Fs> {
|
||||
const result = new CallableEnvBuilder(this.fn, args, this.clone());
|
||||
return result;
|
||||
}
|
||||
|
||||
public withFeatures(enabled: Feature[]): this {
|
||||
const result = this.clone();
|
||||
result.state.features = createFeatures(enabled);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets environment variables that are always available to GitHub Actions,
|
||||
* excluding some that are expected to be set to paths.
|
||||
*
|
||||
* @param overrides Overrides for the defaults.
|
||||
*/
|
||||
public withDefaultActionsEnv(overrides?: ActionVarOverrides): this {
|
||||
const result = this.clone();
|
||||
setupBaseActionsVars(overrides, result.state.env);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets environment variables that are always available to GitHub Actions.
|
||||
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
|
||||
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
|
||||
* @param overrides Overrides for the defaults.
|
||||
*/
|
||||
public withActionsEnv(
|
||||
tempDir: string,
|
||||
toolsDir: string,
|
||||
overrides?: ActionVarOverrides,
|
||||
): this {
|
||||
const result = this.clone();
|
||||
setupActionsVars(tempDir, toolsDir, overrides, result.state.env);
|
||||
return result;
|
||||
}
|
||||
|
||||
public withEnv(arg: ValueOrMutation<Env>): this {
|
||||
const result = this.clone();
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.env);
|
||||
} else {
|
||||
result.state.env = arg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
|
||||
const result = this.clone();
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.actions);
|
||||
} else {
|
||||
result.state.actions = arg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that `messages` are logged. The check will be
|
||||
* performed after the main assertion passes.
|
||||
*/
|
||||
public logs(t: ExecutionContext<unknown>, ...messages: string[]): this {
|
||||
const result = this.clone();
|
||||
result.checks.push(async (env) => {
|
||||
checkExpectedLogMessages(t, env.getLogger().messages, messages);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a delayed check that `messages` are not logged. The check will be
|
||||
* performed after the main assertion passes.
|
||||
*/
|
||||
public notLogs(t: ExecutionContext<unknown>, ...messages: string[]): this {
|
||||
const result = this.clone();
|
||||
result.checks.push(async (env) => {
|
||||
checkUnexpectedLogMessages(t, env.getLogger().messages, messages);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class EnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> extends BaseEnvBuilder<Args, R, Fs> {
|
||||
protected clone(): this {
|
||||
return new EnvBuilder(this.fn, this) as this;
|
||||
}
|
||||
}
|
||||
|
||||
class CallableEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> extends BaseEnvBuilder<Args, R, Fs> {
|
||||
private args: Args;
|
||||
|
||||
constructor(
|
||||
fn: (state: ActionState<Fs>, ...args: Args) => R,
|
||||
args: Args,
|
||||
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
|
||||
) {
|
||||
super(fn, cloneFrom);
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
protected clone(): this {
|
||||
return new CallableEnvBuilder(this.fn, this.args, this) as this;
|
||||
}
|
||||
|
||||
public getArgs(): Args {
|
||||
return this.args;
|
||||
}
|
||||
|
||||
call(): R {
|
||||
return this.fn(this.state as unknown as ActionState<Fs>, ...this.args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the underlying function in the configured environment and passes
|
||||
* the result to `assertion` along with extra `assertionArgs`.
|
||||
*
|
||||
* @param assertion The assertion to apply to the result.
|
||||
* @param assertionArgs Extra arguments for the assertion.
|
||||
* @returns The result of the assertion.
|
||||
*/
|
||||
public async passes<AArgs extends readonly any[], AResult>(
|
||||
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
|
||||
...assertionArgs: AArgs
|
||||
): Promise<AResult> {
|
||||
// this.call() may or may not return a promise,
|
||||
// `Promise.resolve` turns the result into one if it isn't already,
|
||||
// and we then await it. That ensures that `result` is an `Awaited<R>`.
|
||||
const result = await Promise.resolve(this.call());
|
||||
|
||||
// Run the main assertion on the `result`.
|
||||
const assertionResult = await assertion(result, ...assertionArgs);
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the result of the main assertion.
|
||||
return assertionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that calling the underlying function should throw an exception.
|
||||
*
|
||||
* @param t The execution context for the assertion.
|
||||
* @param expectations Expectations for the error.
|
||||
* @returns The error that was thrown.
|
||||
*/
|
||||
public async throws<ErrorType extends ErrorConstructor | Error>(
|
||||
t: ExecutionContext<unknown>,
|
||||
expectations?: ThrowsExpectation<ErrorType>,
|
||||
): Promise<ThrownError<ErrorType>> {
|
||||
// Run the main assertion.
|
||||
const error = await t.throwsAsync(
|
||||
async () => Promise.resolve(this.call()),
|
||||
expectations,
|
||||
);
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the error.
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility function to construct a `TestEnv`. */
|
||||
export function callee<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends readonly StateFeature[],
|
||||
>(fn: (state: ActionState<Fs>, ...args: Args) => R): EnvBuilder<Args, R, Fs> {
|
||||
return new EnvBuilder(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for environment variables typically set in an Actions
|
||||
* environment. Tests can override individual variables by passing them in the
|
||||
@@ -200,7 +470,7 @@ export const DEFAULT_ACTIONS_VARS = {
|
||||
GITHUB_WORKFLOW: "test-workflow",
|
||||
RUNNER_NAME: "my-runner",
|
||||
RUNNER_OS: "Linux",
|
||||
} as const satisfies Record<string, string>;
|
||||
} as const satisfies Partial<Record<ActionsEnvVars, string>>;
|
||||
|
||||
/** Partial mappings from GitHub Actions environment variables to values. */
|
||||
export type ActionVarOverrides = Partial<
|
||||
@@ -212,11 +482,15 @@ export type ActionVarOverrides = Partial<
|
||||
* excluding some that are expected to be set to paths. See `setupActionsVars`.
|
||||
*
|
||||
* @param overrides Overrides for the defaults.
|
||||
* @param env The environment to set the variables for.
|
||||
*/
|
||||
export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
|
||||
export function setupBaseActionsVars(
|
||||
overrides?: ActionVarOverrides,
|
||||
env: Env = getEnv(),
|
||||
) {
|
||||
const vars = { ...DEFAULT_ACTIONS_VARS, ...overrides };
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
process.env[key] = value;
|
||||
env.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,16 +500,18 @@ export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
|
||||
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
|
||||
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
|
||||
* @param overrides Overrides for the defaults.
|
||||
* @param env The environment to set the variables for.
|
||||
*/
|
||||
export function setupActionsVars(
|
||||
tempDir: string,
|
||||
toolsDir: string,
|
||||
overrides?: ActionVarOverrides,
|
||||
env: Env = getEnv(),
|
||||
) {
|
||||
setupBaseActionsVars(overrides);
|
||||
process.env["RUNNER_TEMP"] = tempDir;
|
||||
process.env["RUNNER_TOOL_CACHE"] = toolsDir;
|
||||
process.env["GITHUB_WORKSPACE"] = tempDir;
|
||||
setupBaseActionsVars(overrides, env);
|
||||
env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
|
||||
env.set(ActionsEnvVars.RUNNER_TOOL_CACHE, toolsDir);
|
||||
env.set(ActionsEnvVars.GITHUB_WORKSPACE, tempDir);
|
||||
}
|
||||
|
||||
type LogLevel = "debug" | "info" | "warning" | "error";
|
||||
@@ -369,6 +645,34 @@ export function checkExpectedLogMessages(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that `messages` contains none of `unexpectedMessages`.
|
||||
*/
|
||||
export function checkUnexpectedLogMessages(
|
||||
t: ExecutionContext<any>,
|
||||
messages: LoggedMessage[],
|
||||
unexpectedMessages: string[],
|
||||
) {
|
||||
const presentMessages: string[] = [];
|
||||
|
||||
for (const unexpectedMessage of unexpectedMessages) {
|
||||
if (hasLoggedMessage(messages, unexpectedMessage)) {
|
||||
presentMessages.push(unexpectedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (presentMessages.length > 0) {
|
||||
const listify = (lines: string[]) =>
|
||||
lines.map((m) => ` - '${m}'`).join("\n");
|
||||
|
||||
t.fail(
|
||||
`Did not expect\n\n${listify(presentMessages)}\n\nin the logger output, but found them in:\n\n${messages.map((m) => ` - '${m.message}'`).join("\n")}`,
|
||||
);
|
||||
} else {
|
||||
t.pass();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that `message` should not have been logged to `logger`.
|
||||
*/
|
||||
@@ -479,7 +783,7 @@ export const makeVersionInfo = (
|
||||
version: string,
|
||||
features?: { [name: string]: boolean },
|
||||
overlayVersion?: number,
|
||||
): codeql.VersionInfo => ({
|
||||
): VersionInfo => ({
|
||||
version,
|
||||
features,
|
||||
overlayVersion,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import type { VersionInfo } from "./codeql";
|
||||
import type { VersionInfo } from "./cli/output-cache";
|
||||
|
||||
export enum ToolsFeature {
|
||||
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import { InvalidSarifUploadError } from "./sarif";
|
||||
import {
|
||||
createStatusReportBase,
|
||||
sendStatusReport,
|
||||
sendUnhandledErrorStatusReport,
|
||||
StatusReportBase,
|
||||
getActionsStatus,
|
||||
ActionName,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
ConfigurationError,
|
||||
checkActionVersion,
|
||||
checkDiskUsage,
|
||||
getErrorMessage,
|
||||
initializeEnvironment,
|
||||
shouldSkipSarifUpload,
|
||||
wrapError,
|
||||
@@ -55,12 +54,9 @@ async function sendSuccessStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(startedAt: Date) {
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
const logger = getActionsLogger();
|
||||
|
||||
try {
|
||||
initializeEnvironment(getActionVersion());
|
||||
|
||||
@@ -165,20 +161,12 @@ async function run(startedAt: Date) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the `upload-sarif` Action. */
|
||||
const uploadSarif: Action = {
|
||||
name: ActionName.UploadSarif,
|
||||
run,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
try {
|
||||
await run(startedAt);
|
||||
} catch (error) {
|
||||
core.setFailed(
|
||||
`codeql/upload-sarif action failed: ${getErrorMessage(error)}`,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(
|
||||
ActionName.UploadSarif,
|
||||
startedAt,
|
||||
error,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
await runInActions(uploadSarif);
|
||||
}
|
||||
|
||||
@@ -532,58 +532,3 @@ test("Failure.orElse returns the default value for a failure result", (t) => {
|
||||
const result = new util.Failure(new Error("test error"));
|
||||
t.is(result.orElse("default value"), "default value");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql"), {
|
||||
version: "2.20.0",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = "not valid json";
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
(t) => {
|
||||
for (const value of [
|
||||
JSON.stringify({ cmd: "/path/to/codeql" }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: {} }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: { version: 2 } }),
|
||||
JSON.stringify({ version: { version: "2.20.0" } }),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", overlayVersion: "1" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", features: "nope" },
|
||||
}),
|
||||
]) {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = value;
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined, value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
121
src/util.ts
121
src/util.ts
@@ -10,14 +10,17 @@ import * as yaml from "js-yaml";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { CodeQL } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getRequiredEnvParam } from "./environment";
|
||||
import * as json from "./json";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
|
||||
// Re-export for backwards compatibility to avoid updating a lot of imports elsewhere.
|
||||
export { getRequiredEnvParam, getOptionalEnvVar, getEnv } from "./environment";
|
||||
|
||||
/**
|
||||
* The name of the file containing the base database OIDs, as stored in the
|
||||
* root of the database location.
|
||||
@@ -566,28 +569,6 @@ export function initializeEnvironment(version: string) {
|
||||
core.exportVariable(EnvVar.VERSION, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment parameter, but throw an error if it is not set.
|
||||
*/
|
||||
export function getRequiredEnvParam(paramName: string): string {
|
||||
const value = process.env[paramName];
|
||||
if (value === undefined || value.length === 0) {
|
||||
throw new Error(`${paramName} environment variable must be set`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an environment variable, but return `undefined` if it is not set or empty.
|
||||
*/
|
||||
export function getOptionalEnvVar(paramName: string): string | undefined {
|
||||
const value = process.env[paramName];
|
||||
if (value?.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export class HTTPError extends Error {
|
||||
public status: number;
|
||||
|
||||
@@ -617,90 +598,6 @@ export function asHTTPError(arg: any): HTTPError | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/** The persisted version together with the CLI path it was obtained from. */
|
||||
interface PersistedVersionInfo {
|
||||
cmd: string;
|
||||
version: VersionInfo;
|
||||
}
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
|
||||
const candidate = x as Partial<PersistedVersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
isVersionInfo(candidate.version)
|
||||
);
|
||||
}
|
||||
|
||||
export function cacheCodeQlVersion(cmd: string, version: VersionInfo): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// 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.
|
||||
core.exportVariable(
|
||||
EnvVar.CODEQL_VERSION_INFO,
|
||||
JSON.stringify({ cmd, version }),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
const serialized = process.env[EnvVar.CODEQL_VERSION_INFO];
|
||||
if (!serialized) {
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isPersistedVersionInfo(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.version;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
export async function codeQlVersionAtLeast(
|
||||
codeql: CodeQL,
|
||||
requiredVersion: string,
|
||||
@@ -712,7 +609,13 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
|
||||
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
|
||||
}
|
||||
|
||||
// Create a bundle for the given DB, if it doesn't already exist
|
||||
/**
|
||||
* Bundles the database for the given language into a `.zip` file, returning the path to it.
|
||||
*
|
||||
* If a bundle for `dbName` already exists (e.g. from an earlier call), it is deleted and
|
||||
* re-created, so each call produces a fresh bundle reflecting the current database contents and the
|
||||
* given `includeDiagnostics` value.
|
||||
*/
|
||||
export async function bundleDb(
|
||||
config: Config,
|
||||
language: Language,
|
||||
|
||||
Reference in New Issue
Block a user