mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 09:14:58 +00:00
Merge pull request #4022 from github/backport-v3.37.1-7188fc363
Merge releases/v4 into releases/v3
This commit is contained in:
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@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.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@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
2
.github/workflows/__config-input.yml
generated
vendored
2
.github/workflows/__config-input.yml
generated
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-codescanning-config-inputs-js.yml
generated
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-config-inputs-js.yml
generated
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
2
.github/workflows/__packaging-config-js.yml
generated
vendored
2
.github/workflows/__packaging-config-js.yml
generated
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
2
.github/workflows/__packaging-inputs-js.yml
generated
vendored
@@ -80,7 +80,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
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@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
2
.github/workflows/post-release-mergeback.yml
vendored
2
.github/workflows/post-release-mergeback.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0 # ensure we have all tags and can push commits
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
4
.github/workflows/pr-checks.yml
vendored
4
.github/workflows/pr-checks.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
2
.github/workflows/query-filters.yml
vendored
2
.github/workflows/query-filters.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
2
.github/workflows/rebuild.yml
vendored
2
.github/workflows/rebuild.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
ref: ${{ env.HEAD_REF }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
2
.github/workflows/update-bundle.yml
vendored
2
.github/workflows/update-bundle.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## 3.37.1 - 16 Jul 2026
|
||||
|
||||
- _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)
|
||||
- Update default CodeQL bundle version to [2.26.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1). [#4019](https://github.com/github/codeql-action/pull/4019)
|
||||
|
||||
## 3.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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
1782
lib/entry-points.js
generated
1782
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
260
package-lock.json
generated
260
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.0",
|
||||
"version": "3.37.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codeql",
|
||||
"version": "4.37.0",
|
||||
"version": "3.37.1",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks"
|
||||
@@ -22,13 +22,16 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.1.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -47,21 +50,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.17.0",
|
||||
"eslint-plugin-github": "^6.1.0",
|
||||
"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.7.0",
|
||||
"nock": "^14.0.15",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.0"
|
||||
"typescript-eslint": "^8.63.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2572,9 +2575,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": {
|
||||
@@ -2587,17 +2590,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz",
|
||||
"integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
|
||||
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.62.0",
|
||||
"@typescript-eslint/type-utils": "8.62.0",
|
||||
"@typescript-eslint/utils": "8.62.0",
|
||||
"@typescript-eslint/visitor-keys": "8.62.0",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/type-utils": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2610,7 +2613,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -2626,16 +2629,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz",
|
||||
"integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.62.0",
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/typescript-estree": "8.62.0",
|
||||
"@typescript-eslint/visitor-keys": "8.62.0",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2669,14 +2672,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz",
|
||||
"integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.62.0",
|
||||
"@typescript-eslint/types": "^8.62.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2709,14 +2712,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz",
|
||||
"integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/visitor-keys": "8.62.0"
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2727,9 +2730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz",
|
||||
"integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2744,15 +2747,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz",
|
||||
"integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/typescript-estree": "8.62.0",
|
||||
"@typescript-eslint/utils": "8.62.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2787,9 +2790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz",
|
||||
"integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2801,16 +2804,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz",
|
||||
"integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.62.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.62.0",
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/visitor-keys": "8.62.0",
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2886,16 +2889,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz",
|
||||
"integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
|
||||
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.62.0",
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/typescript-estree": "8.62.0"
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2910,13 +2913,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz",
|
||||
"integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4984,13 +4987,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.0.0.tgz",
|
||||
"integrity": "sha512-J8MvUoiR/TU/Y9NnEmg1AnbvMUj9R6IO260z47zymMLLvso7B4c80IKjd8diqmqtSmeXXlbIus4i0SvK84flag==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.1.0.tgz",
|
||||
"integrity": "sha512-+mA0K1/I1JSE9AOiJ4ifMDGu7NplRZX0e3Uy0SjbwyXb2rsDfo5yfT0UCUO+TiOJ/99R1YxFRltizP/PZuB4PQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@github/browserslist-config": "^1.0.0",
|
||||
@@ -5007,79 +5010,18 @@
|
||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-rule-documentation": ">=1.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"globals": "^17.7.0",
|
||||
"jsx-ast-utils": "^3.3.2",
|
||||
"prettier": "^3.0.0",
|
||||
"svg-element-attributes": "^1.3.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8 || ^9"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
|
||||
"integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.40 || 9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/core": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/globals": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
|
||||
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"eslint": "^8 || ^9 || ^10"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-i18n-text": {
|
||||
@@ -5125,9 +5067,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.0.tgz",
|
||||
"integrity": "sha512-aM7V25Bg6YuYxtEhwjafzfS0NTMds1D2PMQI0K4KqJxQJRtkP4CO+MQTWRdBq2qAnmPxTxLevhXUBtByxJqS1w==",
|
||||
"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": {
|
||||
@@ -6978,9 +6920,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.1.0.tgz",
|
||||
"integrity": "sha512-s8VA5jkR8f22S3NAXmhKPFqGUduqZGlsufabVOgN14iTdw/RXcym7bKkbwjxLK9Yw2lEvvmJjFp119+KPeo8Kg==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7443,9 +7385,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": {
|
||||
@@ -8896,9 +8838,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": {
|
||||
@@ -9165,9 +9107,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
|
||||
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9317,16 +9259,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.62.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz",
|
||||
"integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
|
||||
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@typescript-eslint/typescript-estree": "8.62.0",
|
||||
"@typescript-eslint/utils": "8.62.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -9360,9 +9302,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"
|
||||
@@ -9815,7 +9757,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.22.4"
|
||||
"tsx": "^4.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "3.37.0",
|
||||
"version": "3.37.1",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -30,13 +30,16 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.1.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -55,21 +58,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.17.0",
|
||||
"eslint-plugin-github": "^6.1.0",
|
||||
"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.7.0",
|
||||
"nock": "^14.0.15",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.0"
|
||||
"typescript-eslint": "^8.63.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -5,7 +5,7 @@ versions:
|
||||
- default
|
||||
steps:
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.22.4"
|
||||
"tsx": "^4.23.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +211,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Node.js",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-node",
|
||||
"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e",
|
||||
"v6.4.0",
|
||||
"820762786026740c76f36085b0efc47a31fe5020",
|
||||
"v7.0.0",
|
||||
),
|
||||
with: {
|
||||
"node-version": defaultLanguageVersions.javascript,
|
||||
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"1bcf9fb12cf4aa7d266a90ae39939e61372fe520",
|
||||
"v5.4.0",
|
||||
"0f481fcb613427c0f801b606911222b5b6f3083a",
|
||||
"v5.5.0",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { ActionsEnv, getActionsEnv } from "./actions-util";
|
||||
import { Env } from "./environment";
|
||||
import { FeatureEnablement } from "./feature-flags";
|
||||
import type { ApiClient } from "./api-client";
|
||||
import { Env, ReadOnlyEnv } from "./environment";
|
||||
import type { FeatureEnablement } from "./feature-flags";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import {
|
||||
ActionName,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage } from "./util";
|
||||
|
||||
/** Common state that is always available in `ActionState`. */
|
||||
/** Base state that is available to an Action on startup. */
|
||||
export interface BaseState {
|
||||
/** The name of the Action. */
|
||||
name: ActionName;
|
||||
@@ -21,6 +22,7 @@ export interface BaseState {
|
||||
|
||||
/** Describes different state features that an Action may have. */
|
||||
export interface FeatureState {
|
||||
Base: BaseState;
|
||||
Logger: {
|
||||
/** The logger that is in use. */
|
||||
logger: Logger;
|
||||
@@ -29,10 +31,17 @@ export interface FeatureState {
|
||||
/** Information about environment variables. */
|
||||
env: Env;
|
||||
};
|
||||
ReadOnlyEnv: {
|
||||
env: ReadOnlyEnv;
|
||||
};
|
||||
Actions: {
|
||||
/** Access to Actions-related functionality. */
|
||||
actions: ActionsEnv;
|
||||
};
|
||||
Api: {
|
||||
/** A GitHub API client. */
|
||||
apiClient: ApiClient;
|
||||
};
|
||||
FeatureFlags: {
|
||||
/** Information about enabled feature flags. */
|
||||
features: FeatureEnablement;
|
||||
@@ -44,7 +53,7 @@ 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
|
||||
? Record<never, never>
|
||||
: Fs extends [
|
||||
infer Head extends StateFeature,
|
||||
...infer Tail extends readonly StateFeature[],
|
||||
@@ -60,7 +69,7 @@ export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
|
||||
* Each Action can then augment the `state` further if additional features are required.
|
||||
*/
|
||||
export type ActionMain = (
|
||||
state: ActionState<["Logger", "Env", "Actions"]>,
|
||||
state: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
) => Promise<void>;
|
||||
|
||||
/** A specification for a CodeQL Action step. */
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
@@ -21,28 +22,6 @@ import {
|
||||
*/
|
||||
declare const __CODEQL_ACTION_VERSION__: string;
|
||||
|
||||
/**
|
||||
* 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",
|
||||
RUNNER_NAME = "RUNNER_NAME",
|
||||
RUNNER_OS = "RUNNER_OS",
|
||||
RUNNER_TEMP = "RUNNER_TEMP",
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracts over GitHub Actions functions so that we do not have to stub
|
||||
* global functions in tests.
|
||||
@@ -83,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(ActionsEnvVars.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 {
|
||||
@@ -105,16 +88,16 @@ export function getActionVersion(): string {
|
||||
*
|
||||
* This will be "dynamic" for default setup workflow runs.
|
||||
*/
|
||||
export function getWorkflowEventName() {
|
||||
return getRequiredEnvParam(ActionsEnvVars.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)
|
||||
);
|
||||
@@ -125,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(ActionsEnvVars.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(ActionsEnvVars.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) {
|
||||
@@ -202,8 +185,8 @@ export function getUploadValue(input: string | undefined): UploadKind {
|
||||
/**
|
||||
* Get the workflow run ID.
|
||||
*/
|
||||
export function getWorkflowRunID(): number {
|
||||
const workflowRunIdString = getRequiredEnvParam(ActionsEnvVars.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(
|
||||
@@ -221,8 +204,8 @@ export function getWorkflowRunID(): number {
|
||||
/**
|
||||
* Get the workflow run attempt number.
|
||||
*/
|
||||
export function getWorkflowRunAttempt(): number {
|
||||
const workflowRunAttemptString = getRequiredEnvParam(
|
||||
export function getWorkflowRunAttempt(env: Env = getEnv()): number {
|
||||
const workflowRunAttemptString = env.getRequired(
|
||||
ActionsEnvVars.GITHUB_RUN_ATTEMPT,
|
||||
);
|
||||
const workflowRunAttempt = parseInt(workflowRunAttemptString, 10);
|
||||
@@ -290,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 {
|
||||
@@ -399,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));
|
||||
};
|
||||
@@ -429,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 {
|
||||
@@ -443,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,
|
||||
@@ -459,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,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);
|
||||
|
||||
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
|
||||
await runAutobuild(config, BuiltInLanguage.go, logger);
|
||||
}
|
||||
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import * as retry from "@octokit/plugin-retry";
|
||||
|
||||
import {
|
||||
ActionsEnvVars,
|
||||
getActionVersion,
|
||||
getRequiredInput,
|
||||
} from "./actions-util";
|
||||
import { EnvVar } from "./environment";
|
||||
import { getActionVersion, getRequiredInput } from "./actions-util";
|
||||
import { EnvVar, ReadOnlyEnv, ActionsEnvVars, getEnv } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import { getRepositoryNwo, RepositoryNwo } from "./repository";
|
||||
import {
|
||||
@@ -47,10 +46,13 @@ export interface GitHubApiExternalRepoDetails {
|
||||
apiURL: string | undefined;
|
||||
}
|
||||
|
||||
/** The type of GitHub API client we use. */
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
|
||||
function createApiClientWithDetails(
|
||||
apiDetails: GitHubApiCombinedDetails,
|
||||
{ allowExternal = false } = {},
|
||||
) {
|
||||
): ApiClient {
|
||||
const auth =
|
||||
(allowExternal && apiDetails.externalRepoAuth) || apiDetails.auth;
|
||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry);
|
||||
@@ -71,16 +73,16 @@ function createApiClientWithDetails(
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiDetails(): GitHubApiDetails {
|
||||
export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
|
||||
return {
|
||||
auth: getRequiredInput("token"),
|
||||
url: getRequiredEnvParam(ActionsEnvVars.GITHUB_SERVER_URL),
|
||||
apiURL: getRequiredEnvParam(ActionsEnvVars.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"}
|
||||
|
||||
@@ -68,7 +68,7 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -272,17 +272,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++";
|
||||
|
||||
@@ -6,12 +6,14 @@ import test, { ExecutionContext } from "ava";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { AnalysisKind, supportedAnalysisKinds } from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { UserConfig } from "./config/db-config";
|
||||
import * as file from "./config/file";
|
||||
import * as configUtils from "./config-utils";
|
||||
import * as errorMessages from "./error-messages";
|
||||
import { Feature } from "./feature-flags";
|
||||
@@ -37,6 +39,9 @@ import {
|
||||
createTestConfig,
|
||||
makeMacro,
|
||||
initAllState,
|
||||
callee,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
AssertableTarget,
|
||||
} from "./testing-utils";
|
||||
import {
|
||||
GitHubVariant,
|
||||
@@ -462,6 +467,24 @@ test.serial("load non-existent input", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
/** A non-empty, but fairly minimal configuration file. */
|
||||
const simpleConfigFileContents = `
|
||||
name: my config
|
||||
queries:
|
||||
- uses: ./foo_file`;
|
||||
|
||||
/** A less minimal configuration file. */
|
||||
const otherConfigFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
queries:
|
||||
- uses: ./foo
|
||||
paths-ignore:
|
||||
- a
|
||||
- b
|
||||
paths:
|
||||
- c/d`;
|
||||
|
||||
test.serial("load non-empty input", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
setupActionsVars(tempDir, tempDir);
|
||||
@@ -476,18 +499,6 @@ test.serial("load non-empty input", async (t) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Just create a generic config object with non-default values for all fields
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
disable-default-queries: true
|
||||
queries:
|
||||
- uses: ./foo
|
||||
paths-ignore:
|
||||
- a
|
||||
- b
|
||||
paths:
|
||||
- c/d`;
|
||||
|
||||
fs.mkdirSync(path.join(tempDir, "foo"));
|
||||
|
||||
const userConfig: UserConfig = {
|
||||
@@ -514,7 +525,7 @@ test.serial("load non-empty input", async (t) => {
|
||||
});
|
||||
|
||||
const languagesInput = "javascript";
|
||||
const configFilePath = createConfigFile(inputFileContents, tempDir);
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);
|
||||
|
||||
const state = initAllState();
|
||||
const actualConfig = await configUtils.initConfig(
|
||||
@@ -540,14 +551,12 @@ test.serial(
|
||||
"Using config input and file together, config input should be used.",
|
||||
async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
process.env["RUNNER_TEMP"] = tempDir;
|
||||
process.env["GITHUB_WORKSPACE"] = tempDir;
|
||||
setupActionsVars(tempDir, tempDir);
|
||||
|
||||
const inputFileContents = `
|
||||
name: my config
|
||||
queries:
|
||||
- uses: ./foo_file`;
|
||||
const configFilePath = createConfigFile(inputFileContents, tempDir);
|
||||
const configFilePath = createConfigFile(
|
||||
simpleConfigFileContents,
|
||||
tempDir,
|
||||
);
|
||||
|
||||
const configInput = `
|
||||
name: my config
|
||||
@@ -576,7 +585,7 @@ test.serial(
|
||||
// Only JS, python packs will be ignored
|
||||
const languagesInput = "javascript";
|
||||
|
||||
const state = initAllState();
|
||||
const state = initAllState({ env: util.getEnv() });
|
||||
const config = await configUtils.initConfig(
|
||||
state,
|
||||
createTestInitConfigInputs({
|
||||
@@ -2259,3 +2268,442 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
|
||||
{ exclude: { tags: "exclude-from-incremental" } },
|
||||
]);
|
||||
});
|
||||
|
||||
test("determineUserConfig - empty config when neither input is specified", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withFeatures([])
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
// The returned configuration should be empty.
|
||||
await target
|
||||
// The fact that no configuration was provided should have been logged,
|
||||
.logs(t, "No configuration file was provided")
|
||||
// But not the messages for the two input sources
|
||||
// or the warning about both inputs.
|
||||
.notLogs(
|
||||
t,
|
||||
"Using config from action input:",
|
||||
"Using configuration file:",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.passes(t.deepEqual, {});
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - loads config file", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: undefined,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The path of the input config file should have been logged,
|
||||
.logs(t, `Using configuration file: ${configFilePath}`)
|
||||
.notLogs(
|
||||
t,
|
||||
// The other two origin messages and the warning about both inputs should
|
||||
// not have been logged.
|
||||
"No configuration file was provided",
|
||||
"Using config from action input:",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should not have changed.
|
||||
t.is(inputs.configFile, configFilePath);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - loads config input", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: undefined,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The input source and path of the generated config file should have been logged.
|
||||
.logs(
|
||||
t,
|
||||
"Using config from action input:",
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
)
|
||||
// The message about no configuration input and
|
||||
// the warning about both inputs should not have been logged.
|
||||
.notLogs(
|
||||
t,
|
||||
"No configuration file was provided",
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input when both specified", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
await target
|
||||
// The path of the generated config file and
|
||||
// the warning about both inputs should have been logged.
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
// The loaded configuration should match `simpleConfigFileContents`.
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
});
|
||||
});
|
||||
|
||||
/** A `config` input that we might get from Default Setup. */
|
||||
const defaultSetupConfigInput = `
|
||||
threat-models: [local, remote]
|
||||
default-setup:
|
||||
org:
|
||||
model-packs: [foo, bar]`;
|
||||
|
||||
test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const inputs = createTestInitConfigInputs({
|
||||
configInput: defaultSetupConfigInput,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
});
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
|
||||
.withFeatures([Feature.AllowMergeConfigFiles])
|
||||
.withArgs(tmpDir, inputs);
|
||||
|
||||
// The loaded configuration should match the result of merging
|
||||
// `defaultSetupConfigInput` and `simpleConfigFileContents`.
|
||||
const expectedConfig = {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
"threat-models": ["local", "remote"],
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": ["foo", "bar"],
|
||||
},
|
||||
},
|
||||
} satisfies UserConfig;
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`,
|
||||
)
|
||||
.notLogs(
|
||||
t,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"No configuration file was provided",
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.passes(t.deepEqual, expectedConfig);
|
||||
|
||||
// The `configFile` input should have been mutated to the generated path.
|
||||
t.is(inputs.configFile, expectedConfigPath);
|
||||
|
||||
// Since `result` is the result of merging the configurations in-memory,
|
||||
// also check whether loading the configuration from disk that was written
|
||||
// by `determineUserConfig` matches our expectations.
|
||||
const loadedFromDisk = configUtils.getLocalConfig(
|
||||
getRunnerLogger(true),
|
||||
expectedConfigPath,
|
||||
false,
|
||||
);
|
||||
t.deepEqual(loadedFromDisk, expectedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
|
||||
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
|
||||
const target = callee(configUtils.determineUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withFeatures([Feature.AllowMergeConfigFiles])
|
||||
.withArgs(
|
||||
tmpDir,
|
||||
createTestInitConfigInputs({
|
||||
configInput: simpleConfigFileContents,
|
||||
configFile: configFilePath,
|
||||
workspacePath: tmpDir,
|
||||
}),
|
||||
);
|
||||
|
||||
await target
|
||||
.logs(
|
||||
t,
|
||||
`Using config from action input: ${expectedConfigPath}`,
|
||||
`Using configuration file: ${expectedConfigPath}`,
|
||||
"Both a config file and config input were provided. Ignoring config file.",
|
||||
)
|
||||
.notLogs(t, "No configuration file was provided")
|
||||
.passes(t.deepEqual, {
|
||||
name: "my config",
|
||||
queries: [{ uses: "./foo_file" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("loadUserConfig - loads local configuration files", async (t) => {
|
||||
await withTmpDir(async (workspaceDir) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
// Construct the test target.
|
||||
const loadUserConfig = (
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
filePath: string,
|
||||
) =>
|
||||
configUtils.loadUserConfig(
|
||||
actionState,
|
||||
filePath,
|
||||
workspaceDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
);
|
||||
const target = callee(loadUserConfig);
|
||||
|
||||
// `loadUserConfig` should load local configuration files if they are inside the workspace:
|
||||
const insideOfWorkspace = path.join(workspaceDir, "some-file.yml");
|
||||
fs.writeFileSync(insideOfWorkspace, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(insideOfWorkspace)
|
||||
.passes(t.deepEqual, { "test-key": "present" });
|
||||
|
||||
// `loadUserConfig` should normally throw if the path is outside of the workspace:
|
||||
const outsideOfWorkspace = path.join(
|
||||
tmpDir,
|
||||
"not-the-generated-file.yml",
|
||||
);
|
||||
fs.writeFileSync(outsideOfWorkspace, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(outsideOfWorkspace)
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// `loadUserConfig` does not throw if the path is the result of `userConfigFromActionPath`:
|
||||
const generatedPath = configUtils.userConfigFromActionPath(tmpDir);
|
||||
fs.writeFileSync(generatedPath, "test-key: present", "utf8");
|
||||
|
||||
await target
|
||||
.withArgs(generatedPath)
|
||||
.passes(t.deepEqual, { "test-key": "present" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.serial("loadUserConfig - loads remote configuration files", async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
|
||||
|
||||
const remoteAddress = "owner/repo/file@ref";
|
||||
await callee(configUtils.loadUserConfig)
|
||||
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
|
||||
.passes(t.deepEqual, {});
|
||||
|
||||
t.true(
|
||||
getRemoteConfig.calledOnceWithExactly(
|
||||
sinon.match.any,
|
||||
remoteAddress,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"loadUserConfig - loads remote configuration files (new format, partial)",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
|
||||
|
||||
// Construct the basic test target.
|
||||
const target = callee(configUtils.loadUserConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withFeatures([Feature.NewRemoteFileAddresses]);
|
||||
|
||||
// Utility function to assert that `targetWithArgs` has identified
|
||||
// the input as a remote file address.
|
||||
const checkIsRemote =
|
||||
(address: string) =>
|
||||
async <R>(targetWithArgs: AssertableTarget<R>) => {
|
||||
// We have stubbed `getRemoteConfig` to resolve to `{}`, so we
|
||||
// expect that result.
|
||||
await targetWithArgs.passes(t.deepEqual, {});
|
||||
|
||||
// And `getRemoteConfig` should have been called exactly once.
|
||||
t.is(getRemoteConfig.callCount, 1);
|
||||
|
||||
// Get the arguments for the call and check that there were three.
|
||||
// We don't care about the first, but check that the other two
|
||||
// match our expectations. We break it down like this to get
|
||||
// more useful test output.
|
||||
const args = getRemoteConfig.getCalls()[0].args;
|
||||
t.is(args.length, 3);
|
||||
t.deepEqual(args[1], address);
|
||||
t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS);
|
||||
};
|
||||
|
||||
// Utility function to assert that `targetWithArgs` has not identified
|
||||
// the input as a remote file address.
|
||||
const checkIsNotRemote = async <R>(
|
||||
targetWithArgs: AssertableTarget<R>,
|
||||
) => {
|
||||
// We expect `loadUserConfig` to have thrown if it thinks the path is local,
|
||||
// since the inputs we provide aren't for files that exist.
|
||||
await targetWithArgs.throws(t);
|
||||
|
||||
// Additionally, we expect that `getRemoteConfig` wasn't called.
|
||||
t.is(getRemoteConfig.callCount, 0);
|
||||
};
|
||||
|
||||
// Utility function to add the explicit `REMOTE_PATH_PREFIX` to the input.
|
||||
const withExplicitPrefix = (str: string) =>
|
||||
`${file.REMOTE_PATH_PREFIX}${str}`;
|
||||
|
||||
// Utility to set up a call to `loadUserConfig` with the provided `address`
|
||||
// and pass it to `assertion`.
|
||||
const testTargetWith = async (
|
||||
address: string,
|
||||
assertion: (
|
||||
targetWithArgs: AssertableTarget<Promise<UserConfig>>,
|
||||
) => Promise<any>,
|
||||
) => {
|
||||
// Reset the stub's history since we re-use it.
|
||||
getRemoteConfig.resetHistory();
|
||||
|
||||
// Log the input we are testing so that, in the event of a failure,
|
||||
// it is easier to see which input was responsible.
|
||||
t.log(`testTargetWith("${address}")`);
|
||||
|
||||
// Prepare the test call to `loadUserConfig`.
|
||||
const targetWithArgs = target.withArgs(
|
||||
address,
|
||||
tmpDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
);
|
||||
|
||||
// Pass it to the provided assertion function.
|
||||
await assertion(targetWithArgs);
|
||||
};
|
||||
|
||||
// Since this input contains an '@' character, it is treated as a remote path
|
||||
// by the old logic even without the explicit prefix.
|
||||
const remoteWithoutPrefix = "repo@main";
|
||||
await testTargetWith(
|
||||
remoteWithoutPrefix,
|
||||
checkIsRemote(remoteWithoutPrefix),
|
||||
);
|
||||
await testTargetWith(
|
||||
withExplicitPrefix(remoteWithoutPrefix),
|
||||
checkIsRemote(remoteWithoutPrefix),
|
||||
);
|
||||
// It is only treated as a local path with the corresponding prefix.
|
||||
await testTargetWith(`./${remoteWithoutPrefix}`, checkIsNotRemote);
|
||||
|
||||
// The following test inputs are examples of ambiguous paths. They could refer to
|
||||
// valid local or remote paths. For each, we check that they are treated as remote
|
||||
// paths if the explicit remote file prefix is used and as local paths otherwise.
|
||||
const testInputs = ["repo:file", "input", "../input"];
|
||||
|
||||
for (const testInput of testInputs) {
|
||||
for (const addPrefix of [true, false]) {
|
||||
await testTargetWith(
|
||||
addPrefix ? withExplicitPrefix(testInput) : testInput,
|
||||
addPrefix ? checkIsRemote(testInput) : checkIsNotRemote,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getActionVersion,
|
||||
getOptionalInput,
|
||||
isAnalyzingPullRequest,
|
||||
isDefaultSetup,
|
||||
isDynamicWorkflow,
|
||||
} from "./actions-util";
|
||||
import {
|
||||
@@ -26,10 +27,15 @@ import {
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
generateCodeScanningConfig,
|
||||
mergeDefaultSetupAndUserConfigs,
|
||||
parseUserConfig,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import { getRemoteConfig } from "./config/file";
|
||||
import {
|
||||
getRemoteConfig,
|
||||
LOCAL_PATH_PREFIX,
|
||||
REMOTE_PATH_PREFIX,
|
||||
} from "./config/file";
|
||||
import {
|
||||
parseRegistries,
|
||||
type RegistryConfigNoCredentials,
|
||||
@@ -466,14 +472,28 @@ async function downloadCacheWithTime(
|
||||
return { trapCaches, trapCacheDownloadTime };
|
||||
}
|
||||
|
||||
async function loadUserConfig(
|
||||
/**
|
||||
* Loads a CLI configuration file from `configFile`.
|
||||
*
|
||||
* @param actionState The Action state.
|
||||
* @param configFile The address of the configuration file.
|
||||
* @param workspacePath The workspace path, used to check that the configuration file exists relative to it.
|
||||
* @param apiDetails Information for how to access the API to fetch remote files.
|
||||
* @param tempDir The temporary directory which may contain a CodeQL Action-generated configuration file.
|
||||
* @returns The loaded configuration file, if successful.
|
||||
*/
|
||||
export async function loadUserConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
configFile: string,
|
||||
workspacePath: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
tempDir: string,
|
||||
): Promise<UserConfig> {
|
||||
if (isLocal(configFile)) {
|
||||
const allowNewFormat = await actionState.features.getValue(
|
||||
Feature.NewRemoteFileAddresses,
|
||||
);
|
||||
|
||||
if (isLocal(configFile, allowNewFormat)) {
|
||||
if (configFile !== userConfigFromActionPath(tempDir)) {
|
||||
// If the config file is not generated by the Action, it should be relative to the workspace.
|
||||
configFile = path.resolve(workspacePath, configFile);
|
||||
@@ -489,6 +509,12 @@ async function loadUserConfig(
|
||||
);
|
||||
return getLocalConfig(actionState.logger, configFile, validateConfig);
|
||||
} else {
|
||||
// Drop the explicit prefix if it is present. Since `REMOTE_PATH_PREFIX` is chosen
|
||||
// to not conflict with permissible characters in "owner" or "repo" components,
|
||||
// this does not risk removing valid parts of either component by accident.
|
||||
if (allowNewFormat && isExplicitRemotePath(configFile)) {
|
||||
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
|
||||
}
|
||||
return await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
}
|
||||
}
|
||||
@@ -936,7 +962,11 @@ function dbLocationOrDefault(
|
||||
return dbLocation || path.resolve(tempDir, "codeql_databases");
|
||||
}
|
||||
|
||||
function userConfigFromActionPath(tempDir: string): string {
|
||||
/**
|
||||
* Gets the path for the CodeQL Action-generated configuration file,
|
||||
* which is used to store the `config` input.
|
||||
*/
|
||||
export function userConfigFromActionPath(tempDir: string): string {
|
||||
return path.resolve(tempDir, "user-config-from-action.yml");
|
||||
}
|
||||
|
||||
@@ -996,6 +1026,110 @@ export async function applyIncrementalAnalysisSettings(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines where to load the `UserConfig` for the CLI from and loads it.
|
||||
*
|
||||
* @param inputs The Action inputs. The `configFile` value will be mutated
|
||||
* if a CodeQL Action-generated file should be used.
|
||||
*
|
||||
* @returns The loaded `UserConfig`, which might be empty if no configuration
|
||||
* was specified.
|
||||
*/
|
||||
export async function determineUserConfig(
|
||||
action: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
tempDir: string,
|
||||
inputs: InitConfigInputs,
|
||||
): Promise<UserConfig> {
|
||||
const validateConfig = await action.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
|
||||
// We have the following cases:
|
||||
// 1. A `config` or `config-file` input is provided, but not both: use the provided one.
|
||||
// 2. Both are provided and we are in an advanced workflow: ignore the `config-file` input.
|
||||
// 3. Both are provided and we are in Default Setup: the `config` input uses a limited
|
||||
// set of options, which are supported by `mergeDefaultSetupAndUserConfigs`,
|
||||
// and we merge the two configs.
|
||||
if (inputs.configInput) {
|
||||
const computedConfigPath = userConfigFromActionPath(tempDir);
|
||||
|
||||
// Get a function which enables us to determine whether the FF that allows us to
|
||||
// merge supported configuration file properties is enabled. We only execute
|
||||
// this lazily if the other checks pass.
|
||||
const allowMergeConfigs = () =>
|
||||
action.features.getValue(Feature.AllowMergeConfigFiles);
|
||||
|
||||
// Check whether we also have a `config-file` input and decide what to do.
|
||||
if (
|
||||
inputs.configFile &&
|
||||
isDefaultSetup(action.env) &&
|
||||
(await allowMergeConfigs())
|
||||
) {
|
||||
// If the FF is enabled and we are in Default Setup, combine the supported
|
||||
// configuration file properties and write the result to disk.
|
||||
const fromConfigInput = parseUserConfig(
|
||||
action.logger,
|
||||
"`config` input",
|
||||
inputs.configInput,
|
||||
validateConfig,
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
);
|
||||
|
||||
// Write the merged configuration to disk so that it can be loaded subsequently by
|
||||
// the CLI or other CodeQL Action steps.
|
||||
const mergedConfig = mergeDefaultSetupAndUserConfigs(
|
||||
action.logger,
|
||||
fromConfigInput,
|
||||
fromConfigFile,
|
||||
);
|
||||
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
|
||||
action.logger.debug(
|
||||
`Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`,
|
||||
);
|
||||
|
||||
inputs.configFile = computedConfigPath;
|
||||
return mergedConfig;
|
||||
} else {
|
||||
// If we are in this branch and there is a `config-file` input, then it means
|
||||
// we didn't meet the conditions for merging the configurations. Warn the user
|
||||
// that the configuration file will be ignored.
|
||||
if (inputs.configFile) {
|
||||
action.logger.warning(
|
||||
`Both a config file and config input were provided. Ignoring config file.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Write the `config` input straight to disk.
|
||||
fs.writeFileSync(computedConfigPath, inputs.configInput);
|
||||
inputs.configFile = computedConfigPath;
|
||||
action.logger.debug(
|
||||
`Using config from action input: ${inputs.configFile}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load whatever configuration file we have, if any.
|
||||
if (!inputs.configFile) {
|
||||
action.logger.debug("No configuration file was provided");
|
||||
return {};
|
||||
} else {
|
||||
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
return await loadUserConfig(
|
||||
action,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and return the config.
|
||||
*
|
||||
@@ -1009,31 +1143,7 @@ export async function initConfig(
|
||||
const { logger, features } = actionState;
|
||||
const { tempDir } = inputs;
|
||||
|
||||
// if configInput is set, it takes precedence over configFile
|
||||
if (inputs.configInput) {
|
||||
if (inputs.configFile) {
|
||||
logger.warning(
|
||||
`Both a config file and config input were provided. Ignoring config file.`,
|
||||
);
|
||||
}
|
||||
inputs.configFile = userConfigFromActionPath(tempDir);
|
||||
fs.writeFileSync(inputs.configFile, inputs.configInput);
|
||||
logger.debug(`Using config from action input: ${inputs.configFile}`);
|
||||
}
|
||||
|
||||
let userConfig: UserConfig = {};
|
||||
if (!inputs.configFile) {
|
||||
logger.debug("No configuration file was provided");
|
||||
} else {
|
||||
logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
userConfig = await loadUserConfig(
|
||||
actionState,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
tempDir,
|
||||
);
|
||||
}
|
||||
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
|
||||
|
||||
const config = await initActionState(inputs, userConfig);
|
||||
|
||||
@@ -1181,16 +1291,61 @@ export async function initConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
function isLocal(configPath: string): boolean {
|
||||
// If the path starts with ./, look locally
|
||||
if (configPath.indexOf("./") === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return configPath.indexOf("@") === -1;
|
||||
/**
|
||||
* Determines if `configPath` is explicitly local. That is, it starts with `LOCAL_PATH_PREFIX`.
|
||||
* A configuration file path that starts with `LOCAL_PATH_PREFIX` is always treated as a local path.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function isExplicitLocalPath(configPath: string): boolean {
|
||||
return configPath.startsWith(LOCAL_PATH_PREFIX);
|
||||
}
|
||||
|
||||
function getLocalConfig(
|
||||
/**
|
||||
* Determines if `configPath` starts with the prefix used to explicitly mark a path
|
||||
* as a remote path (`REMOTE_PATH_PREFIX`).
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function isExplicitRemotePath(configPath: string): boolean {
|
||||
return configPath.startsWith(REMOTE_PATH_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `configPath` contains a '@' character.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
*/
|
||||
function containsAtRef(configPath: string): boolean {
|
||||
return configPath.includes("@");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if `configPath` refers to a local configuration file.
|
||||
*
|
||||
* @param configPath The path to test.
|
||||
* @returns True if it is local, or false otherwise.
|
||||
*/
|
||||
function isLocal(configPath: string, allowNewFormat: boolean): boolean {
|
||||
// If the path starts with `LOCAL_PATH_PREFIX`, it is explicitly local.
|
||||
// This allows local paths that would otherwise contain '@'
|
||||
// to be used with a `LOCAL_PATH_PREFIX` prefix.
|
||||
if (isExplicitLocalPath(configPath)) {
|
||||
return true;
|
||||
}
|
||||
// If the path starts with `REMOTE_PATH_PREFIX`, it is explicitly remote.
|
||||
// This allows users to resolve ambiguity by specifying `REMOTE_PATH_PREFIX`.
|
||||
if (allowNewFormat && isExplicitRemotePath(configPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, the path is also local if it does not contain '@'.
|
||||
// This assumes the `OLD_REMOTE_ADDRESS_FORMAT` which must contain a '@'
|
||||
// character for remote addresses.
|
||||
return !containsAtRef(configPath);
|
||||
}
|
||||
|
||||
export function getLocalConfig(
|
||||
logger: Logger,
|
||||
configFile: string,
|
||||
validateConfig: boolean,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getRecordingLogger,
|
||||
LoggedMessage,
|
||||
makeMacro,
|
||||
RecordingLogger,
|
||||
} from "../testing-utils";
|
||||
import { ConfigurationError, prettyPrintPack } from "../util";
|
||||
|
||||
@@ -488,3 +489,139 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{ "threat-models": ["a", "b"] },
|
||||
{ "threat-models": ["local", "remote"] },
|
||||
);
|
||||
|
||||
const threatModels = result["threat-models"];
|
||||
|
||||
if (t.truthy(threatModels)) {
|
||||
t.deepEqual(threatModels, ["a", "b", "local", "remote"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{},
|
||||
{ "default-setup": {} },
|
||||
);
|
||||
|
||||
// User-supplied value is ignored.
|
||||
t.deepEqual(result, {});
|
||||
|
||||
// Warning is logged.
|
||||
t.true(
|
||||
logger.hasMessage(
|
||||
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - keeps default setup key from 'config' input", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const expected: dbConfig.DefaultSetupConfig = {
|
||||
org: { "model-packs": ["some-pack"] },
|
||||
};
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{ "default-setup": expected },
|
||||
{},
|
||||
);
|
||||
|
||||
// Result matches the input.
|
||||
t.deepEqual(result["default-setup"], expected);
|
||||
|
||||
// No warning is logged.
|
||||
t.false(
|
||||
logger.hasMessage(
|
||||
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplied configuration", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, configFile);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {
|
||||
"query-filters": [{ exclude: { a: "b" } }],
|
||||
"paths-ignore": ["path"],
|
||||
};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
borg: [],
|
||||
org: {
|
||||
unknown: "foo",
|
||||
"model-packs": [],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
"paths-ignore": ["other-path"],
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [] } },
|
||||
});
|
||||
|
||||
const expectedUnrecognisedKeys = [
|
||||
".default-setup.org.unknown",
|
||||
".default-setup.borg",
|
||||
".paths-ignore",
|
||||
].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
|
||||
]);
|
||||
});
|
||||
|
||||
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
|
||||
const logger = new RecordingLogger();
|
||||
const configFile: dbConfig.UserConfig = {};
|
||||
|
||||
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
|
||||
logger,
|
||||
{
|
||||
"default-setup": {
|
||||
org: {
|
||||
"model-packs": [123],
|
||||
},
|
||||
} as unknown as dbConfig.DefaultSetupConfig,
|
||||
},
|
||||
configFile,
|
||||
);
|
||||
|
||||
t.deepEqual(result, {
|
||||
...configFile,
|
||||
"default-setup": { org: { "model-packs": [123] } },
|
||||
});
|
||||
|
||||
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
|
||||
checkExpectedLogMessages(t, logger.messages, [
|
||||
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -4,11 +4,16 @@ import * as yaml from "js-yaml";
|
||||
import * as jsonschema from "jsonschema";
|
||||
import * as semver from "semver";
|
||||
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "../diagnostics";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
} from "../feature-flags/properties";
|
||||
import * as json from "../json";
|
||||
import { Language } from "../languages";
|
||||
import { Logger } from "../logging";
|
||||
import { cloneObject, ConfigurationError, prettyPrintPack } from "../util";
|
||||
@@ -28,6 +33,21 @@ export interface QuerySpec {
|
||||
uses: string;
|
||||
}
|
||||
|
||||
const ORG_SCHEMA = {
|
||||
/** An array of model pack names. */
|
||||
"model-packs": json.optional(json.array(json.string)),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Not intended to be provided directly by a user. */
|
||||
export type OrgType = json.FromSchema<typeof ORG_SCHEMA>;
|
||||
|
||||
const DEFAULT_SETUP_SCHEMA = {
|
||||
org: json.optional<OrgType>(json.object(ORG_SCHEMA)),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/** Not intended to be provided directly by a user. */
|
||||
export type DefaultSetupConfig = json.FromSchema<typeof DEFAULT_SETUP_SCHEMA>;
|
||||
|
||||
/**
|
||||
* Format of the config file supplied by the user.
|
||||
*/
|
||||
@@ -46,6 +66,119 @@ export interface UserConfig {
|
||||
// Set of query filters to include and exclude extra queries based on
|
||||
// codeql query suite `include` and `exclude` properties
|
||||
"query-filters"?: QueryFilter[];
|
||||
|
||||
/** An array (possibly empty or absent) of threat models to use. */
|
||||
"threat-models"?: string[];
|
||||
|
||||
/**
|
||||
* Configuration options that are reserved for us in Default Setup and
|
||||
* not intended to be supplied directly by users.
|
||||
*/
|
||||
"default-setup"?: DefaultSetupConfig;
|
||||
}
|
||||
|
||||
/** A subset of the `UserConfig` schema that is used by Default Setup. */
|
||||
const DEFAULT_SETUP_CONFIG_SCHEMA = {
|
||||
"threat-models": json.optional(json.array(json.string)),
|
||||
"default-setup": json.optional<DefaultSetupConfig>(
|
||||
json.object(DEFAULT_SETUP_SCHEMA),
|
||||
),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* Merges supported properties from two configuration files. This is intended only for
|
||||
* use with merging the `config` input provided by Default Setup with a potentially
|
||||
* richer configuration file provided by a user.
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @param fromConfigInput The configuration from Default Setup.
|
||||
* @param fromConfigFile The user-supplied configuration.
|
||||
* @returns The combination of both configuration files.
|
||||
*/
|
||||
export function mergeDefaultSetupAndUserConfigs(
|
||||
logger: Logger,
|
||||
fromConfigInput: UserConfig,
|
||||
fromConfigFile: UserConfig,
|
||||
): UserConfig {
|
||||
logger.debug(
|
||||
"Combining configuration files from 'config' and 'config-file' inputs",
|
||||
);
|
||||
|
||||
// Check for unexpected keys in the configuration from the `config` input
|
||||
// that was provided by Default Setup. This should only contain the keys
|
||||
// we would expect to receive from Default Setup.
|
||||
const schemaCheckResult = json.checkSchema(
|
||||
DEFAULT_SETUP_CONFIG_SCHEMA,
|
||||
fromConfigInput as json.UnvalidatedObject<any>,
|
||||
);
|
||||
|
||||
// Report any invalid or unrecognised keys.
|
||||
if (schemaCheckResult.invalidKeys.length > 0) {
|
||||
logger.warning(
|
||||
`Invalid keys in Default Setup configuration: ${schemaCheckResult.invalidKeys.join(", ")}`,
|
||||
);
|
||||
addNoLanguageDiagnostic(
|
||||
undefined,
|
||||
makeTelemetryDiagnostic(
|
||||
"codeql-action/invalid-default-setup-config-keys",
|
||||
"Invalid Default Setup configuration keys",
|
||||
{
|
||||
invalidKeys: schemaCheckResult.invalidKeys,
|
||||
},
|
||||
["internal-error"],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (schemaCheckResult.unknownKeys.length > 0) {
|
||||
logger.warning(
|
||||
`Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`,
|
||||
);
|
||||
addNoLanguageDiagnostic(
|
||||
undefined,
|
||||
makeTelemetryDiagnostic(
|
||||
"codeql-action/unrecognised-default-setup-config-keys",
|
||||
"Unrecognised Default Setup configuration keys",
|
||||
{
|
||||
unrecognisedKeys: schemaCheckResult.unknownKeys,
|
||||
},
|
||||
["internal-error"],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Combine all specified threat models from both sources.
|
||||
const threatModels = new Set(fromConfigInput["threat-models"] || []);
|
||||
for (const configFileThreatModel of fromConfigFile["threat-models"] || []) {
|
||||
threatModels.add(configFileThreatModel);
|
||||
}
|
||||
|
||||
// Warn if there is a 'default-setup' configuration key in the user-supplied configuration,
|
||||
// since it is not meant to be used and we therefore ignore it here.
|
||||
if (fromConfigFile["default-setup"]) {
|
||||
logger.warning(
|
||||
`The 'default-setup' configuration key is not supported in user-supplied configuration files and will be ignored.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Since we expect the `fromConfigInput` configuration to be provided by Default Setup,
|
||||
// we expect a limited set of options. Therefore, we base the overall configuration on
|
||||
// the one provided via the `config-file` input, which may be richer.
|
||||
const result = { ...fromConfigFile };
|
||||
delete result["threat-models"];
|
||||
delete result["default-setup"];
|
||||
|
||||
if (fromConfigInput["default-setup"]?.org?.["model-packs"]) {
|
||||
result["default-setup"] = {
|
||||
org: {
|
||||
"model-packs": fromConfigInput["default-setup"].org["model-packs"],
|
||||
},
|
||||
};
|
||||
}
|
||||
if (threatModels.size > 0) {
|
||||
result["threat-models"] = Array.from(threatModels);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(async (fn) => t.is(await fn(), undefined));
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
const repositoryProperties = {
|
||||
@@ -22,47 +22,29 @@ const repositoryProperties = {
|
||||
|
||||
test("getConfigFileInput returns input value", async (t) => {
|
||||
const testInput = "/some/path";
|
||||
const target = callee(getConfigFileInput).withFeatures([
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
]);
|
||||
|
||||
const actionsEnv = target.getState().actions;
|
||||
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 targetWithArgs = target
|
||||
.withActions(actionsEnv)
|
||||
.withArgs(repositoryProperties);
|
||||
await targetWithArgs.passes(async (fn) => t.is(await fn(), testInput));
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(
|
||||
targetWithArgs
|
||||
.getLogger()
|
||||
.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) => {
|
||||
// Since there is no direct input, we should use the repository property.
|
||||
const target = callee(getConfigFileInput)
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs(repositoryProperties);
|
||||
|
||||
await target.passes(async (fn) =>
|
||||
t.is(await fn(), repositoryProperties[RepositoryPropertyName.CONFIG_FILE]),
|
||||
);
|
||||
|
||||
// Check for the expected log message.
|
||||
t.true(
|
||||
target
|
||||
.getLogger()
|
||||
.hasMessage("Using configuration file input from repository property"),
|
||||
);
|
||||
.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) => {
|
||||
@@ -70,27 +52,18 @@ test("getConfigFileInput ignores empty repository property value", async (t) =>
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
|
||||
.passes(async (fn) => t.is(await fn(), undefined));
|
||||
.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.
|
||||
const target = callee(getConfigFileInput)
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties);
|
||||
|
||||
await target.passes(async (fn) => t.is(await fn(), undefined));
|
||||
|
||||
t.false(
|
||||
target
|
||||
.getLogger()
|
||||
.hasMessage("Using configuration file input from repository property"),
|
||||
);
|
||||
t.true(
|
||||
target
|
||||
.getLogger()
|
||||
.hasMessage(
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
),
|
||||
);
|
||||
.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);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,19 @@ import { ConfigurationError } from "../util";
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
* The prefix that can be specified to indicate that a path should be treated as a local file address.
|
||||
*/
|
||||
export const LOCAL_PATH_PREFIX = "./";
|
||||
|
||||
/**
|
||||
* The prefix that can be specified to indicate that a path should be treated as a remote file address.
|
||||
* The new remote file address format must start with either an owner or repository name. Both
|
||||
* are restricted to ASCII characters, '.', and '-'. The prefix chosen here does not interfere with
|
||||
* those (since it contains an `=`) and is _unlikely_ (but not impossible) to appear in a local file path.
|
||||
*/
|
||||
export const REMOTE_PATH_PREFIX = "remote=";
|
||||
|
||||
/**
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionsEnvVars } from "../actions-util";
|
||||
import { ActionsEnvVars } from "../environment";
|
||||
import * as errors from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { callee, getTestEnv } from "../testing-utils";
|
||||
import { callee } from "../testing-utils";
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import {
|
||||
@@ -50,7 +50,7 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
|
||||
for (const oldFormatInput of oldFormatInputs) {
|
||||
await target
|
||||
.withArgs(oldFormatInput.input)
|
||||
.passes(async (fn) => t.deepEqual(await fn(), oldFormatInput.expected));
|
||||
.passes(t.deepEqual, oldFormatInput.expected);
|
||||
}
|
||||
|
||||
// New format.
|
||||
@@ -78,28 +78,23 @@ test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), newFormatInput.expected));
|
||||
.passes(t.deepEqual, newFormatInput.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress);
|
||||
|
||||
const env = target.getState().env;
|
||||
const owner = "test-owner";
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
|
||||
const targetWithEnv = target.withEnv(env);
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testCases: ParseRemoteFileAddressTest[] = [
|
||||
{
|
||||
@@ -141,33 +136,33 @@ test("parseRemoteFileAddress accepts remote address without an owner", async (t)
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const targetWithArgs = targetWithEnv.withArgs(testCase.input);
|
||||
const targetWithArgs = target.withArgs(testCase.input);
|
||||
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
|
||||
.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
|
||||
const target = callee(parseRemoteFileAddress).withArgs("repo@ref");
|
||||
|
||||
const env = target.getState().env;
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
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
|
||||
.withEnv(env)
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.throwsAsync(fn, { instanceOf: Error }));
|
||||
.throws(t, { instanceOf: Error });
|
||||
|
||||
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
|
||||
});
|
||||
@@ -202,14 +197,12 @@ test("parseRemoteFileAddress accepts remote address without a path", async (t) =
|
||||
// Should fail when the FF is not enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, { instanceOf: ConfigurationError }),
|
||||
);
|
||||
.throws(t, { instanceOf: ConfigurationError });
|
||||
|
||||
// And pass when the FF is enabled.
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) => t.deepEqual(await fn(), testCase.expected));
|
||||
.passes(t.deepEqual, testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,28 +210,25 @@ 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([]).passes(t.throwsAsync);
|
||||
await target.withFeatures([]).throws(t);
|
||||
await target
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) =>
|
||||
t.deepEqual(await fn(), {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
} satisfies RemoteFileAddress),
|
||||
);
|
||||
.passes(t.deepEqual, {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
path: "path",
|
||||
ref: DEFAULT_CONFIG_FILE_REF,
|
||||
} satisfies RemoteFileAddress);
|
||||
});
|
||||
|
||||
test("parseRemoteFileAddress rejects invalid values", async (t) => {
|
||||
const env = getTestEnv();
|
||||
const owner = "owner";
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
|
||||
const target = callee(parseRemoteFileAddress).withEnv(env);
|
||||
const target = callee(parseRemoteFileAddress).withEnv((env) => {
|
||||
const getRequired = sinon.stub(env, "getRequired");
|
||||
getRequired
|
||||
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
|
||||
.returns(`${owner}/current-repo`);
|
||||
});
|
||||
|
||||
const testInputs = [
|
||||
" ",
|
||||
@@ -262,21 +252,17 @@ test("parseRemoteFileAddress rejects invalid values", async (t) => {
|
||||
const targetWithArgs = target.withArgs(testInput);
|
||||
|
||||
// Should throw both when the new format is and isn't accepted.
|
||||
await targetWithArgs.withFeatures([]).passes(async (fn) =>
|
||||
t.throwsAsync(fn, {
|
||||
instanceOf: ConfigurationError,
|
||||
message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
|
||||
}),
|
||||
);
|
||||
await targetWithArgs.withFeatures([]).throws(t, {
|
||||
instanceOf: ConfigurationError,
|
||||
message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
|
||||
});
|
||||
await targetWithArgs
|
||||
.withFeatures([Feature.NewRemoteFileAddresses])
|
||||
.passes(async (fn) =>
|
||||
t.throwsAsync(fn, {
|
||||
// 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,
|
||||
}),
|
||||
);
|
||||
.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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ActionState } from "../action-common";
|
||||
import { ActionsEnvVars } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import { ActionsEnvVars, ReadOnlyEnv } from "../environment";
|
||||
import * as errorMessages from "../error-messages";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { ConfigurationError, Failure, Result, Success } from "../util";
|
||||
@@ -24,7 +23,7 @@ export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */
|
||||
function getDefaultOwner(env: Env): string {
|
||||
function getDefaultOwner(env: ReadOnlyEnv): string {
|
||||
const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY);
|
||||
const nwoParts = currentRepoNwo.split("/");
|
||||
|
||||
@@ -71,6 +70,42 @@ function parseOldRemoteFileAddress(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `input` as a `RemoteFileAddress` using the new format.
|
||||
*
|
||||
* @param env The read-only environment to obtain the owner name from if needed.
|
||||
* @param configFile The input to try and parse.
|
||||
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
|
||||
*/
|
||||
export function parseNewRemoteFileAddress(
|
||||
env: ReadOnlyEnv,
|
||||
configFile: string,
|
||||
): Result<RemoteFileAddress, undefined> {
|
||||
// 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) {
|
||||
return new Failure(undefined);
|
||||
}
|
||||
|
||||
const owner: string | undefined = pieces.groups.owner?.trim();
|
||||
const path: string | undefined = pieces.groups.path?.trim();
|
||||
const ref: string | undefined = pieces.groups.ref?.trim();
|
||||
|
||||
return new Success({
|
||||
owner: owner || getDefaultOwner(env),
|
||||
repo,
|
||||
path: path || DEFAULT_CONFIG_FILE_NAME,
|
||||
ref: ref || DEFAULT_CONFIG_FILE_REF,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
|
||||
*
|
||||
@@ -102,15 +137,12 @@ export async function parseRemoteFileAddress(
|
||||
}
|
||||
|
||||
// retrieve the various parts of the config location, and ensure they're present
|
||||
const format = new RegExp(
|
||||
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
|
||||
const newFormatAddressResult = parseNewRemoteFileAddress(
|
||||
actionState.env,
|
||||
configFile,
|
||||
);
|
||||
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) {
|
||||
if (newFormatAddressResult.isFailure()) {
|
||||
// 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.
|
||||
@@ -119,21 +151,14 @@ export async function parseRemoteFileAddress(
|
||||
);
|
||||
}
|
||||
|
||||
const owner: string | undefined = pieces.groups.owner?.trim();
|
||||
const path: string | undefined = pieces.groups.path?.trim();
|
||||
const ref: string | undefined = pieces.groups.ref?.trim();
|
||||
const address = newFormatAddressResult.value;
|
||||
|
||||
// Ensure that the path is a relative path.
|
||||
if (path?.startsWith("/")) {
|
||||
if (address.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,
|
||||
};
|
||||
return address;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.0",
|
||||
"cliVersion": "2.26.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.25.6",
|
||||
"priorCliVersion": "2.25.6"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
@@ -6,24 +6,45 @@ import { Language } from "./languages";
|
||||
import { getActionsLogger } from "./logging";
|
||||
import { getCodeQLDatabasePath } from "./util";
|
||||
|
||||
/** Represents a diagnostic message for the tool status page, etc. */
|
||||
export interface DiagnosticMessage {
|
||||
/**
|
||||
* Known tags for diagnostics. There is currently only "internal-error",
|
||||
* but others may be added in the future.
|
||||
*/
|
||||
export type DiagnosticTag = "internal-error";
|
||||
|
||||
/** Optional information about the origin of a diagnostic. */
|
||||
export type DiagnosticSourceOptions = {
|
||||
/**
|
||||
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
|
||||
* descriptor object should be nested under in SARIF.
|
||||
*/
|
||||
extractorName?: string;
|
||||
/** An array of tags for the diagnostic. */
|
||||
tags?: DiagnosticTag[];
|
||||
};
|
||||
|
||||
/** Represents information about the origin of a diagnostic. */
|
||||
export type DiagnosticSource = {
|
||||
/**
|
||||
* An identifier under which it makes sense to group this diagnostic message.
|
||||
* This is used to build the SARIF reporting descriptor object.
|
||||
*/
|
||||
id: string;
|
||||
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
|
||||
name: string;
|
||||
} & DiagnosticSourceOptions;
|
||||
|
||||
/**
|
||||
* Represents a diagnostic message for the tool status page, etc.
|
||||
*
|
||||
* Unlike {@link DiagnosticMessage}, properties which can automatically
|
||||
* be populated are optional in this type.
|
||||
*/
|
||||
export type DiagnosticMessageOptions = {
|
||||
/** ISO 8601 timestamp */
|
||||
timestamp: string;
|
||||
source: {
|
||||
/**
|
||||
* An identifier under which it makes sense to group this diagnostic message.
|
||||
* This is used to build the SARIF reporting descriptor object.
|
||||
*/
|
||||
id: string;
|
||||
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
|
||||
name: string;
|
||||
/**
|
||||
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
|
||||
* descriptor object should be nested under in SARIF.
|
||||
*/
|
||||
extractorName?: string;
|
||||
};
|
||||
timestamp?: string;
|
||||
/** Information about the origin of the diagnostic. */
|
||||
source?: DiagnosticSourceOptions;
|
||||
/** GitHub flavored Markdown formatted message. Should include inline links to any help pages. */
|
||||
markdownMessage?: string;
|
||||
/** Plain text message. Used by components where the string processing needed to support Markdown is cumbersome. */
|
||||
@@ -53,7 +74,15 @@ export interface DiagnosticMessage {
|
||||
};
|
||||
/** Structured metadata about the diagnostic message */
|
||||
attributes?: { [key: string]: any };
|
||||
}
|
||||
};
|
||||
|
||||
/** Represents a diagnostic message for the tool status page, etc. */
|
||||
export type DiagnosticMessage = DiagnosticMessageOptions & {
|
||||
/** ISO 8601 timestamp */
|
||||
timestamp: string;
|
||||
/** Information about the origin of the diagnostic. */
|
||||
source: DiagnosticSource;
|
||||
};
|
||||
|
||||
/** Represents a diagnostic message that has not yet been written to the database. */
|
||||
interface UnwrittenDiagnostic {
|
||||
@@ -90,7 +119,7 @@ let diagnosticCounter = 0;
|
||||
export function makeDiagnostic(
|
||||
id: string,
|
||||
name: string,
|
||||
data: Partial<DiagnosticMessage> | undefined = undefined,
|
||||
data: DiagnosticMessageOptions | undefined = undefined,
|
||||
): DiagnosticMessage {
|
||||
return {
|
||||
...data,
|
||||
@@ -243,6 +272,7 @@ export function makeTelemetryDiagnostic(
|
||||
id: string,
|
||||
name: string,
|
||||
attributes: { [key: string]: any },
|
||||
tags?: DiagnosticTag[],
|
||||
): DiagnosticMessage {
|
||||
return makeDiagnostic(id, name, {
|
||||
attributes,
|
||||
@@ -251,5 +281,8 @@ export function makeTelemetryDiagnostic(
|
||||
statusPage: false,
|
||||
telemetry: true,
|
||||
},
|
||||
source: {
|
||||
tags,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ export enum EnvVar {
|
||||
*/
|
||||
CLI_VERBOSITY = "CODEQL_VERBOSITY",
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
@@ -83,6 +95,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",
|
||||
|
||||
@@ -161,10 +176,124 @@ export enum EnvVar {
|
||||
RISK_ASSESSMENT_ID = "CODEQL_ACTION_RISK_ASSESSMENT_ID",
|
||||
}
|
||||
|
||||
/** A wrapper around an environment, to allow abstracting away from `process.env` in tests. */
|
||||
export interface Env {
|
||||
/** Tries to get the value for `name` and throws if there isn't one. */
|
||||
getRequired(name: string): string;
|
||||
/** Gets the value for `name`, or `undefined` if it isn't set or empty. */
|
||||
getOptional(name: string): string | undefined;
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ export interface CodeQLDefaultVersionInfo {
|
||||
* Legacy features should end with `_enabled`.
|
||||
*/
|
||||
export enum Feature {
|
||||
/** Allows supported properties of configuration files to be merged. */
|
||||
AllowMergeConfigFiles = "allow_merge_config_files",
|
||||
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||
AllowToolcacheInput = "allow_toolcache_input",
|
||||
@@ -173,6 +175,11 @@ export type FeatureConfig = {
|
||||
};
|
||||
|
||||
export const featureConfig = {
|
||||
[Feature.AllowMergeConfigFiles]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_MERGE_CONFIG_FILES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.AllowMultipleAnalysisKinds]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
|
||||
@@ -203,7 +203,9 @@ async function sendCompletedStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
|
||||
async function run(
|
||||
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ async function sendCompletedStatusReport(
|
||||
async function run({
|
||||
startedAt,
|
||||
logger,
|
||||
}: ActionState<["Logger"]>): Promise<void> {
|
||||
}: ActionState<["Base", "Logger"]>): Promise<void> {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
@@ -11,7 +13,7 @@ import nock from "nock";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { ActionState, StateFeature } from "./action-common";
|
||||
import { ActionsEnv, ActionsEnvVars, getActionVersion } from "./actions-util";
|
||||
import { ActionsEnv, getActionVersion } from "./actions-util";
|
||||
import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
import { GitHubApiDetails } from "./api-client";
|
||||
@@ -19,7 +21,7 @@ import { CachingKind } from "./caching-utils";
|
||||
import * as codeql from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { Env } from "./environment";
|
||||
import { Env, ActionsEnvVars } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -191,7 +193,15 @@ export function getTestActionsEnv(): ActionsEnv {
|
||||
}
|
||||
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
type AllState = ["Logger", "Env", "Actions", "FeatureFlags"];
|
||||
type AllState = [
|
||||
"Base",
|
||||
"Logger",
|
||||
"Env",
|
||||
"ReadOnlyEnv",
|
||||
"Actions",
|
||||
"Api",
|
||||
"FeatureFlags",
|
||||
];
|
||||
|
||||
/** Initialise a fresh `ActionState<AllState>` value. */
|
||||
export function initAllState(
|
||||
@@ -203,40 +213,56 @@ export function initAllState(
|
||||
logger: new RecordingLogger(),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
apiClient: github.getOctokit("123"),
|
||||
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.
|
||||
*/
|
||||
export class TestEnv<
|
||||
abstract class BaseEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> {
|
||||
private readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
|
||||
private args?: Args;
|
||||
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
|
||||
private logger: RecordingLogger;
|
||||
private state: ActionState<AllState>;
|
||||
protected state: ActionState<AllState>;
|
||||
protected checks: Array<DelayedCheck<Args, R, Fs>>;
|
||||
|
||||
constructor(
|
||||
fn: (state: ActionState<Fs>, ...args: Args) => R,
|
||||
cloneFrom?: TestEnv<Args, R, Fs>,
|
||||
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.args = cloneFrom?.args;
|
||||
this.logger = new RecordingLogger();
|
||||
this.state =
|
||||
cloneFrom !== undefined
|
||||
? { ...cloneFrom.state, logger: this.logger }
|
||||
? ({
|
||||
...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 ?? [])];
|
||||
}
|
||||
|
||||
private clone(): TestEnv<Args, R, Fs> {
|
||||
return new TestEnv(this.fn, this);
|
||||
}
|
||||
/**
|
||||
* Creates a clone of this object. Used internally.
|
||||
* Must be overridden by subclasses.
|
||||
*/
|
||||
protected abstract clone(): this;
|
||||
|
||||
public getLogger(): RecordingLogger {
|
||||
return this.logger;
|
||||
@@ -246,48 +272,205 @@ export class TestEnv<
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public getArgs(): Args | undefined {
|
||||
return this.args;
|
||||
}
|
||||
|
||||
public withArgs(...args: Args) {
|
||||
const result = this.clone();
|
||||
result.args = args;
|
||||
public withArgs(...args: Args): CallableEnvBuilder<Args, R, Fs> {
|
||||
const result = new CallableEnvBuilder(this.fn, args, this.clone());
|
||||
return result;
|
||||
}
|
||||
|
||||
public withFeatures(enabled: Feature[]): TestEnv<Args, R, Fs> {
|
||||
public withFeatures(enabled: Feature[]): this {
|
||||
const result = this.clone();
|
||||
result.state.features = createFeatures(enabled);
|
||||
return result;
|
||||
}
|
||||
|
||||
public withEnv(env: Env): TestEnv<Args, R, Fs> {
|
||||
/**
|
||||
* 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();
|
||||
result.state.env = env;
|
||||
setupBaseActionsVars(overrides, result.state.env);
|
||||
return result;
|
||||
}
|
||||
|
||||
public withActions(actions: ActionsEnv): TestEnv<Args, R, Fs> {
|
||||
/**
|
||||
* 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();
|
||||
result.state.actions = actions;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PassedAssertion<R, T> {
|
||||
result: Awaited<R>;
|
||||
assertionResult: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* A more minimal, exported interface for `CallableEnvBuilder`. This makes it easier to
|
||||
* define helper functions in tests which expect a value of a compatible type.
|
||||
*/
|
||||
export interface AssertableTarget<R> {
|
||||
passes<AArgs extends readonly any[], AResult>(
|
||||
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
|
||||
...assertionArgs: AArgs
|
||||
): Promise<PassedAssertion<R, AResult>>;
|
||||
|
||||
throws<ErrorType extends ErrorConstructor | Error>(
|
||||
t: ExecutionContext<unknown>,
|
||||
expectations?: ThrowsExpectation<ErrorType>,
|
||||
): Promise<ThrownError<ErrorType>>;
|
||||
}
|
||||
|
||||
class CallableEnvBuilder<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
>
|
||||
extends BaseEnvBuilder<Args, R, Fs>
|
||||
implements AssertableTarget<R>
|
||||
{
|
||||
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 {
|
||||
if (!this.args) {
|
||||
throw new Error("Trying to call function in TestEnv without arguments.");
|
||||
}
|
||||
return this.fn(this.state as unknown as ActionState<Fs>, ...this.args);
|
||||
}
|
||||
|
||||
public passes<T>(
|
||||
assertion: (makeCall: () => R) => T | Promise<T>,
|
||||
): T | Promise<T> {
|
||||
return assertion(() => {
|
||||
const result = this.call();
|
||||
return result;
|
||||
});
|
||||
/**
|
||||
* 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<PassedAssertion<R, 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 results of the function call and the main assertion.
|
||||
return { result, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +479,8 @@ export function callee<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends readonly StateFeature[],
|
||||
>(fn: (state: ActionState<Fs>, ...args: Args) => R): TestEnv<Args, R, Fs> {
|
||||
return new TestEnv(fn);
|
||||
>(fn: (state: ActionState<Fs>, ...args: Args) => R): EnvBuilder<Args, R, Fs> {
|
||||
return new EnvBuilder(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,11 +514,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,16 +532,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";
|
||||
@@ -488,6 +677,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`.
|
||||
*/
|
||||
|
||||
@@ -54,7 +54,7 @@ async function sendSuccessStatusReport(
|
||||
}
|
||||
}
|
||||
|
||||
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
|
||||
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
try {
|
||||
|
||||
55
src/util.ts
55
src/util.ts
@@ -13,11 +13,14 @@ import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { Env, 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,56 +569,6 @@ export function initializeEnvironment(version: string) {
|
||||
core.exportVariable(EnvVar.VERSION, version);
|
||||
}
|
||||
|
||||
/** Gets an `Env` instance for `env`, which is `process.env` by default. */
|
||||
export function getEnv(env: NodeJS.ProcessEnv = process.env): Env {
|
||||
return {
|
||||
getRequired: (name) => getRequiredEnvVar(env, name),
|
||||
getOptional: (name) => getOptionalEnvVarFrom(env, name),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an environment variable, but throws an error if it is not set.
|
||||
*/
|
||||
export 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
export 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.
|
||||
*/
|
||||
export function getOptionalEnvVar(paramName: string): string | undefined {
|
||||
return getOptionalEnvVarFrom(process.env, paramName);
|
||||
}
|
||||
|
||||
export class HTTPError extends Error {
|
||||
public status: number;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user