Merge pull request #4020 from github/update-v4.37.1-9e7c07009

Merge main into releases/v4
This commit is contained in:
Óscar San José
2026-07-16 17:33:33 +02:00
committed by GitHub
56 changed files with 3313 additions and 1292 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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.
## 4.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)
## 4.37.0 - 08 Jul 2026
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)

View File

@@ -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

File diff suppressed because it is too large Load Diff

260
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.37.0",
"version": "4.37.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.37.0",
"version": "4.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"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.37.0",
"version": "4.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": {

View File

@@ -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

View File

@@ -12,6 +12,6 @@
},
"devDependencies": {
"@types/node": "^20.19.43",
"tsx": "^4.22.4"
"tsx": "^4.23.0"
}
}

View File

@@ -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}' }}`,

View File

@@ -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. */

View File

@@ -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);

View File

@@ -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.

View File

@@ -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({

View File

@@ -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(

View File

@@ -1 +1 @@
{"maximumVersion": "3.22", "minimumVersion": "3.16"}
{"maximumVersion": "3.22", "minimumVersion": "3.17"}

View File

@@ -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.

View File

@@ -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(

View File

@@ -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++";

View File

@@ -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,
);
}
}
});
},
);

View File

@@ -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,

View File

@@ -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}`,
]);
});

View File

@@ -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;
}
/**

View File

@@ -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);
});

View File

@@ -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.
*/

View File

@@ -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,
});
}
});

View File

@@ -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;
}

View File

@@ -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"
}

View File

@@ -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,
},
});
}

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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.

View File

@@ -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 });

View File

@@ -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(),
);
});

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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([

View File

@@ -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) {

View File

@@ -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)
) {

View File

@@ -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. */

View File

@@ -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`.
*/

View File

@@ -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 {

View File

@@ -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;