Compare commits

...

193 Commits

Author SHA1 Message Date
Mario Campos
a35a8ac9ef Update isVersionInfo validation function with check lambda 2026-07-13 23:45:44 -05:00
Mario Campos
ecf4e52a5e Merge remote-tracking branch 'origin/main' into mario-campos/cache-cli-resolve-langs 2026-07-13 23:29:44 -05:00
Mario Campos
0daa052859 Validate CLI memory-backed cached objects against CLI path 2026-07-13 23:22:43 -05:00
Mario Campos
ec96b5271e Delete uncached codeql.resolveLanguages() function call
This CLI command was cached, at one point, but that's been saved for another PR, so this change will too.
2026-07-13 22:37:29 -05:00
Mario Campos
8f1212fe5c Revert src/config-utils.ts
These hunks were related to another hunk elsewhere, that has since now been reverted in favor of a separate/distinct PR.
2026-07-13 22:30:38 -05:00
Michael B. Gale
f133996fb5 Merge pull request #4006 from github/mbg/json/schema-checker
Add schema checker to `json` module
2026-07-13 15:28:24 +00:00
Michael B. Gale
0c2cea31ba Break out of loop to allow result object to be updated after failing fast 2026-07-13 14:47:38 +01:00
Michael B. Gale
3f1e4da48d Propagate and prefer invalidKeys from element validator in array validator 2026-07-13 10:23:24 +01:00
Michael B. Gale
89343ee1aa Fix checkSchema test names 2026-07-13 10:22:19 +01:00
Michael B. Gale
ba5fbf159a Propagate CheckSchemaOptions to allow failFast to work as intended 2026-07-13 10:16:24 +01:00
Michael B. Gale
9a1225df61 Track invalid keys, and use more standard JSON path notation 2026-07-13 10:15:59 +01:00
Michael B. Gale
aec5441507 Add convenience functions to produce CheckSchemaResult values 2026-07-13 10:14:49 +01:00
Michael B. Gale
f8ed33e067 Remove keys from unrecognised set as soon as found 2026-07-13 10:14:13 +01:00
Michael B. Gale
ec0b8ae45f Allow nested checking 2026-07-13 10:14:02 +01:00
Michael B. Gale
36ef889329 Improve type inference for object and validateSchema 2026-07-13 10:13:13 +01:00
Michael B. Gale
50d76e2fa3 Add an object Validator 2026-07-13 10:13:05 +01:00
Michael B. Gale
6a62e99aa7 Add an array Validator 2026-07-13 10:12:56 +01:00
Michael B. Gale
fb5048ee88 Add checkSchema function 2026-07-13 10:12:46 +01:00
Michael B. Gale
8096e4f957 Merge pull request #4005 from github/mbg/test-env/improvements
Improve `TestEnv` for cleaner tests
2026-07-11 05:25:00 +00:00
Michael B. Gale
85830a2768 Merge pull request #4000 from github/mbg/start-proxy/make-git-repos-always-available
Always enable `git_source` registries
2026-07-10 13:44:37 +00:00
Michael B. Gale
29c712e0b7 Merge pull request #4002 from github/dependabot/npm_and_yarn/types/sinon-22.0.0
Bump @types/sinon from 21.0.1 to 22.0.0
2026-07-09 23:10:37 +00:00
Michael B. Gale
ff973a1072 Merge branch 'main' into mbg/start-proxy/make-git-repos-always-available 2026-07-09 22:55:04 +01:00
Michael B. Gale
ed1bc3d57d Merge branch 'main' into dependabot/npm_and_yarn/types/sinon-22.0.0 2026-07-09 22:54:39 +01:00
Michael B. Gale
ac331c28db Merge pull request #4003 from github/dependabot/github_actions/dot-github/workflows/actions-minor-ba9ed4b957
Bump the actions-minor group across 1 directory with 2 updates
2026-07-09 21:54:24 +00:00
Michael B. Gale
9a9ab4cc7d Make throwsAsync arg async 2026-07-09 22:45:33 +01:00
Michael B. Gale
d8866c2578 fixup! Add action vars helpers to BaseEnvBuilder 2026-07-09 22:40:38 +01:00
Mario Campos
d4b99ad098 Merge branch 'main' into mario-campos/cache-cli-resolve-langs
# Conflicts:
#	lib/entry-points.js
#	src/codeql.ts
#	src/environment.ts
#	src/status-report.ts
2026-07-09 15:03:56 -05:00
Mario Campos
5110948a58 Revert runCliJson implementation
It's been forked off into #3981
2026-07-09 14:05:47 -05:00
Michael B. Gale
355aa4a77f Merge pull request #4001 from github/dependabot/npm_and_yarn/npm-minor-10933408d3
Bump the npm-minor group across 1 directory with 4 updates
2026-07-09 19:04:53 +00:00
Michael B. Gale
81d0f34a44 Merge branch 'main' into dependabot/github_actions/dot-github/workflows/actions-minor-ba9ed4b957 2026-07-09 20:04:18 +01:00
Michael B. Gale
d0a69d77dc Clone env and actions so that sinon stubs survive 2026-07-09 20:03:27 +01:00
Michael B. Gale
a1e3f70a6f Add satisfies for state in BaseEnvBuilder constructor 2026-07-09 20:03:27 +01:00
Michael B. Gale
6b3ba17c47 Fix typo 2026-07-09 20:03:27 +01:00
Michael B. Gale
da75ca58fa Add action vars helpers to BaseEnvBuilder 2026-07-09 20:03:26 +01:00
Michael B. Gale
ab6fd520bd Accept Env for setupActionsVars
Also extract env var strings into `ActionsEnvVars`
2026-07-09 20:03:26 +01:00
Michael B. Gale
c77051d879 Allow withActions and withEnv to mutate values 2026-07-09 20:03:26 +01:00
Michael B. Gale
1a57d7ae11 Simplify checking logs 2026-07-09 20:03:26 +01:00
Michael B. Gale
6a782906dc Add abstract BaseEnvBuilder to enforce that clone is overriden by subclasses 2026-07-09 20:03:26 +01:00
Michael B. Gale
a4319e3365 Split EnvBuilder into it + CallableEnvBuilder 2026-07-09 20:03:26 +01:00
Michael B. Gale
a8ac1e029c Rename TestEnv to EnvBuilder 2026-07-09 20:03:25 +01:00
Michael B. Gale
b571644f21 Clean-up passes by splitting it into passes and throws 2026-07-09 20:03:25 +01:00
Michael B. Gale
31842a7e06 Simplify return types for passes 2026-07-09 20:03:25 +01:00
Michael B. Gale
c23977f57b Merge branch 'main' into dependabot/npm_and_yarn/npm-minor-10933408d3 2026-07-09 16:17:22 +01:00
Michael B. Gale
0c7580ae89 Merge pull request #3956 from github/update-supported-enterprise-server-versions
Update supported GitHub Enterprise Server versions
2026-07-09 14:59:18 +00:00
Michael B. Gale
37ea21c1fc Merge pull request #4004 from github/mbg/env-improvements
Improve `Env` abstraction
2026-07-09 14:50:57 +00:00
Michael B. Gale
4b164dbc17 Add CODE_SCANNING_* vars to EnvVar enum 2026-07-09 14:51:22 +01:00
Michael B. Gale
97b52d19da Simplify getTemporaryDirectory 2026-07-09 14:51:22 +01:00
Michael B. Gale
3f2a29899f Default to getEnv in setupBaseActionsVars 2026-07-09 14:51:22 +01:00
Michael B. Gale
80dcb7ccdf Add RUNNER_ENVIRONMENT to ActionsEnvVars 2026-07-09 14:51:22 +01:00
Michael B. Gale
a440e35eda Move ActionsEnvVars to environment.ts 2026-07-09 12:48:49 +01:00
Michael B. Gale
be67a7be93 Add hasChanged method to Env 2026-07-09 12:41:31 +01:00
Michael B. Gale
732911a2fa Turn Env into a class, and add ReadOnlyEnv 2026-07-09 12:38:40 +01:00
Michael B. Gale
1d0e1b20e5 Move env var getters out of util.ts 2026-07-09 12:20:20 +01:00
Michael B. Gale
4c868b3e7e Allow setting environment variables 2026-07-09 12:14:09 +01:00
Michael B. Gale
8d24507592 Allow env input for relevant functions in actions-util.ts 2026-07-09 12:14:07 +01:00
Michael B. Gale
c7d9083db4 Make ALWAYS_ENABLED_REGISTRY_TYPE read-only 2026-07-09 12:05:14 +01:00
Michael B. Gale
2831d9a0e9 Fix previously wrong comment 2026-07-09 12:03:31 +01:00
github-actions[bot]
258af3df60 Rebuild 2026-07-08 17:59:16 +00:00
github-actions[bot]
d8168ab655 Rebuild 2026-07-08 17:57:12 +00:00
dependabot[bot]
ac36b7d725 Bump the actions-minor group across 1 directory with 2 updates
Bumps the actions-minor group with 2 updates in the /.github/workflows directory: [actions/setup-java](https://github.com/actions/setup-java) and [ruby/setup-ruby](https://github.com/ruby/setup-ruby).


Updates `actions/setup-java` from 5.4.0 to 5.5.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](1bcf9fb12c...0f481fcb61)

Updates `ruby/setup-ruby` from 1.314.0 to 1.315.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](9eb537ca03...0dafeac902)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 5.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
- dependency-name: ruby/setup-ruby
  dependency-version: 1.315.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-08 17:57:09 +00:00
dependabot[bot]
9fdfd3e4c8 Bump @types/sinon from 21.0.1 to 22.0.0
Bumps [@types/sinon](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sinon) from 21.0.1 to 22.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sinon)

---
updated-dependencies:
- dependency-name: "@types/sinon"
  dependency-version: 22.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-08 17:55:17 +00:00
dependabot[bot]
1c42f21061 Bump the npm-minor group across 1 directory with 4 updates
Bumps the npm-minor group with 4 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml), [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x), [nock](https://github.com/nock/nock) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `js-yaml` from 5.1.0 to 5.2.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.1.0...5.2.0)

Updates `eslint-plugin-import-x` from 4.17.0 to 4.17.1
- [Release notes](https://github.com/un-ts/eslint-plugin-import-x/releases)
- [Changelog](https://github.com/un-ts/eslint-plugin-import-x/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un-ts/eslint-plugin-import-x/compare/v4.17.0...v4.17.1)

Updates `nock` from 14.0.15 to 14.0.16
- [Release notes](https://github.com/nock/nock/releases)
- [Changelog](https://github.com/nock/nock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nock/nock/compare/v14.0.15...v14.0.16)

Updates `typescript-eslint` from 8.62.0 to 8.62.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: eslint-plugin-import-x
  dependency-version: 4.17.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: nock
  dependency-version: 14.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.62.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-08 17:55:10 +00:00
Michael B. Gale
ef79560bc8 Always enable git_source registries 2026-07-08 17:13:20 +01:00
Michael B. Gale
4581e395f3 Merge pull request #3999 from github/mbg/bump-undici
Bump undici and js-yaml
2026-07-08 15:29:40 +00:00
Michael B. Gale
76d37c509a Bump undici and js-yaml 2026-07-08 15:47:15 +01:00
Mario Campos
0e974e6288 Merge pull request #3980 from github/mario-campos/add-optional-to-json
Support `undefined`, `number` in JSON module
2026-07-08 13:05:24 +00:00
Michael B. Gale
721b977126 Merge pull request #3997 from github/mergeback/v4.37.0-to-main-99df26d4
Mergeback v4.37.0 refs/heads/releases/v4 into main
2026-07-08 12:46:02 +00:00
github-actions[bot]
329fc9cde0 Rebuild 2026-07-08 12:09:56 +00:00
github-actions[bot]
f27c81bb1c Update changelog and version after v4.37.0 2026-07-08 12:09:43 +00:00
Michael B. Gale
99df26d4f1 Merge pull request #3996 from github/update-v4.37.0-c7c896d71
Merge main into releases/v4
2026-07-08 13:08:22 +01:00
Michael B. Gale
31c27074fd Add changenote for #3973 2026-07-08 12:53:26 +01:00
github-actions[bot]
72df2181aa Update changelog for v4.37.0 2026-07-08 11:42:48 +00:00
Michael B. Gale
c7c896d71b Merge pull request #3995 from github/update-bundle/codeql-bundle-v2.26.0
Update default bundle to 2.26.0
2026-07-08 07:57:31 +00:00
github-actions[bot]
3f34ff0ea3 Add changelog note 2026-07-08 07:34:38 +00:00
github-actions[bot]
43bec09f1d Update default bundle to codeql-bundle-v2.26.0 2026-07-08 07:34:33 +00:00
Michael B. Gale
f58f0d11eb Merge pull request #3973 from github/mbg/repo-props/config-file-shorthands
Support shorthand forms for `getRemoteConfig`
2026-07-07 16:55:52 +00:00
Michael B. Gale
7dc37cbb5b Merge remote-tracking branch 'origin/main' into mbg/repo-props/config-file-shorthands 2026-07-06 15:51:36 +01:00
Michael B. Gale
8e22350a7e Thread ActionState to initConfig 2026-07-06 15:46:35 +01:00
Michael B. Gale
69c9e8c7d9 Mark some status-report imports as type-only to avoid circular dependencies 2026-07-06 15:27:54 +01:00
Michael B. Gale
73f4741039 Refactor pack registry code out of config-utils 2026-07-06 15:24:50 +01:00
Michael B. Gale
5094019011 Refactor Config type out of config-utils 2026-07-06 15:14:11 +01:00
Michael B. Gale
006d029c8d Merge pull request #3990 from github/mbg/action-state
Add `ActionState` type and make some initial use of it
2026-07-06 13:47:46 +00:00
Michael B. Gale
d309c0e332 Improve FieldsOf 2026-07-06 14:34:41 +01:00
Michael B. Gale
e34d7fe5bf Fix: Use getTestActionsEnv 2026-07-06 14:32:18 +01:00
Michael B. Gale
5fa9c0c3e2 Add and use getDisplayActionName for log message 2026-07-06 14:20:08 +01:00
Michael B. Gale
eba9d55e1f Merge remote-tracking branch 'origin/main' into mbg/action-state 2026-07-06 14:13:51 +01:00
Michael B. Gale
ae4c8d057d Merge pull request #3993 from github/mbg/env-abstraction
Allow abstracting over `process.env`
2026-07-06 12:29:34 +00:00
Michael B. Gale
73b0850a89 Allow abstracting over process.env 2026-07-06 12:27:42 +01:00
Michael B. Gale
5e3e6b81b9 Add ActionsEnvVars enum 2026-07-06 12:27:26 +01:00
Michael B. Gale
3cf0a529d8 Merge pull request #3986 from github/dependabot/npm_and_yarn/npm-minor-41f3755806
Bump the npm-minor group across 1 directory with 6 updates
2026-07-02 11:17:44 +00:00
Michael B. Gale
de65da38e5 Merge pull request #3991 from github/mergeback/v4.36.3-to-main-54f647b7
Mergeback v4.36.3 refs/heads/releases/v4 into main
2026-07-02 11:14:25 +00:00
Michael B. Gale
b16b719b76 Merge branch 'main' into dependabot/npm_and_yarn/npm-minor-41f3755806 2026-07-02 12:05:12 +01:00
Michael B. Gale
37ae669b89 Update deprecation date 2026-07-02 11:04:03 +01:00
github-actions[bot]
b987514f02 Rebuild 2026-07-02 09:29:32 +00:00
github-actions[bot]
a04a87c9c9 Update changelog and version after v4.36.3 2026-07-02 09:29:19 +00:00
Henry Mercer
54f647b7e1 Merge pull request #3984 from github/update-v4.36.3-1f34ec164
Merge main into releases/v4
2026-07-02 10:27:54 +01:00
Henry Mercer
e78819e055 Trigger checks
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-02 10:15:44 +01:00
Mario Campos
c1e7c7a0de Merge pull request #3981 from github/mario-campos/runCliJson
Create wrapper `runCliJson`
2026-07-01 21:18:51 +00:00
Michael B. Gale
50bd53bfbc Merge pull request #3989 from github/dependabot/npm_and_yarn/js-yaml-5.1.0
Bump js-yaml from 5.0.0 to 5.1.0
2026-07-01 21:08:54 +00:00
Michael B. Gale
289efcad49 Re-add forceQuotes: true 2026-07-01 21:56:08 +01:00
Michael B. Gale
17a28bc4c5 Merge pull request #3985 from github/dependabot/github_actions/dot-github/workflows/actions-minor-fba93c31a5
Bump the actions-minor group across 1 directory with 3 updates
2026-07-01 20:53:01 +00:00
Mario Campos
cbf67da2fa Adjust test name
By @mbg

Co-authored-by: Michael B. Gale <mbg@github.com>
2026-07-01 15:46:20 -05:00
Mario Campos
4c18ab020b Change optional to include undefined
The previous implementation of `optional` is now `optionalOrNull`.
2026-07-01 15:46:15 -05:00
Mario Campos
252ad66b0d Perform deep validation of VersionInfo.features 2026-07-01 15:38:56 -05:00
Mario Campos
d5b8046386 Add JSDoc comment to runCliJson 2026-07-01 14:20:36 -05:00
Mario Campos
a8958a9883 Keep the JSON exception message consistent 2026-07-01 14:20:36 -05:00
github-actions[bot]
1ba744977b Rebuild 2026-07-01 18:06:05 +00:00
dependabot[bot]
e0f825ed66 Bump js-yaml from 5.0.0 to 5.1.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 5.0.0 to 5.1.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.0.0...5.1.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.1.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 18:04:12 +00:00
github-actions[bot]
fbcae9afa6 Rebuild 2026-07-01 18:00:41 +00:00
github-actions[bot]
8f47ee793f Rebuild 2026-07-01 18:00:37 +00:00
dependabot[bot]
67ec1e3914 Bump the npm-minor group across 1 directory with 6 updates
Bumps the npm-minor group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [js-yaml](https://github.com/nodeca/js-yaml) | `5.0.0` | `5.1.0` |
| [semver](https://github.com/npm/node-semver) | `7.8.4` | `7.8.5` |
| [uuid](https://github.com/uuidjs/uuid) | `14.0.0` | `14.0.1` |
| [eslint-plugin-import-x](https://github.com/un-ts/eslint-plugin-import-x) | `4.16.2` | `4.17.0` |
| [globals](https://github.com/sindresorhus/globals) | `17.6.0` | `17.7.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.61.1` | `8.62.0` |



Updates `js-yaml` from 5.0.0 to 5.1.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.0.0...5.1.0)

Updates `semver` from 7.8.4 to 7.8.5
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.4...v7.8.5)

Updates `uuid` from 14.0.0 to 14.0.1
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v14.0.0...v14.0.1)

Updates `eslint-plugin-import-x` from 4.16.2 to 4.17.0
- [Release notes](https://github.com/un-ts/eslint-plugin-import-x/releases)
- [Changelog](https://github.com/un-ts/eslint-plugin-import-x/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un-ts/eslint-plugin-import-x/compare/v4.16.2...v4.17.0)

Updates `globals` from 17.6.0 to 17.7.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.6.0...v17.7.0)

Updates `typescript-eslint` from 8.61.1 to 8.62.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.62.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: semver
  dependency-version: 7.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: uuid
  dependency-version: 14.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: eslint-plugin-import-x
  dependency-version: 4.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: globals
  dependency-version: 17.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 17:59:06 +00:00
dependabot[bot]
1578ba0b4f Bump the actions-minor group across 1 directory with 3 updates
Bumps the actions-minor group with 3 updates in the /.github/workflows directory: [actions/setup-dotnet](https://github.com/actions/setup-dotnet), [actions/setup-java](https://github.com/actions/setup-java) and [ruby/setup-ruby](https://github.com/ruby/setup-ruby).


Updates `actions/setup-dotnet` from 5.3.0 to 5.4.0
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](9a946fdbd5...26b0ec14cb)

Updates `actions/setup-java` from 5.3.0 to 5.4.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](ad2b38190b...1bcf9fb12c)

Updates `ruby/setup-ruby` from 1.313.0 to 1.314.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](89f90524b8...9eb537ca03)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
- dependency-name: actions/setup-java
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
- dependency-name: ruby/setup-ruby
  dependency-version: 1.314.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 17:58:47 +00:00
Michael B. Gale
ef29bf2e43 Improve some comments 2026-07-01 18:55:25 +01:00
Michael B. Gale
ceac4af134 Use the common entry point for analyze 2026-07-01 18:49:33 +01:00
Michael B. Gale
772e8e9310 Use the common entry point for autobuild 2026-07-01 18:47:25 +01:00
Michael B. Gale
fcd9f6cbac Fix logger reference 2026-07-01 18:41:03 +01:00
Michael B. Gale
a9ceac150d Accept ActionState in getConfigFileInput 2026-07-01 18:37:06 +01:00
Michael B. Gale
6136b93088 Use the common entry point for init 2026-07-01 18:37:06 +01:00
Michael B. Gale
6ab762cada Use the common entry point for setup-codeql 2026-07-01 18:37:06 +01:00
Michael B. Gale
a3a1cdd43a Use the common entry point for upload-sarif 2026-07-01 18:37:06 +01:00
Michael B. Gale
9c16c3b47b Add common entry point 2026-07-01 18:37:05 +01:00
Michael B. Gale
b009eac559 Add ActionsEnv support to ActionState 2026-07-01 18:33:42 +01:00
Michael B. Gale
26e9e9e932 Add TestEnv class to simplify testing of ActionState-dependent functions 2026-07-01 18:32:17 +01:00
Michael B. Gale
9b35ab94e1 Add initial ActionState type 2026-07-01 17:29:26 +01:00
Michael B. Gale
e1905475d1 Allow abstracting over process.env 2026-07-01 17:26:53 +01:00
Michael B. Gale
2399df7427 Add ActionsEnvVars enum 2026-07-01 17:23:21 +01:00
github-actions[bot]
2c9d3d63eb Update changelog for v4.36.3 2026-07-01 15:58:40 +00:00
Michael B. Gale
1f34ec1643 Merge pull request #3983 from github/mbg/repo-props/ff-for-config-file-prop
Add FF for configuration file repository property
2026-07-01 15:33:40 +00:00
Michael B. Gale
d5f0145480 Log when repository property has a value but is ignored 2026-07-01 16:22:48 +01:00
Michael B. Gale
f27f56386a Add test for when the FF is off 2026-07-01 15:55:57 +01:00
Michael B. Gale
0025d0f2b5 Use FF 2026-07-01 15:54:22 +01:00
Michael B. Gale
f7fa18f05d Add FF for config file repo property 2026-07-01 15:51:37 +01:00
Mario Campos
7f4f27e7e6 Revert change to resolveExtractor
This is secondary work that will follow from the main purpose of this PR.
2026-07-01 09:41:56 -05:00
Mario Campos
7d1c6e90aa Include codeql path in runCliJson error 2026-06-30 23:13:36 -05:00
Mario Campos
6cdc6063c0 Create wrapper runCliJson 2026-06-30 23:07:02 -05:00
Mario Campos
bba0d6c67c Revert JSON module changes
These have been spun off into #3980.
2026-06-30 22:25:11 -05:00
Mario Campos
ffb3423848 Add isNumber/number validator 2026-06-30 16:35:16 -05:00
Mario Campos
da149812f1 Switch CommandCacheKey back to an enum
The code reads better/simpler as an `enum`.
2026-06-30 16:10:41 -05:00
Mario Campos
0d698171d3 Move output type validators to output-cache.ts module
This move simplifies the consumer-side of the module. And, also, the validators should be an internal implementation detail—not the concern of those who wish to "get" a cached output.
2026-06-30 15:39:36 -05:00
Henry Mercer
628fc3f124 Merge pull request #3979 from github/henrymercer/overlay-db-cleanup-size-telemetry
Measure overlay-base database size at the clear cleanup level
2026-06-30 16:46:03 +00:00
Michael B. Gale
708e106533 Index ActionState over the types of state used 2026-06-30 17:19:20 +01:00
Henry Mercer
9cfb67bab9 Add clarifying comments 2026-06-30 16:49:29 +01:00
Michael B. Gale
b6be81d5b9 Gate new remote file format behind FF 2026-06-30 13:19:30 +01:00
Michael B. Gale
7b2af8940d Add TestEnv class to simplify testing of ActionState-dependent functions 2026-06-30 13:19:10 +01:00
Michael B. Gale
f86aa521ce Use ActionState for getRemoteConfig 2026-06-30 13:03:00 +01:00
Michael B. Gale
e446d55e5a Add initial ActionState type 2026-06-30 12:59:35 +01:00
Mario Campos
c2f43799c5 Refactor CommandCacheKey as a string union 2026-06-29 23:25:50 -05:00
Mario Campos
41965b0e7b Move cache.ts to cli/output-cache.ts
This change should hopefully make the purpose of this module clear.
2026-06-29 22:16:21 -05:00
Henry Mercer
6f8864c8b0 Address review comments 2026-06-26 18:43:09 +01:00
Michael B. Gale
d8d1d6d754 Add FF for shorthands 2026-06-26 18:12:14 +01:00
Michael B. Gale
f77cf557cd Support old and new formats 2026-06-26 18:09:25 +01:00
Henry Mercer
e90a71aaea Add tests for clear cleanup size telemetry 2026-06-26 17:44:30 +01:00
Henry Mercer
ad159d34f6 Measure overlay-base database size at the clear cleanup level
This lets us compare the storage cost of overlay-base and trimmed databases for the same commit.
2026-06-26 17:42:49 +01:00
Michael B. Gale
812b882dee Merge remote-tracking branch 'origin/main' into mbg/repo-props/config-file-shorthands 2026-06-23 18:44:20 +01:00
Michael B. Gale
3fe7ef97d3 Fix getEnv not using getOptionalEnvVarFrom 2026-06-23 18:43:56 +01:00
Michael B. Gale
8d69da902e Improve whitespace handling 2026-06-23 18:42:43 +01:00
Michael B. Gale
00e5a58139 Update format in error message 2026-06-23 18:34:25 +01:00
Michael B. Gale
8102fa6675 Anchor regex and trim input 2026-06-23 18:32:55 +01:00
Michael B. Gale
81ad479e13 Fix test names 2026-06-23 18:31:16 +01:00
Michael B. Gale
12821cff0c Make owner optional in parseRemoteFileAddress 2026-06-23 18:13:16 +01:00
Michael B. Gale
e537ff20a4 Make path optional in parseRemoteFileAddress 2026-06-23 17:51:33 +01:00
Michael B. Gale
598d00854a Make ref optional in parseRemoteFileAddress 2026-06-23 17:50:21 +01:00
Michael B. Gale
85c8a8cebe Add tests for parseRemoteFileAddress 2026-06-23 17:50:01 +01:00
Michael B. Gale
c7a94c979d Add getRemoteConfig JSDoc 2026-06-23 17:03:33 +01:00
Michael B. Gale
82e5ca6c55 Return RemoteFileAddress from parseRemoteFileAddress 2026-06-23 16:49:03 +01:00
Michael B. Gale
9ef8be7176 Refactor parseRemoteFileAddress out of getRemoteConfig 2026-06-23 16:49:03 +01:00
Michael B. Gale
07b6b1eac5 Move getRemoteConfig to config/file.ts 2026-06-23 16:49:03 +01:00
Michael B. Gale
9c4ee01a28 Add RemoteFileAddress type 2026-06-23 16:49:02 +01:00
Michael B. Gale
652296eb9e Allow abstracting over process.env 2026-06-23 16:16:08 +01:00
Michael B. Gale
c05a5b71ad Add ActionsEnvVars enum 2026-06-23 16:16:07 +01:00
Mario Campos
b3c7aa7372 Add warning log for invalid data in command cache retrieval 2026-06-19 18:04:52 -05:00
Mario Campos
649bababe7 Refactor cache storage to use cacheCommandOutput for consistency 2026-06-19 17:49:45 -05:00
Mario Campos
895ffe7eb5 Front-load caching and rear-load saving to disk
This implementation saves CPU and I/O by not trying to write to the file on every set-cache.
2026-06-19 17:45:48 -05:00
Mario Campos
805b59092e Remove duplicate key check in cacheCommandOutput and related tests
This key-check is unnecessary because this function is only ever called if the key does NOT exist in the cache already. In that case, why check again?
2026-06-19 16:29:52 -05:00
Mario Campos
d1bd5ec7b0 Add logging for command cache file read/write failures 2026-06-19 16:07:35 -05:00
Mario Campos
94b12602ac Refactor cache file reading to check for existence before attempting to read 2026-06-19 15:41:47 -05:00
Mario Campos
126166c212 Rename CacheEntry to CommandCacheEntry for consistency 2026-06-19 15:07:56 -05:00
Mario Campos
dd7ff30205 Replace string with CommandCacheKey for better type safety 2026-06-19 15:04:22 -05:00
Mario Campos
420c97eadd Improve documentation for cache file and command keys in CLI 2026-06-19 14:12:27 -05:00
Mario Campos
defcf1bff6 Rename optional and undefinable functions for clarity; update related schemas to use optionalOrNull 2026-06-19 13:55:56 -05:00
Mario Campos
c8e32e423d Refactor resolveLanguages() to cache output according to CLI feature support
The output of `resolveLanguages()` can vary based on whether the flag `--filter-to-languages-with-queries` is included, but not all versions of the CLI support that. This makes caching a single execution problematic, so I opted to cache it based on whether it's supported. If it's supported, it's used; otherwise, it's not.
2026-06-18 14:46:44 -05:00
Mario Campos
553eef0d3f Add error handling for undefined extractors in language resolution 2026-06-18 10:36:50 -05:00
Mario Campos
b18df17ee7 Rebased onto main; fixups were needed 2026-06-18 10:25:23 -05:00
Mario Campos
a602287f73 Refactor CLI caching with in-memory and file storage 2026-06-18 10:10:46 -05:00
Mario Campos
dc8e1e9aa0 Refactor CLI JSON handling into a dedicated runCliJson function 2026-06-18 10:05:20 -05:00
Mario Campos
889ae42672 Refactor CLI executions into helper functions
This provides a separation of concerns between the memoization and the execution.
2026-06-18 10:02:31 -05:00
Mario Campos
587fcb33c7 Refactor isVersionInfo() to use json` module 2026-06-18 09:58:23 -05:00
Mario Campos
445107e23a Validate numbers, objects, and undefinables in the json module
This commit adds a `number` validator`, an `object` validator, an `isNumber` predicate, and `undefinable()` to test optional-but-not-null properties.
2026-06-18 09:58:23 -05:00
Mario Campos
6010f85d81 Reimplement resolveExtractor() as wrapper over resolveLanguages()
By wrapping `resolveLanguages()`, which is memoized, we can avoid executing `codeql resolve extractor` several times over the course of an analysis.
2026-06-18 09:58:23 -05:00
Mario Campos
311292c28f Cache the output of codeql resolve languages
Repeated calls to `resolveLanguages()` will only pay the performance penalty of executing `codeql resolve languages` once.
2026-06-18 09:58:20 -05:00
Michael B. Gale
1023397ae4 Update changelog 2026-06-15 17:46:46 +02:00
Michael B. Gale
68ec727966 Update constants 2026-06-15 17:43:18 +02:00
github-actions[bot]
cb4720a101 Update supported GitHub Enterprise Server versions 2026-06-10 00:49:37 +00:00
75 changed files with 6551 additions and 4137 deletions

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -67,7 +67,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -61,7 +61,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Prepare test

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@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.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@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin

View File

@@ -67,7 +67,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -69,7 +69,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -67,7 +67,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -101,7 +101,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -69,7 +69,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -54,7 +54,7 @@ jobs:
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Set up Ruby
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -77,7 +77,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -71,7 +71,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -69,7 +69,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -67,7 +67,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -74,7 +74,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -68,7 +68,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go

View File

@@ -58,7 +58,7 @@ jobs:
with:
go-version: ^1.13.1
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: '9.x'
- name: Assert best-effort artifact scan completed

View File

@@ -54,7 +54,7 @@ jobs:
with:
go-version: ^1.13.1
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: '9.x'
- name: Assert best-effort artifact scan completed

View File

@@ -46,7 +46,7 @@ jobs:
version: ${{ matrix.version }}
use-all-platform-bundle: true
- name: Install .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: '9.x'
- id: init

View File

@@ -4,6 +4,15 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956)
## 4.37.0 - 08 Jul 2026
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
- In addition to the existing input format, the `config-file` input for the `codeql-action/init` step will soon support a new `[owner/]repo[@ref][:path]` format. All components except the repository name are optional. If omitted, `owner` defaults to the same owner as the repository the analysis is running for, `ref` to `main`, and `path` to `.github/codeql-action.yaml`. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. [#3973](https://github.com/github/codeql-action/pull/3973)
## 4.36.3 - 01 Jul 2026
No user facing changes.
## 4.36.2 - 04 Jun 2026

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.25.6",
"cliVersion": "2.25.6",
"priorBundleVersion": "codeql-bundle-v2.25.5",
"priorCliVersion": "2.25.5"
"bundleVersion": "codeql-bundle-v2.26.0",
"cliVersion": "2.26.0",
"priorBundleVersion": "codeql-bundle-v2.25.6",
"priorCliVersion": "2.25.6"
}

6856
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

212
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.36.3",
"version": "4.37.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.36.3",
"version": "4.37.1",
"license": "MIT",
"workspaces": [
"pr-checks"
@@ -28,12 +28,12 @@
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.0.0",
"js-yaml": "^5.2.0",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
"semver": "^7.8.4",
"uuid": "^14.0.0"
"semver": "^7.8.5",
"uuid": "^14.0.1"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
@@ -47,21 +47,21 @@
"@types/node-forge": "^1.3.14",
"@types/sarif": "^2.1.7",
"@types/semver": "^7.7.1",
"@types/sinon": "^21.0.1",
"@types/sinon": "^22.0.0",
"ava": "^6.4.1",
"esbuild": "^0.28.1",
"eslint": "^9.39.4",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-github": "^6.0.0",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.6.0",
"nock": "^14.0.15",
"globals": "^17.7.0",
"nock": "^14.0.16",
"sinon": "^22.0.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.1"
"typescript-eslint": "^8.62.1"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2274,13 +2274,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@package-json/types": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz",
"integrity": "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==",
"dev": true,
"license": "MIT"
},
"node_modules/@pkgr/core": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz",
@@ -2579,9 +2572,9 @@
"license": "MIT"
},
"node_modules/@types/sinon": {
"version": "21.0.1",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz",
"integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==",
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-22.0.0.tgz",
"integrity": "sha512-TDbVpbccc2HfiqHR09Argj3mHV1KMW7sCCKj52fsl8lbRLkEn7fB1966EWhOKWUBcqfBueZuPoA7/OK1CKiy3g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2594,17 +2587,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/type-utils": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/type-utils": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -2617,7 +2610,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.61.1",
"@typescript-eslint/parser": "^8.62.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -2633,16 +2626,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3"
},
"engines": {
@@ -2676,14 +2669,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.61.1",
"@typescript-eslint/types": "^8.61.1",
"@typescript-eslint/tsconfig-utils": "^8.62.1",
"@typescript-eslint/types": "^8.62.1",
"debug": "^4.4.3"
},
"engines": {
@@ -2716,14 +2709,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1"
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2734,9 +2727,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2751,15 +2744,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -2794,9 +2787,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2808,16 +2801,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.61.1",
"@typescript-eslint/tsconfig-utils": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"@typescript-eslint/project-service": "8.62.1",
"@typescript-eslint/tsconfig-utils": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/visitor-keys": "8.62.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2846,9 +2839,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2893,16 +2886,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1"
"@typescript-eslint/scope-manager": "8.62.1",
"@typescript-eslint/types": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2917,13 +2910,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/types": "8.62.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -5132,13 +5125,12 @@
}
},
"node_modules/eslint-plugin-import-x": {
"version": "4.16.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.2.tgz",
"integrity": "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==",
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz",
"integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@package-json/types": "^0.0.12",
"@typescript-eslint/types": "^8.56.0",
"comment-parser": "^1.4.1",
"debug": "^4.4.1",
@@ -6143,9 +6135,9 @@
}
},
"node_modules/globals": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
"integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6986,9 +6978,9 @@
}
},
"node_modules/js-yaml": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.0.0.tgz",
"integrity": "sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.0.tgz",
"integrity": "sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==",
"funding": [
{
"type": "github",
@@ -7451,9 +7443,9 @@
"license": "MIT"
},
"node_modules/nock": {
"version": "14.0.15",
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.15.tgz",
"integrity": "sha512-S0a47C9pLvcYx/Ugf0H30BVBEcUgMMBDk9VJIDlJ8XGrfH2QDUD4Tgdp45qDIiHttokBG+IbsOtsvIjGR/j3bg==",
"version": "14.0.16",
"resolved": "https://registry.npmjs.org/nock/-/nock-14.0.16.tgz",
"integrity": "sha512-8r4KEc6nT1D/fdLD/R1BO1CPaVEL8o40u/guFRJlXabN7vr3RmMqyjsY5Krt0nMwhsOAwXQ/mtN5vy5Jh3aErg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8367,9 +8359,9 @@
}
},
"node_modules/semver": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8904,9 +8896,9 @@
}
},
"node_modules/supertap/node_modules/js-yaml": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9325,16 +9317,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
"version": "8.62.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.61.1",
"@typescript-eslint/parser": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1"
"@typescript-eslint/eslint-plugin": "8.62.1",
"@typescript-eslint/parser": "8.62.1",
"@typescript-eslint/typescript-estree": "8.62.1",
"@typescript-eslint/utils": "8.62.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -9368,9 +9360,9 @@
}
},
"node_modules/undici": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"license": "MIT",
"engines": {
"node": ">=18.17"
@@ -9498,9 +9490,9 @@
"license": "MIT"
},
"node_modules/uuid": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -9818,7 +9810,7 @@
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": ">=9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"semver": "^7.8.0",
"semver": "^7.8.5",
"yaml": "^2.9.0"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.36.3",
"version": "4.37.1",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -36,12 +36,12 @@
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.0.0",
"js-yaml": "^5.2.0",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
"semver": "^7.8.4",
"uuid": "^14.0.0"
"semver": "^7.8.5",
"uuid": "^14.0.1"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
@@ -55,21 +55,21 @@
"@types/node-forge": "^1.3.14",
"@types/sarif": "^2.1.7",
"@types/semver": "^7.7.1",
"@types/sinon": "^21.0.1",
"@types/sinon": "^22.0.0",
"ava": "^6.4.1",
"esbuild": "^0.28.1",
"eslint": "^9.39.4",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-github": "^6.0.0",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.6.0",
"nock": "^14.0.15",
"globals": "^17.7.0",
"nock": "^14.0.16",
"sinon": "^22.0.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.1"
"typescript-eslint": "^8.62.1"
},
"overrides": {
"@actions/tool-cache": {

View File

@@ -5,7 +5,7 @@ versions:
- default
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@89f90524b88a01fe6e0b732220432cc6142926af # v1.313.0
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -7,7 +7,7 @@
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": ">=9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"semver": "^7.8.0",
"semver": "^7.8.5",
"yaml": "^2.9.0"
},
"devDependencies": {

View File

@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
name: "Install Java",
uses: pinnedUses(
"actions/setup-java",
"ad2b38190b15e4d6bdf0c97fb4fca8412226d287",
"v5.3.0",
"0f481fcb613427c0f801b606911222b5b6f3083a",
"v5.5.0",
),
with: {
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
@@ -288,8 +288,8 @@ const languageSetups: LanguageSetups = {
name: "Install .NET",
uses: pinnedUses(
"actions/setup-dotnet",
"9a946fdbd5fb07b82b2f5a4466058b876ab72bb2",
"v5.3.0",
"26b0ec14cb23fa6904739307f278c14f94c95bf1",
"v5.4.0",
),
with: {
"dotnet-version": `\${{ inputs.dotnet-version || '${defaultLanguageVersions.csharp}' }}`,

95
src/action-common.ts Normal file
View File

@@ -0,0 +1,95 @@
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
import { Env } from "./environment";
import { FeatureEnablement } from "./feature-flags";
import { getActionsLogger, Logger } from "./logging";
import {
ActionName,
getDisplayActionName,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { getEnv, getErrorMessage } from "./util";
/** Common state that is always available in `ActionState`. */
export interface BaseState {
/** The name of the Action. */
name: ActionName;
/** When the Action was started. */
startedAt: Date;
}
/** Describes different state features that an Action may have. */
export interface FeatureState {
Logger: {
/** The logger that is in use. */
logger: Logger;
};
Env: {
/** Information about environment variables. */
env: Env;
};
Actions: {
/** Access to Actions-related functionality. */
actions: ActionsEnv;
};
FeatureFlags: {
/** Information about enabled feature flags. */
features: FeatureEnablement;
};
}
/** Identifies a type of state an Action may have. */
export type StateFeature = keyof FeatureState;
/** Constructs the intersection of all state types identifies by `Fs`. */
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
? BaseState
: Fs extends [
infer Head extends StateFeature,
...infer Tail extends readonly StateFeature[],
]
? FeatureState[Head] & FieldsOf<Tail>
: never;
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
/** The type of an Action's main entry point. This is a function that is provided
* with a basic `ActionState` object with features that are always available.
* Each Action can then augment the `state` further if additional features are required.
*/
export type ActionMain = (
state: ActionState<["Logger", "Env", "Actions"]>,
) => Promise<void>;
/** A specification for a CodeQL Action step. */
export interface Action {
/** The name of the Action. */
name: ActionName;
/** The entry point for the Action. */
run: ActionMain;
}
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
export async function runInActions(action: Action) {
const startedAt = new Date();
const logger = getActionsLogger();
const env = getEnv();
const actionsEnv = getActionsEnv();
try {
await action.run({
name: action.name,
startedAt,
logger,
env,
actions: actionsEnv,
});
} catch (error) {
core.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
);
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
}
}

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";
/**
@@ -61,17 +62,21 @@ export const getOptionalInput = function (name: string): string | undefined {
return value.length > 0 ? value : undefined;
};
export function getTemporaryDirectory(): string {
const value = process.env["CODEQL_ACTION_TEMP"];
return value !== undefined && value !== ""
? value
: getRequiredEnvParam("RUNNER_TEMP");
/**
* Gets the temporary directory used by the CodeQL Action. This will either be the temporary
* directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the
* value of `RUNNER_TEMP` otherwise.
*/
export function getTemporaryDirectory(env: Env = getEnv()): string {
return (
env.getOptional(EnvVar.TEMP) ?? env.getRequired(ActionsEnvVars.RUNNER_TEMP)
);
}
const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json";
export function getDiffRangesJsonFilePath(): string {
return path.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME);
export function getDiffRangesJsonFilePath(env: Env = getEnv()): string {
return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME);
}
export function getActionVersion(): string {
@@ -83,16 +88,16 @@ export function getActionVersion(): string {
*
* This will be "dynamic" for default setup workflow runs.
*/
export function getWorkflowEventName() {
return getRequiredEnvParam("GITHUB_EVENT_NAME");
export function getWorkflowEventName(env: Env = getEnv()) {
return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME);
}
/**
* Returns whether the current workflow is executing a local copy of the Action, e.g. we're running
* a workflow on the codeql-action repo itself.
*/
export function isRunningLocalAction(): boolean {
const relativeScriptPath = getRelativeScriptPath();
export function isRunningLocalAction(env: Env = getEnv()): boolean {
const relativeScriptPath = getRelativeScriptPath(env);
return (
relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath)
);
@@ -103,15 +108,15 @@ export function isRunningLocalAction(): boolean {
*
* This can be used to get the Action's name or tell if we're running a local Action.
*/
function getRelativeScriptPath(): string {
const runnerTemp = getRequiredEnvParam("RUNNER_TEMP");
function getRelativeScriptPath(env: Env): string {
const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP);
const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions");
return path.relative(actionsDirectory, __filename);
}
/** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */
export function getWorkflowEvent(): any {
const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH");
export function getWorkflowEvent(env: Env = getEnv()): any {
const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH);
try {
return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8"));
} catch (e) {
@@ -180,17 +185,17 @@ export function getUploadValue(input: string | undefined): UploadKind {
/**
* Get the workflow run ID.
*/
export function getWorkflowRunID(): number {
const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID");
export function getWorkflowRunID(env: Env = getEnv()): number {
const workflowRunIdString = env.getRequired(ActionsEnvVars.GITHUB_RUN_ID);
const workflowRunID = parseInt(workflowRunIdString, 10);
if (Number.isNaN(workflowRunID)) {
throw new Error(
`GITHUB_RUN_ID must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
`${ActionsEnvVars.GITHUB_RUN_ID} must define a non NaN workflow run ID. Current value is ${workflowRunIdString}`,
);
}
if (workflowRunID < 0) {
throw new Error(
`GITHUB_RUN_ID must be a non-negative integer. Current value is ${workflowRunIdString}`,
`${ActionsEnvVars.GITHUB_RUN_ID} must be a non-negative integer. Current value is ${workflowRunIdString}`,
);
}
return workflowRunID;
@@ -199,17 +204,19 @@ export function getWorkflowRunID(): number {
/**
* Get the workflow run attempt number.
*/
export function getWorkflowRunAttempt(): number {
const workflowRunAttemptString = getRequiredEnvParam("GITHUB_RUN_ATTEMPT");
export function getWorkflowRunAttempt(env: Env = getEnv()): number {
const workflowRunAttemptString = env.getRequired(
ActionsEnvVars.GITHUB_RUN_ATTEMPT,
);
const workflowRunAttempt = parseInt(workflowRunAttemptString, 10);
if (Number.isNaN(workflowRunAttempt)) {
throw new Error(
`GITHUB_RUN_ATTEMPT must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must define a non NaN workflow run attempt. Current value is ${workflowRunAttemptString}`,
);
}
if (workflowRunAttempt <= 0) {
throw new Error(
`GITHUB_RUN_ATTEMPT must be a positive integer. Current value is ${workflowRunAttemptString}`,
`${ActionsEnvVars.GITHUB_RUN_ATTEMPT} must be a positive integer. Current value is ${workflowRunAttemptString}`,
);
}
return workflowRunAttempt;
@@ -266,18 +273,18 @@ export const getFileType = async (filePath: string): Promise<string> => {
}
};
export function isSelfHostedRunner() {
return process.env.RUNNER_ENVIRONMENT === "self-hosted";
export function isSelfHostedRunner(env: Env = getEnv()) {
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
}
/** Determines whether the workflow trigger is `dynamic`. */
export function isDynamicWorkflow(): boolean {
return getWorkflowEventName() === "dynamic";
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
return getWorkflowEventName(env) === "dynamic";
}
/** Determines whether we are running in default setup. */
export function isDefaultSetup(): boolean {
return isDynamicWorkflow();
export function isDefaultSetup(env: Env = getEnv()): boolean {
return isDynamicWorkflow(env);
}
export function prettyPrintInvocation(cmd: string, args: string[]): string {
@@ -375,9 +382,10 @@ const persistedInputsKey = "persisted_inputs";
* This would be simplified if actions/runner#3514 is addressed.
* https://github.com/actions/runner/issues/3514
*/
export const persistInputs = function () {
const inputEnvironmentVariables = Object.entries(process.env).filter(
([name]) => name.startsWith("INPUT_"),
export const persistInputs = function (env: Env = getEnv()) {
const entries = env.entries();
const inputEnvironmentVariables = entries.filter(([name]) =>
name.startsWith("INPUT_"),
);
core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables));
};
@@ -405,7 +413,9 @@ export interface PullRequestBranches {
* @returns the base and head branches of the pull request, or undefined if
* we are not analyzing a pull request.
*/
export function getPullRequestBranches(): PullRequestBranches | undefined {
export function getPullRequestBranches(
env: Env = getEnv(),
): PullRequestBranches | undefined {
const pullRequest = github.context.payload.pull_request;
if (pullRequest) {
return {
@@ -419,8 +429,10 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
// PR analysis under Default Setup does not have the pull_request context,
// but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH.
const codeScanningRef = process.env.CODE_SCANNING_REF;
const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH;
const codeScanningRef = env.getOptional(EnvVar.CODE_SCANNING_REF);
const codeScanningBaseBranch = env.getOptional(
EnvVar.CODE_SCANNING_BASE_BRANCH,
);
if (codeScanningRef && codeScanningBaseBranch) {
return {
base: codeScanningBaseBranch,
@@ -435,8 +447,8 @@ export function getPullRequestBranches(): PullRequestBranches | undefined {
/**
* Returns whether we are analyzing a pull request.
*/
export function isAnalyzingPullRequest(): boolean {
return getPullRequestBranches() !== undefined;
export function isAnalyzingPullRequest(env: Env = getEnv()): boolean {
return getPullRequestBranches(env) !== undefined;
}
/**
@@ -460,13 +472,14 @@ const qualityCategoryMapping: Record<string, string> = {
export function fixCodeQualityCategory(
logger: Logger,
category?: string,
env: Env = getEnv(),
): string | undefined {
// The `category` should always be set by Default Setup. We perform this check
// to avoid potential issues if Code Quality supports Advanced Setup in the future
// and before this workaround is removed.
if (
category !== undefined &&
isDefaultSetup() &&
isDefaultSetup(env) &&
category.startsWith("/language:")
) {
const language = category.substring("/language:".length);

View File

@@ -4,6 +4,7 @@ import { performance } from "perf_hooks";
import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import * as actionsUtil from "./actions-util";
import * as analyses from "./analyses";
import {
@@ -40,7 +41,6 @@ import {
createStatusReportBase,
DatabaseCreationTimings,
getActionsStatus,
sendUnhandledErrorStatusReport,
StatusReportBase,
} from "./status-report";
import {
@@ -212,7 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
await runAutobuild(config, BuiltInLanguage.go, logger);
}
async function run(startedAt: Date) {
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -228,7 +228,6 @@ async function run(startedAt: Date) {
let didUploadTrapCaches = false;
let dependencyCacheResults: DependencyCacheUploadStatusReport | undefined;
let databaseUploadResults: DatabaseUploadResult[] = [];
const logger = getActionsLogger();
try {
util.initializeEnvironment(actionsUtil.getActionVersion());
@@ -523,19 +522,13 @@ async function run(startedAt: Date) {
}
}
/** Defines the `analyze` Action. */
const analyze: Action = {
name: ActionName.Analyze,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`analyze action failed: ${util.getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Analyze,
startedAt,
error,
logger,
);
}
await runInActions(analyze);
await util.checkForTimeout();
}

View File

@@ -285,7 +285,7 @@ extensions:
// characters are escaped, and that the path is always rendered as a
// quoted string on a single line.
return (
` - [${yaml.dump(filename, { quoteStyle: "single" }).trim()}, ` +
` - [${yaml.dump(filename, { forceQuotes: true, quoteStyle: "single" }).trim()}, ` +
`${range.startLine}, ${range.endLine}]\n`
);
})

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

@@ -3,7 +3,7 @@ import * as githubUtils from "@actions/github/lib/utils";
import * as retry from "@octokit/plugin-retry";
import { getActionVersion, getRequiredInput } from "./actions-util";
import { EnvVar } from "./environment";
import { EnvVar, ReadOnlyEnv, ActionsEnvVars, getEnv } from "./environment";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
import {
@@ -67,16 +67,16 @@ function createApiClientWithDetails(
);
}
export function getApiDetails(): GitHubApiDetails {
export function getApiDetails(env: ReadOnlyEnv = getEnv()): GitHubApiDetails {
return {
auth: getRequiredInput("token"),
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
url: env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
apiURL: env.getRequired(ActionsEnvVars.GITHUB_API_URL),
};
}
export function getApiClient() {
return createApiClientWithDetails(getApiDetails());
export function getApiClient(env: ReadOnlyEnv = getEnv()) {
return createApiClientWithDetails(getApiDetails(env));
}
export function getApiClientWithExternalAuth(

View File

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

View File

@@ -1,5 +1,6 @@
import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import {
getActionVersion,
getOptionalInput,
@@ -11,13 +12,12 @@ import { getCodeQL } from "./codeql";
import { Config, getConfig } from "./config-utils";
import { EnvVar } from "./environment";
import { Language } from "./languages";
import { Logger, getActionsLogger } from "./logging";
import { Logger } from "./logging";
import {
StatusReportBase,
getActionsStatus,
createStatusReportBase,
sendStatusReport,
sendUnhandledErrorStatusReport,
ActionName,
} from "./status-report";
import { endTracingForCluster } from "./tracer-config";
@@ -26,7 +26,6 @@ import {
checkDiskUsage,
checkGitHubVersionInRange,
ConfigurationError,
getErrorMessage,
initializeEnvironment,
wrapError,
} from "./util";
@@ -69,11 +68,10 @@ async function sendCompletedStatusReport(
}
}
async function run(startedAt: Date) {
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
let config: Config | undefined;
let currentLanguage: Language | undefined;
let languages: Language[] | undefined;
@@ -142,18 +140,12 @@ async function run(startedAt: Date) {
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
}
/** Defines the `autobuild` Action. */
const autobuild: Action = {
name: ActionName.Autobuild,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`autobuild action failed. ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Autobuild,
startedAt,
error,
logger,
);
}
await runInActions(autobuild);
}

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

@@ -0,0 +1,220 @@
import * as fs from "fs";
import * as os from "os";
import path from "path";
import test from "ava";
import { setupTests } from "../testing-utils";
import {
cacheCommandOutput,
CommandCacheKey,
getCachedCommandOutput,
resetCachedCommandOutputs,
type VersionInfo,
} from "./output-cache";
setupTests(test);
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
/**
* Runs `body` with a temporary directory configured as the cache's backing
* store (`RUNNER_TEMP`). `CODEQL_ACTION_TEMP` is cleared so that
* `getTemporaryDirectory()` falls back to `RUNNER_TEMP`.
*
* `setupTests` snapshots and restores `process.env` around every test, so we
* don't restore the environment variables we set here ourselves.
*/
async function withCacheDir(
body: (cacheFilePath: string) => Promise<void> | void,
): Promise<void> {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cache-test-"));
process.env["RUNNER_TEMP"] = tmpDir;
delete process.env["CODEQL_ACTION_TEMP"];
resetCachedCommandOutputs();
try {
await body(path.join(tmpDir, COMMAND_CACHE_FILENAME));
} finally {
await fs.promises.rm(tmpDir, { force: true, recursive: true });
}
}
function writeCacheFile(
cacheFilePath: string,
contents: Record<string, unknown>,
): void {
fs.writeFileSync(cacheFilePath, JSON.stringify(contents));
}
test.serial(
"getCachedCommandOutput reuses an output persisted by an earlier step",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.20.0" },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
{ version: "2.20.0" },
);
});
},
);
test.serial(
"getCachedCommandOutput ignores an output persisted from a different CLI",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/other-codeql",
output: { version: "2.20.0" },
},
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput ignores a malformed cache file",
async (t) => {
await withCacheDir((cacheFilePath) => {
fs.writeFileSync(cacheFilePath, "not valid json");
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput returns undefined when there is no cache file",
async (t) => {
await withCacheDir(() => {
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput ignores an output that fails validation",
async (t) => {
await withCacheDir((cacheFilePath) => {
for (const output of [
{},
{ version: 2 },
{ version: "2.20.0", overlayVersion: "1" },
{ version: "2.20.0", features: "nope" },
]) {
resetCachedCommandOutputs();
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: { cmd: "/path/to/codeql", output },
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
JSON.stringify(output),
);
}
});
},
);
test.serial(
"getCachedCommandOutput ignores an entry missing the cmd field",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: { output: { version: "2.20.0" } },
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial("cacheCommandOutput persists the output to the memo", async (t) => {
await withCacheDir(() => {
const output: VersionInfo = { version: "2.20.0" };
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
// Tier 1: the value is immediately available from the memo.
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
output,
);
});
});
test.serial(
"getCachedCommandOutput treats a memoized output from another CLI as a miss",
async (t) => {
await withCacheDir(() => {
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
version: "2.20.0",
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput prefers the in-memory memo over the file",
async (t) => {
await withCacheDir((cacheFilePath) => {
const output: VersionInfo = { version: "2.20.0", overlayVersion: 1 };
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
// Overwrite the file with a different value; the memo (tier 1) should win.
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.21.0" },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
output,
);
});
},
);
test.serial(
"getCachedCommandOutput falls back to file when memoized output comes from another CLI",
async (t) => {
await withCacheDir((cacheFilePath) => {
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
version: "2.19.0",
});
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.20.0", overlayVersion: 1 },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
{ version: "2.20.0", overlayVersion: 1 },
);
});
},
);

238
src/cli/output-cache.ts Normal file
View File

@@ -0,0 +1,238 @@
import * as fs from "fs";
import * as path from "path";
import { getTemporaryDirectory } from "../actions-util";
import * as json from "../json";
import { getActionsLogger } from "../logging";
/**
* The name of the temporary file that backs the on-disk cache of
* CLI responses between workflow steps.
*/
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
/** A key used to identify cached command output. */
export enum CommandCacheKey {
Version = "version",
ResolveLanguages = "resolve languages",
}
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
* analysis with that pack. Otherwise, if the overlay versions are different,
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
/** Returns true if `x` is a {@link VersionInfo}. */
export function isVersionInfo(x: unknown): x is VersionInfo {
const isBooleanRecord = (obj: unknown): obj is Record<string, boolean> =>
json.isObject(obj) &&
Object.values(obj).every((val) => typeof val === "boolean");
return (
json.isObject(x) &&
json.validateSchema(
{
version: json.string,
features: {
validate: isBooleanRecord,
check: (obj) => ({
unknownKeys: [],
invalidKeys: [],
valid: isBooleanRecord(obj),
}),
required: false,
},
overlayVersion: json.optional(json.number),
},
x,
)
);
}
export interface ResolveLanguagesOutput {
aliases?: {
[alias: string]: string;
};
extractors: {
[language: string]: Array<{
extractor_root: string;
extractor_options?: any;
}>;
};
}
/** Returns true if `x` is a {@link ResolveLanguagesOutput}. */
export function isResolveLanguagesOutput(
x: unknown,
): x is ResolveLanguagesOutput {
return (
json.isObject<ResolveLanguagesOutput>(x) &&
json.isObject(x.extractors) &&
Object.values(x.extractors).every(
(extractorList) =>
json.isArray(extractorList) &&
extractorList.every(
(extractor) =>
json.isObject<{ extractor_root: unknown }>(extractor) &&
json.isString(extractor.extractor_root),
),
) &&
(x.aliases === undefined ||
(json.isObject(x.aliases) &&
Object.values(x.aliases).every((alias) => json.isString(alias))))
);
}
export type CommandCacheKeyOutputMap = {
[CommandCacheKey.Version]: VersionInfo;
[CommandCacheKey.ResolveLanguages]: ResolveLanguagesOutput;
};
const commandCacheValidators: {
[K in CommandCacheKey]: (
output: unknown,
) => output is CommandCacheKeyOutputMap[K];
} = {
[CommandCacheKey.Version]: isVersionInfo,
[CommandCacheKey.ResolveLanguages]: isResolveLanguagesOutput,
};
interface StoredCommandCacheEntry {
cmd: string;
output: unknown;
}
/** A single cached command output together with the CLI path it came from. */
export interface CommandCacheEntry<K extends CommandCacheKey> {
/**
* The path to the CodeQL CLI that produced `output`. Persisted so that a
* different step using a different CodeQL bundle doesn't pick up a stale
* value.
*/
cmd: string;
output: CommandCacheKeyOutputMap[K];
}
/**
* Tier 1: the in-process memo. Consulted first on every lookup and populated
* whenever a value is read from the file (tier 2) or computed via the CLI
* (tier 3).
*/
const inMemoryCache = new Map<
CommandCacheKey,
CommandCacheEntry<CommandCacheKey>
>();
const logger = getActionsLogger();
function getCommandCacheFilePath(): string {
return path.join(getTemporaryDirectory(), COMMAND_CACHE_FILENAME);
}
/**
* Reads and parses the temporary cache file. Best-effort: a missing, malformed,
* or otherwise unreadable file is treated as an empty cache.
*/
function readCommandCacheFile(): Record<string, StoredCommandCacheEntry> {
if (!fs.existsSync(getCommandCacheFilePath())) {
return {};
}
try {
const contents = fs.readFileSync(getCommandCacheFilePath(), "utf8");
const parsed = json.parseString(contents);
if (json.isObject(parsed)) {
return parsed;
}
} catch (e) {
logger.warning(`Failed to read or parse command cache file: ${e}`);
}
return {};
}
/**
* Persists the in-memory cache to the temporary file. Best-effort: a failure to write
* just means a later step re-runs the CLI.
*/
export function writeCommandCacheFile(): void {
try {
fs.writeFileSync(
getCommandCacheFilePath(),
JSON.stringify(Object.fromEntries(inMemoryCache)),
);
} catch (e) {
logger.warning(`Failed to write command cache file: ${e}`);
}
}
/**
* Stores the output of a CLI command under `key` in a module-global object.
*/
export function cacheCommandOutput<K extends CommandCacheKey>(
key: K,
cmd: string,
output: CommandCacheKeyOutputMap[K],
): void {
const entry: CommandCacheEntry<K> = { cmd, output };
inMemoryCache.set(key, entry);
}
/**
* Returns the cached output for `key`, or `undefined` if it isn't cached.
*
* Resolves tier 1 (in-memory memo) first, then tier 2 (temporary file). A value
* loaded from the file is ignored unless its `cmd` matches the optional `cmd`
* argument and its output satisfies the internal validator for `key`; valid
* values are memoized into tier 1 before being returned.
*
* A return value of `undefined` signals the caller to fall back to tier 3 (the
* CLI).
*/
export function getCachedCommandOutput<K extends CommandCacheKey>(
key: K,
cmd?: string,
): CommandCacheKeyOutputMap[K] | undefined {
// Tier 1: the in-memory variable.
const memoized = inMemoryCache.get(key);
if (memoized !== undefined) {
if (cmd === undefined || memoized.cmd === cmd) {
return memoized.output as CommandCacheKeyOutputMap[K];
}
// If the memoized entry doesn't match the requested CLI,
// treat it as a miss and fall back to tier 2, the file.
inMemoryCache.delete(key);
}
// Tier 2: the temporary file persisted by an earlier step, if any.
const entry = readCommandCacheFile()[key] as unknown;
if (
!json.isObject<StoredCommandCacheEntry>(entry) ||
!json.isString(entry.cmd) ||
(cmd !== undefined && entry.cmd !== cmd) ||
!commandCacheValidators[key](entry.output)
) {
logger.warning("Received invalid data from the command-cache file.");
return undefined;
}
// Memoize so subsequent lookups in this process hit tier 1.
const cachedEntry = entry as StoredCommandCacheEntry;
const output = cachedEntry.output as CommandCacheKeyOutputMap[K];
cacheCommandOutput(key, cachedEntry.cmd, output);
return output;
}
/**
* Clears the in-process memo (tier 1). Only for use in tests, which exercise
* multiple "steps" within a single process.
*/
export function resetCachedCommandOutputs(): void {
inMemoryCache.clear();
}

View File

@@ -12,6 +12,14 @@ import {
runTool,
} from "./actions-util";
import * as api from "./api-client";
import {
cacheCommandOutput,
CommandCacheKey,
getCachedCommandOutput,
type CommandCacheKeyOutputMap,
type ResolveLanguagesOutput,
type VersionInfo,
} from "./cli/output-cache";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
@@ -216,36 +224,10 @@ export interface CodeQL {
): Promise<void>;
}
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
* analysis with that pack. Otherwise, if the overlay versions are different,
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
export interface ResolveDatabaseOutput {
overlayBaseSpecifier?: string;
}
export interface ResolveLanguagesOutput {
aliases?: {
[alias: string]: string;
};
extractors: {
[language: string]: Array<{
extractor_root: string;
extractor_options?: any;
}>;
};
}
export interface ResolveBuildEnvironmentOutput {
configuration?: {
[language: string]: {
@@ -272,17 +254,17 @@ const CODEQL_MINIMUM_VERSION = "2.19.4";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
*/
const CODEQL_NEXT_MINIMUM_VERSION = "2.19.4";
const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7";
/**
* This is the version of GHES that was most recently deprecated.
*/
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15";
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16";
/**
* This is the deprecation date for the version of GHES that was most recently deprecated.
*/
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09";
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
/** The CLI verbosity level to use for extraction in debug mode. */
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
@@ -495,6 +477,29 @@ export async function getCodeQLForTesting(
return getCodeQLForCmd(cmd, false);
}
/**
* Returns the cached output for a `codeql` command, resolving through the three
* caching tiers in order: the in-memory memo, then the temporary file (both via
* {@link getCachedCommandOutput}), and finally the CLI itself by invoking `run`
* on a miss and persisting its result back into the first two tiers.
*
* @param key The cache key identifying the command's output.
* @param cmd The path to the CodeQL CLI the output is obtained from.
* @param run Invokes the CLI to compute the output when it isn't cached.
*/
async function getCachedOrRun<K extends CommandCacheKey>(
key: K,
cmd: string,
run: () => Promise<CommandCacheKeyOutputMap[K]>,
): Promise<CommandCacheKeyOutputMap[K]> {
let result = getCachedCommandOutput(key, cmd);
if (result === undefined) {
result = await run();
cacheCommandOutput(key, cmd, result);
}
return result;
}
/**
* Return a CodeQL object for CodeQL CLI access.
*
@@ -512,21 +517,11 @@ async function getCodeQLForCmd(
return cmd;
},
async getVersion() {
let result = util.getCachedCodeQlVersion(cmd);
if (result === undefined) {
const output = await runCli(cmd, ["version", "--format=json"], {
return getCachedOrRun(CommandCacheKey.Version, cmd, () => {
return runCliJson<VersionInfo>(cmd, ["version", "--format=json"], {
noStreamStdout: true,
});
try {
result = JSON.parse(output) as VersionInfo;
} catch {
throw Error(
`Invalid JSON output from \`version --format=json\`: ${output}`,
);
}
util.cacheCodeQlVersion(cmd, result);
}
return result;
});
},
async printVersion() {
// Reuse the cached version information rather than invoking the CLI again.
@@ -731,26 +726,20 @@ async function getCodeQLForCmd(
filterToLanguagesWithQueries: boolean;
} = { filterToLanguagesWithQueries: false },
) {
const codeqlArgs = [
return runCliJson<ResolveLanguagesOutput>(cmd, [
"resolve",
"languages",
"--format=betterjson",
"--extractor-options-verbosity=4",
"--extractor-include-aliases",
// TODO: Unconditionally include `--filter-to-languages-with-queries`
// once CODEQL_MINIMUM_VERSION is at least v2.23.0
// — the first version to support this flag.
...(filterToLanguagesWithQueries
? ["--filter-to-languages-with-queries"]
: []),
...getExtraOptionsFromEnv(["resolve", "languages"]),
];
const output = await runCli(cmd, codeqlArgs);
try {
return JSON.parse(output) as ResolveLanguagesOutput;
} catch (e) {
throw new Error(
`Unexpected output from codeql resolve languages with --format=betterjson: ${e}`,
);
}
]);
},
async resolveBuildEnvironment(
workingDir: string | undefined,
@@ -766,15 +755,7 @@ async function getCodeQLForCmd(
if (workingDir !== undefined) {
codeqlArgs.push("--working-dir", workingDir);
}
const output = await runCli(cmd, codeqlArgs);
try {
return JSON.parse(output) as ResolveBuildEnvironmentOutput;
} catch (e) {
throw new Error(
`Unexpected output from codeql resolve build-environment: ${e} in\n${output}`,
);
}
return await runCliJson<ResolveBuildEnvironmentOutput>(cmd, codeqlArgs);
},
async databaseRunQueries(
databasePath: string,
@@ -976,15 +957,9 @@ async function getCodeQLForCmd(
...getExtraOptionsFromEnv(["resolve", "queries"]),
...queries,
];
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
try {
return JSON.parse(output) as string[];
} catch (e) {
throw new Error(
`Unexpected output from codeql resolve queries --format=startingpacks: ${e}`,
);
}
return await runCliJson<string[]>(cmd, codeqlArgs, {
noStreamStdout: true,
});
},
async resolveDatabase(
databasePath: string,
@@ -996,15 +971,9 @@ async function getCodeQLForCmd(
"--format=json",
...getExtraOptionsFromEnv(["resolve", "database"]),
];
const output = await runCli(cmd, codeqlArgs, { noStreamStdout: true });
try {
return JSON.parse(output) as ResolveDatabaseOutput;
} catch (e) {
throw new Error(
`Unexpected output from codeql resolve database --format=json: ${e}`,
);
}
return await runCliJson<ResolveDatabaseOutput>(cmd, codeqlArgs, {
noStreamStdout: true,
});
},
async mergeResults(
sarifFiles: string[],
@@ -1160,6 +1129,30 @@ async function runCli(
}
}
/**
* Wraps the command executor {@link runCli} and tries to parse the output as JSON.
* @param cmd The command to run.
* @param args The arguments to pass to the command.
* @param opts The options for running the command.
* @param opts.stdin Optional string to pass to the command's standard input.
* @param opts.noStreamStdout Optional boolean to indicate whether to stream the command's standard output.
* @returns The parsed JSON output from the command.
*/
async function runCliJson<T>(
cmd: string,
args: string[] = [],
opts: { stdin?: string; noStreamStdout?: boolean } = {},
): Promise<T> {
const output = await runCli(cmd, args, opts);
try {
return JSON.parse(output) as T;
} catch (e) {
throw Error(
`Unexpected output from codeql ${args.join(" ")}: ${getErrorMessage(e)}`,
);
}
}
/**
* Writes the code scanning configuration that is to be used by the CLI.
*

View File

@@ -36,6 +36,7 @@ import {
mockCodeQLVersion,
createTestConfig,
makeMacro,
initAllState,
} from "./testing-utils";
import {
GitHubVariant,
@@ -160,8 +161,9 @@ test.serial("load empty config", async (t) => {
},
});
const state = initAllState({ logger });
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput: languages,
repository: { owner: "github", repo: "example" },
@@ -202,8 +204,9 @@ test.serial("load code quality config", async (t) => {
},
});
const state = initAllState({ logger });
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
analysisKinds: [AnalysisKind.CodeQuality],
languagesInput: languages,
@@ -280,9 +283,10 @@ test.serial(
repositoryProperties,
});
const state = initAllState({ logger });
await t.notThrowsAsync(async () => {
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
analysisKinds: [AnalysisKind.CodeQuality],
languagesInput: languages,
@@ -321,8 +325,9 @@ test.serial("loading a saved config produces the same config", async (t) => {
// Sanity check that getConfig returns undefined before we have called initConfig
t.deepEqual(await configUtils.getConfig(tempDir, logger), undefined);
const state = initAllState({ logger });
const config1 = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput: "javascript,python",
tempDir,
@@ -373,8 +378,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
.stub(actionsUtil, "getActionVersion")
.returns("does-not-exist");
const state = initAllState({ logger });
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput: "javascript,python",
tempDir,
@@ -402,8 +408,9 @@ test.serial("loading config with version mismatch throws", async (t) => {
test.serial("load input outside of workspace", async (t) => {
return await withTmpDir(async (tempDir) => {
try {
const state = initAllState();
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
configFile: "../input",
tempDir,
@@ -424,34 +431,6 @@ test.serial("load input outside of workspace", async (t) => {
});
});
test.serial("load non-local input with invalid repo syntax", async (t) => {
return await withTmpDir(async (tempDir) => {
// no filename given, just a repo
const configFile = "octo-org/codeql-config@main";
try {
await configUtils.initConfig(
createFeatures([]),
createTestInitConfigInputs({
configFile,
tempDir,
workspacePath: tempDir,
}),
);
throw new Error("initConfig did not throw error");
} catch (err) {
t.deepEqual(
err,
new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(
"octo-org/codeql-config@main",
),
),
);
}
});
});
test.serial("load non-existent input", async (t) => {
return await withTmpDir(async (tempDir) => {
const languagesInput = "javascript";
@@ -459,8 +438,9 @@ test.serial("load non-existent input", async (t) => {
t.false(fs.existsSync(path.join(tempDir, configFile)));
try {
const state = initAllState();
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
configFile,
@@ -536,8 +516,9 @@ test.serial("load non-empty input", async (t) => {
const languagesInput = "javascript";
const configFilePath = createConfigFile(inputFileContents, tempDir);
const state = initAllState();
const actualConfig = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
buildModeInput: "none",
@@ -595,8 +576,9 @@ test.serial(
// Only JS, python packs will be ignored
const languagesInput = "javascript";
const state = initAllState();
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
configFile: configFilePath,
@@ -647,8 +629,9 @@ test.serial("API client used when reading remote config", async (t) => {
const configFile = "octo-org/codeql-config/config.yaml@main";
const languagesInput = "javascript";
const state = initAllState();
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
configFile,
@@ -669,9 +652,10 @@ test.serial(
mockGetContents(dummyResponse);
const repoReference = "octo-org/codeql-config/config.yaml@main";
const state = initAllState();
try {
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
configFile: repoReference,
tempDir,
@@ -699,9 +683,10 @@ test.serial("Invalid format of remote config handled correctly", async (t) => {
mockGetContents(dummyResponse);
const repoReference = "octo-org/codeql-config/config.yaml@main";
const state = initAllState();
try {
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
configFile: repoReference,
tempDir,
@@ -729,9 +714,10 @@ test.serial("No detected languages", async (t) => {
},
});
const state = initAllState();
try {
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
tempDir,
codeql,
@@ -752,9 +738,10 @@ test.serial("Unknown languages", async (t) => {
return await withTmpDir(async (tempDir) => {
const languagesInput = "rubbish,english";
const state = initAllState();
try {
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
tempDir,

View File

@@ -5,6 +5,7 @@ import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as yaml from "js-yaml";
import { ActionState } from "./action-common";
import {
getActionVersion,
getOptionalInput,
@@ -18,8 +19,9 @@ import {
getAnalysisConfig,
} from "./analyses";
import * as api from "./api-client";
import { CachingKind, getCachingKind } from "./caching-utils";
import { getCachingKind } from "./caching-utils";
import { type CodeQL } from "./codeql";
import { type Config } from "./config/action-config";
import {
calculateAugmentation,
ExcludeQueryFilter,
@@ -27,6 +29,12 @@ import {
parseUserConfig,
UserConfig,
} from "./config/db-config";
import { getRemoteConfig } from "./config/file";
import {
parseRegistries,
type RegistryConfigNoCredentials,
type RegistryConfigWithCredentials,
} from "./config/pack-registries";
import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
@@ -79,6 +87,8 @@ import {
isHostedRunner,
} from "./util";
export { type Config } from "./config/action-config";
/**
* The minimum available disk space (in MB) required to perform overlay analysis.
* If the available disk space on the runner is below the threshold when deciding
@@ -117,148 +127,6 @@ const OVERLAY_MINIMUM_MEMORY_MB = 5 * 1024;
*/
const CODEQL_VERSION_REDUCED_OVERLAY_MEMORY_USAGE = "2.24.3";
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
// Token to use when downloading packs from this registry.
token: string;
};
/**
* The list of registries and the associated pack globs that determine where each
* pack can be downloaded from.
*/
export interface RegistryConfigNoCredentials {
// URL of a package registry, eg- https://ghcr.io/v2/
url: string;
// List of globs that determine which packs are associated with this registry.
packages: string[] | string;
// Kind of registry, either "github" or "docker". Default is "docker".
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
// where GHCR is not available, such as certain GHES environments.
kind?: "github" | "docker";
}
/**
* Format of the parsed config file.
*/
export interface Config {
/**
* The version of the CodeQL Action that the configuration is for.
*/
version: string;
/**
* Set of analysis kinds that are enabled.
*/
analysisKinds: AnalysisKind[];
/**
* Set of languages to run analysis for.
*/
languages: Language[];
/**
* Build mode, if set. Currently only a single build mode is supported per job.
*/
buildMode: BuildMode | undefined;
/**
* A unaltered copy of the original user input.
* Mainly intended to be used for status reporting.
* If any field is useful for the actual processing
* of the action then consider pulling it out to a
* top-level field above.
*/
originalUserInput: UserConfig;
/**
* Directory to use for temporary files that should be
* deleted at the end of the job.
*/
tempDir: string;
/**
* Path of the CodeQL executable.
*/
codeQLCmd: string;
/**
* Version of GitHub we are talking to.
*/
gitHubVersion: GitHubVersion;
/**
* The location where CodeQL databases should be stored.
*/
dbLocation: string;
/**
* Specifies whether we are debugging mode and should try to produce extra
* output for debugging purposes when possible.
*/
debugMode: boolean;
/**
* Specifies the name of the debugging artifact if we are in debug mode.
*/
debugArtifactName: string;
/**
* Specifies the name of the database in the debugging artifact.
*/
debugDatabaseName: string;
/**
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
* as well as adjustments made to it based on unsupported or required options.
*/
computedConfig: UserConfig;
/**
* Partial map from languages to locations of TRAP caches for that language.
* If a key is omitted, then TRAP caching should not be used for that language.
*/
trapCaches: { [language: Language]: string };
/**
* Time taken to download TRAP caches. Used for status reporting.
*/
trapCacheDownloadTime: number;
/** A value indicating how dependency caching should be used. */
dependencyCachingEnabled: CachingKind;
/** The keys of caches that we restored, if any. */
dependencyCachingRestoredKeys: string[];
/**
* Extra query exclusions to append to the config.
*/
extraQueryExclusions: ExcludeQueryFilter[];
/**
* The overlay database mode to use.
*/
overlayDatabaseMode: OverlayDatabaseMode;
/**
* Whether to use caching for overlay databases. If it is true, the action
* will upload the created overlay-base database to the actions cache, and
* download an overlay-base database from the actions cache before it creates
* a new overlay database. If it is false, the action assumes that the
* workflow will be responsible for managing database storage and retrieval.
*
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
* `OverlayBase`.
*/
useOverlayDatabaseCaching: boolean;
/**
* Whether the overlay database mode was set explicitly.
*/
overlayModeSetExplicitly: boolean;
/**
* A partial mapping from repository properties that affect us to their values.
*/
repositoryProperties: RepositoryProperties;
/**
* Whether to enable file coverage information.
*/
enableFileCoverageInformation: boolean;
}
async function getSupportedLanguageMap(
codeql: CodeQL,
logger: Logger,
@@ -599,12 +467,11 @@ async function downloadCacheWithTime(
}
async function loadUserConfig(
logger: Logger,
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
configFile: string,
workspacePath: string,
apiDetails: api.GitHubApiCombinedDetails,
tempDir: string,
validateConfig: boolean,
): Promise<UserConfig> {
if (isLocal(configFile)) {
if (configFile !== userConfigFromActionPath(tempDir)) {
@@ -617,14 +484,12 @@ async function loadUserConfig(
);
}
}
return getLocalConfig(logger, configFile, validateConfig);
} else {
return await getRemoteConfig(
logger,
configFile,
apiDetails,
validateConfig,
const validateConfig = await actionState.features.getValue(
Feature.ValidateDbConfig,
);
return getLocalConfig(actionState.logger, configFile, validateConfig);
} else {
return await getRemoteConfig(actionState, configFile, apiDetails);
}
}
@@ -1138,10 +1003,11 @@ export async function applyIncrementalAnalysisSettings(
* a default config. The parsed config is then stored to a known location.
*/
export async function initConfig(
features: FeatureEnablement,
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
inputs: InitConfigInputs,
): Promise<Config> {
const { logger, tempDir } = inputs;
const { logger, features } = actionState;
const { tempDir } = inputs;
// if configInput is set, it takes precedence over configFile
if (inputs.configInput) {
@@ -1160,14 +1026,12 @@ export async function initConfig(
logger.debug("No configuration file was provided");
} else {
logger.debug(`Using configuration file: ${inputs.configFile}`);
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
userConfig = await loadUserConfig(
logger,
actionState,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
validateConfig,
);
}
@@ -1317,29 +1181,6 @@ export async function initConfig(
return config;
}
function parseRegistries(
registriesInput: string | undefined,
): RegistryConfigWithCredentials[] | undefined {
try {
return registriesInput
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
: undefined;
} catch {
throw new ConfigurationError(
"Invalid registries input. Must be a YAML string.",
);
}
}
export function parseRegistriesWithoutCredentials(
registriesInput?: string,
): RegistryConfigNoCredentials[] | undefined {
return parseRegistries(registriesInput)?.map((r) => {
const { url, packages, kind } = r;
return { url, packages, kind };
});
}
function isLocal(configPath: string): boolean {
// If the path starts with ./, look locally
if (configPath.indexOf("./") === 0) {
@@ -1369,54 +1210,6 @@ function getLocalConfig(
);
}
async function getRemoteConfig(
logger: Logger,
configFile: string,
apiDetails: api.GitHubApiCombinedDetails,
validateConfig: boolean,
): Promise<UserConfig> {
// retrieve the various parts of the config location, and ensure they're present
const format = new RegExp(
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
);
const pieces = format.exec(configFile);
// 5 = 4 groups + the whole expression
if (pieces?.groups === undefined || pieces.length < 5) {
throw new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
);
}
const response = await api
.getApiClientWithExternalAuth(apiDetails)
.rest.repos.getContent({
owner: pieces.groups.owner,
repo: pieces.groups.repo,
path: pieces.groups.path,
ref: pieces.groups.ref,
});
let fileContents: string;
if ("content" in response.data && response.data.content !== undefined) {
fileContents = response.data.content;
} else if (Array.isArray(response.data)) {
throw new ConfigurationError(
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
);
} else {
throw new ConfigurationError(
errorMessages.getConfigFileFormatInvalidMessage(configFile),
);
}
return parseUserConfig(
logger,
configFile,
Buffer.from(fileContents, "base64").toString("binary"),
validateConfig,
);
}
/**
* Get the file path where the parsed config will be stored.
*/

128
src/config/action-config.ts Normal file
View File

@@ -0,0 +1,128 @@
import type { AnalysisKind } from "../analyses";
import type { CachingKind } from "../caching-utils";
import type { RepositoryProperties } from "../feature-flags/properties";
import type { Language } from "../languages";
import type { OverlayDatabaseMode } from "../overlay/overlay-database-mode";
import type { BuildMode, GitHubVersion } from "../util";
import type { ExcludeQueryFilter, UserConfig } from "./db-config";
/**
* Format of the CodeQL Action configuration state that is persisted
* between steps of the CodeQL Action in a CodeQL workflow.
*/
export interface Config {
/**
* The version of the CodeQL Action that the configuration is for.
*/
version: string;
/**
* Set of analysis kinds that are enabled.
*/
analysisKinds: AnalysisKind[];
/**
* Set of languages to run analysis for.
*/
languages: Language[];
/**
* Build mode, if set. Currently only a single build mode is supported per job.
*/
buildMode: BuildMode | undefined;
/**
* A unaltered copy of the original user input.
* Mainly intended to be used for status reporting.
* If any field is useful for the actual processing
* of the action then consider pulling it out to a
* top-level field above.
*/
originalUserInput: UserConfig;
/**
* Directory to use for temporary files that should be
* deleted at the end of the job.
*/
tempDir: string;
/**
* Path of the CodeQL executable.
*/
codeQLCmd: string;
/**
* Version of GitHub we are talking to.
*/
gitHubVersion: GitHubVersion;
/**
* The location where CodeQL databases should be stored.
*/
dbLocation: string;
/**
* Specifies whether we are debugging mode and should try to produce extra
* output for debugging purposes when possible.
*/
debugMode: boolean;
/**
* Specifies the name of the debugging artifact if we are in debug mode.
*/
debugArtifactName: string;
/**
* Specifies the name of the database in the debugging artifact.
*/
debugDatabaseName: string;
/**
* The configuration we computed by combining `originalUserInput` with `augmentationProperties`,
* as well as adjustments made to it based on unsupported or required options.
*/
computedConfig: UserConfig;
/**
* Partial map from languages to locations of TRAP caches for that language.
* If a key is omitted, then TRAP caching should not be used for that language.
*/
trapCaches: { [language: Language]: string };
/**
* Time taken to download TRAP caches. Used for status reporting.
*/
trapCacheDownloadTime: number;
/** A value indicating how dependency caching should be used. */
dependencyCachingEnabled: CachingKind;
/** The keys of caches that we restored, if any. */
dependencyCachingRestoredKeys: string[];
/**
* Extra query exclusions to append to the config.
*/
extraQueryExclusions: ExcludeQueryFilter[];
/**
* The overlay database mode to use.
*/
overlayDatabaseMode: OverlayDatabaseMode;
/**
* Whether to use caching for overlay databases. If it is true, the action
* will upload the created overlay-base database to the actions cache, and
* download an overlay-base database from the actions cache before it creates
* a new overlay database. If it is false, the action assumes that the
* workflow will be responsible for managing database storage and retrieval.
*
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
* `OverlayBase`.
*/
useOverlayDatabaseCaching: boolean;
/**
* Whether the overlay database mode was set explicitly.
*/
overlayModeSetExplicitly: boolean;
/**
* A partial mapping from repository properties that affect us to their values.
*/
repositoryProperties: RepositoryProperties;
/**
* Whether to enable file coverage information.
*/
enableFileCoverageInformation: boolean;
}

View File

@@ -1,22 +1,19 @@
import test from "ava";
import sinon from "sinon";
import { Feature } from "../feature-flags";
import { RepositoryPropertyName } from "../feature-flags/properties";
import {
getTestActionsEnv,
RecordingLogger,
setupTests,
} from "../testing-utils";
import { callee, setupTests } from "../testing-utils";
import { getConfigFileInput } from "./file";
setupTests(test);
test("getConfigFileInput returns undefined by default", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
const result = getConfigFileInput(logger, actionsEnv, {});
t.is(result, undefined);
await callee(getConfigFileInput)
.withArgs({})
.withFeatures([Feature.ConfigFileRepositoryProperty])
.passes(t.is, undefined);
});
const repositoryProperties = {
@@ -24,46 +21,49 @@ const repositoryProperties = {
};
test("getConfigFileInput returns input value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
const testInput = "/some/path";
sinon
.stub(actionsEnv, "getOptionalInput")
.withArgs("config-file")
.returns(testInput);
// Even though both an input and repository property are configured,
// we prefer the direct input to the Action.
const result = getConfigFileInput(logger, actionsEnv, repositoryProperties);
t.is(result, testInput);
// Check for the expected log message.
t.true(logger.hasMessage("Using configuration file input from workflow"));
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withActions((actionsEnv) => {
sinon
.stub(actionsEnv, "getOptionalInput")
.withArgs("config-file")
.returns(testInput);
})
.withArgs(repositoryProperties)
.logs(t, "Using configuration file input from workflow")
.passes(t.is, testInput);
});
test("getConfigFileInput returns repository property value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since there is no direct input, we should use the repository property.
const result = getConfigFileInput(logger, actionsEnv, repositoryProperties);
t.is(result, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
// Check for the expected log message.
t.true(
logger.hasMessage(
"Using configuration file input from repository property",
),
);
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs(repositoryProperties)
.logs(t, "Using configuration file input from repository property")
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
});
test("getConfigFileInput ignores empty repository property value", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since the repository property value is an empty/whitespace string, we should ignore it.
const result = getConfigFileInput(logger, actionsEnv, {
[RepositoryPropertyName.CONFIG_FILE]: " ",
});
t.is(result, undefined);
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
.passes(t.is, undefined);
});
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
// Since the FF is off, we should ignore the repository property value.
await callee(getConfigFileInput)
.withFeatures([])
.withArgs(repositoryProperties)
.notLogs(t, "Using configuration file input from repository property")
.logs(
t,
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
)
.passes(t.is, undefined);
});

View File

@@ -1,18 +1,27 @@
import { ActionsEnv } from "../actions-util";
import { ActionState } from "../action-common";
import * as api from "../api-client";
import * as errorMessages from "../error-messages";
import { Feature } from "../feature-flags";
import {
RepositoryProperties,
RepositoryPropertyName,
} from "../feature-flags/properties";
import { Logger } from "../logging";
import { ConfigurationError } from "../util";
import { parseUserConfig, UserConfig } from "./db-config";
import { parseRemoteFileAddress } from "./remote-file";
/**
* Gets the value that is configured for the configuration file, if any.
*/
export function getConfigFileInput(
logger: Logger,
actions: ActionsEnv,
export async function getConfigFileInput(
{
logger,
actions,
features,
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
repositoryProperties: Partial<RepositoryProperties>,
): string | undefined {
): Promise<string | undefined> {
const input = actions.getOptionalInput("config-file");
if (input !== undefined) {
@@ -24,11 +33,71 @@ export function getConfigFileInput(
repositoryProperties[RepositoryPropertyName.CONFIG_FILE];
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
logger.info(
`Using configuration file input from repository property: ${propertyValue}`,
// Only use the repository property value if the FF is enabled.
const useRepositoryProperty = await features.getValue(
Feature.ConfigFileRepositoryProperty,
);
return propertyValue;
if (useRepositoryProperty) {
logger.info(
`Using configuration file input from repository property: ${propertyValue}`,
);
return propertyValue;
} else {
logger.info(
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
);
}
}
return undefined;
}
/**
* Attempts to fetch a `UserConfig` from a remote `address`.
*
* @param actionState The current Action state.
* @param configFile The remote address of the configuration file.
* @param apiDetails Information about how to connect to the API.
*
* @returns The `UserConfig`, if it could be fetched and parsed successfully.
*/
export async function getRemoteConfig(
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
configFile: string,
apiDetails: api.GitHubApiCombinedDetails,
): Promise<UserConfig> {
const address = await parseRemoteFileAddress(actionState, configFile);
const response = await api
.getApiClientWithExternalAuth(apiDetails)
.rest.repos.getContent({
owner: address.owner,
repo: address.repo,
path: address.path,
ref: address.ref,
});
let fileContents: string;
if ("content" in response.data && response.data.content !== undefined) {
fileContents = response.data.content;
} else if (Array.isArray(response.data)) {
throw new ConfigurationError(
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
);
} else {
throw new ConfigurationError(
errorMessages.getConfigFileFormatInvalidMessage(configFile),
);
}
const validateConfig = await actionState.features.getValue(
Feature.ValidateDbConfig,
);
return parseUserConfig(
actionState.logger,
configFile,
Buffer.from(fileContents, "base64").toString("binary"),
validateConfig,
);
}

View File

@@ -0,0 +1,49 @@
import * as yaml from "js-yaml";
import { ConfigurationError } from "../util";
export type RegistryConfigWithCredentials = RegistryConfigNoCredentials & {
// Token to use when downloading packs from this registry.
token: string;
};
/**
* The list of registries and the associated pack globs that determine where each
* pack can be downloaded from.
*/
export interface RegistryConfigNoCredentials {
// URL of a package registry, eg- https://ghcr.io/v2/
url: string;
// List of globs that determine which packs are associated with this registry.
packages: string[] | string;
// Kind of registry, either "github" or "docker". Default is "docker".
// "docker" refers specifically to the GitHub Container Registry, which is the usual way of sharing CodeQL packs.
// "github" refers to packs published as content in a GitHub repository. This kind of registry is used in scenarios
// where GHCR is not available, such as certain GHES environments.
kind?: "github" | "docker";
}
export function parseRegistries(
registriesInput: string | undefined,
): RegistryConfigWithCredentials[] | undefined {
try {
return registriesInput
? (yaml.load(registriesInput) as RegistryConfigWithCredentials[])
: undefined;
} catch {
throw new ConfigurationError(
"Invalid registries input. Must be a YAML string.",
);
}
}
export function parseRegistriesWithoutCredentials(
registriesInput?: string,
): RegistryConfigNoCredentials[] | undefined {
return parseRegistries(registriesInput)?.map((r) => {
const { url, packages, kind } = r;
return { url, packages, kind };
});
}

View File

@@ -0,0 +1,268 @@
import test from "ava";
import sinon from "sinon";
import { ActionsEnvVars } from "../environment";
import * as errors from "../error-messages";
import { Feature } from "../feature-flags";
import { callee } from "../testing-utils";
import { ConfigurationError } from "../util";
import {
DEFAULT_CONFIG_FILE_NAME,
DEFAULT_CONFIG_FILE_REF,
parseRemoteFileAddress,
RemoteFileAddress,
} from "./remote-file";
type ParseRemoteFileAddressTest = {
input: string;
expected: RemoteFileAddress;
};
test("parseRemoteFileAddress accepts full remote addresses", async (t) => {
const target = callee(parseRemoteFileAddress);
const expected: RemoteFileAddress = {
owner: "owner",
repo: "repo",
path: "path",
ref: "ref",
};
const oldFormatInputs: ParseRemoteFileAddressTest[] = [
{ input: "owner/repo/path@ref", expected },
{ input: "owner /repo/path@ref", expected },
{ input: "owner/ repo/path@ref", expected },
{ input: "owner/repo /path@ref", expected },
{ input: "owner/repo/ path@ref", expected },
{ input: "owner/repo/path @ref", expected },
{ input: "owner/repo/path@ ref", expected },
{
input: "owner/repo/path/to/codeql.yml@ref/feature",
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
},
{
input: " owner/repo/path/to/codeql.yml@ref/feature ",
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
},
];
for (const oldFormatInput of oldFormatInputs) {
await target
.withArgs(oldFormatInput.input)
.passes(t.deepEqual, oldFormatInput.expected);
}
// New format.
const newFormatInputs: ParseRemoteFileAddressTest[] = [
{ input: "owner/repo@ref:path", expected },
{ input: "owner /repo@ref:path", expected },
{ input: "owner/ repo@ref:path", expected },
{ input: "owner/repo @ref:path", expected },
{ input: "owner/repo@ ref:path", expected },
{ input: "owner/repo@ref :path", expected },
{ input: "owner/repo@ref: path", expected },
{
input: "owner/repo@ref/feature:path/to/codeql.yml",
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
},
{
input: " owner/repo@ref/feature:path/to/codeql.yml ",
expected: { ...expected, path: "path/to/codeql.yml", ref: "ref/feature" },
},
];
for (const newFormatInput of newFormatInputs) {
const targetWithArgs = target.withArgs(newFormatInput.input);
// Should fail when the FF is not enabled.
await targetWithArgs
.withFeatures([])
.throws(t, { instanceOf: ConfigurationError });
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(t.deepEqual, newFormatInput.expected);
}
});
test("parseRemoteFileAddress accepts remote address without an owner", async (t) => {
const owner = "test-owner";
const target = callee(parseRemoteFileAddress).withEnv((env) => {
const getRequired = sinon.stub(env, "getRequired");
getRequired
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
.returns(`${owner}/current-repo`);
});
const testCases: ParseRemoteFileAddressTest[] = [
{
input: "repo@ref:path.yml",
expected: {
owner,
repo: "repo",
path: "path.yml",
ref: "ref",
},
},
{
input: "repo@ref",
expected: {
owner,
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: "ref",
},
},
{
input: "repo:path.yml",
expected: {
owner,
repo: "repo",
path: "path.yml",
ref: DEFAULT_CONFIG_FILE_REF,
},
},
{
input: "repo",
expected: {
owner,
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: DEFAULT_CONFIG_FILE_REF,
},
},
];
for (const testCase of testCases) {
const targetWithArgs = target.withArgs(testCase.input);
// Should fail when the FF is not enabled.
await targetWithArgs
.withFeatures([])
.throws(t, { instanceOf: ConfigurationError });
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(t.deepEqual, testCase.expected);
}
});
test("parseRemoteFileAddress throws for invalid `GITHUB_REPOSITORY`", async (t) => {
const getRequired: sinon.SinonStub = sinon.stub();
getRequired.withArgs(ActionsEnvVars.GITHUB_REPOSITORY).returns(`not-valid`);
const target = callee(parseRemoteFileAddress)
.withArgs("repo@ref")
.withEnv((env) => {
sinon.define(env, "getRequired", getRequired);
});
await target
.withFeatures([Feature.NewRemoteFileAddresses])
.throws(t, { instanceOf: Error });
t.assert(getRequired.calledOnceWith(ActionsEnvVars.GITHUB_REPOSITORY));
});
test("parseRemoteFileAddress accepts remote address without a path", async (t) => {
const target = callee(parseRemoteFileAddress);
const testCases: ParseRemoteFileAddressTest[] = [
{
input: "owner/repo@ref",
expected: {
owner: "owner",
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: "ref",
},
},
{
input: "owner/repo",
expected: {
owner: "owner",
repo: "repo",
path: DEFAULT_CONFIG_FILE_NAME,
ref: DEFAULT_CONFIG_FILE_REF,
},
},
];
for (const testCase of testCases) {
const targetWithArgs = target.withArgs(testCase.input);
// Should fail when the FF is not enabled.
await targetWithArgs
.withFeatures([])
.throws(t, { instanceOf: ConfigurationError });
// And pass when the FF is enabled.
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(t.deepEqual, testCase.expected);
}
});
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
// Should only accept the input if the FF is enabled.
await target.withFeatures([]).throws(t);
await target
.withFeatures([Feature.NewRemoteFileAddresses])
.passes(t.deepEqual, {
owner: "owner",
repo: "repo",
path: "path",
ref: DEFAULT_CONFIG_FILE_REF,
} satisfies RemoteFileAddress);
});
test("parseRemoteFileAddress rejects invalid values", async (t) => {
const owner = "owner";
const target = callee(parseRemoteFileAddress).withEnv((env) => {
const getRequired = sinon.stub(env, "getRequired");
getRequired
.withArgs(ActionsEnvVars.GITHUB_REPOSITORY)
.returns(`${owner}/current-repo`);
});
const testInputs = [
" ",
"repo//absolute",
"repo:/absolute",
"/repo@ref",
" /repo@ref",
"repo@",
"repo:",
"repo/",
"/repo",
":path",
"@ref",
"@ref:path",
"owner/@ref:path",
"owner/@ref",
"owner/:path",
];
for (const testInput of testInputs) {
const targetWithArgs = target.withArgs(testInput);
// Should throw both when the new format is and isn't accepted.
await targetWithArgs.withFeatures([]).throws(t, {
instanceOf: ConfigurationError,
message: errors.getConfigFileRepoOldFormatInvalidMessage(testInput),
});
await targetWithArgs
.withFeatures([Feature.NewRemoteFileAddresses])
.throws(t, {
// When the new format is accepted, there are some more specific
// errors in some cases. It is sufficient for us to check that
// an exception is thrown.
instanceOf: ConfigurationError,
});
}
});

138
src/config/remote-file.ts Normal file
View File

@@ -0,0 +1,138 @@
import { ActionState } from "../action-common";
import { Env, ActionsEnvVars } from "../environment";
import * as errorMessages from "../error-messages";
import { Feature } from "../feature-flags";
import { ConfigurationError, Failure, Result, Success } from "../util";
/** Represents remote file addresses. */
export interface RemoteFileAddress {
/** The owner of the repository. */
owner: string;
/** The repository name. */
repo: string;
/** The path of the file. */
path: string;
/** The ref of the repository. */
ref: string;
}
/** The default file path to use in configuration file shorthands. */
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
/** The default ref to use in configuration file shorthands. */
export const DEFAULT_CONFIG_FILE_REF = "main";
/** Extracts the owner from the `GITHUB_REPOSITORY` environment variable. */
function getDefaultOwner(env: Env): string {
const currentRepoNwo = env.getRequired(ActionsEnvVars.GITHUB_REPOSITORY);
const nwoParts = currentRepoNwo.split("/");
if (nwoParts.length !== 2 || nwoParts[0].trim().length === 0) {
// This shouldn't happen, so we should throw if `GITHUB_REPOSITORY` doesn't match
// our expectations.
throw new Error(
`Expected ${ActionsEnvVars.GITHUB_REPOSITORY} to contain a name with owner, but got '${currentRepoNwo}'.`,
);
}
return nwoParts[0].trim();
}
/**
* The old remote address format that's always been supported for the `config-file` input.
* All the components are required. Unchanged from the previous implementation.
*/
const OLD_REMOTE_ADDRESS_FORMAT = new RegExp(
"(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)",
);
/**
* Attempts to parse `input` as a `RemoteFileAddress` using the old format.
*
* @param input The input to try and parse.
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
*/
function parseOldRemoteFileAddress(
input: string,
): Result<RemoteFileAddress, undefined> {
const pieces = OLD_REMOTE_ADDRESS_FORMAT.exec(input);
// 5 = 4 groups + the whole expression
if (pieces?.groups === undefined || pieces.length < 5) {
return new Failure(undefined);
}
return new Success({
owner: pieces.groups.owner.trim(),
repo: pieces.groups.repo.trim(),
path: pieces.groups.path.trim(),
ref: pieces.groups.ref.trim(),
});
}
/**
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
*
* @param actionState The current Action state.
* @param configFile The string to try and parse.
* @returns The successful result of executing the regex.
* @throws `ConfigurationError` if the format of `configFile` is not valid.
*/
export async function parseRemoteFileAddress(
actionState: ActionState<["FeatureFlags", "Env"]>,
configFile: string,
): Promise<RemoteFileAddress> {
// Try to parse the input using the old format. If successful, return the
// resulting `RemoteFileAddress`. Otherwise, continue using the new format.
const oldFormatAddressResult = parseOldRemoteFileAddress(configFile);
if (oldFormatAddressResult.isSuccess()) {
return oldFormatAddressResult.value;
}
// If the FF for the new format is not enabled, throw the old format error.
const allowNewFormat = await actionState.features.getValue(
Feature.NewRemoteFileAddresses,
);
if (!allowNewFormat) {
throw new ConfigurationError(
errorMessages.getConfigFileRepoOldFormatInvalidMessage(configFile),
);
}
// retrieve the various parts of the config location, and ensure they're present
const format = new RegExp(
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
);
const pieces = format.exec(configFile.trim());
const repo: string | undefined = pieces?.groups?.repo?.trim();
// Check that the regular expression matched and that we have at least the repo name.
if (!pieces?.groups || !repo || repo.length === 0) {
// Neither the old format nor the new format worked. Throw an error that
// explains the format we accept. We only mention the new format, since that's
// what we want to be used going forward.
throw new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
);
}
const owner: string | undefined = pieces.groups.owner?.trim();
const path: string | undefined = pieces.groups.path?.trim();
const ref: string | undefined = pieces.groups.ref?.trim();
// Ensure that the path is a relative path.
if (path?.startsWith("/")) {
throw new ConfigurationError(
`The path component of '${configFile}' cannot be an absolute path.`,
);
}
return {
owner: owner || getDefaultOwner(actionState.env),
repo,
path: path || DEFAULT_CONFIG_FILE_NAME,
ref: ref || DEFAULT_CONFIG_FILE_REF,
};
}

View File

@@ -11,8 +11,10 @@ import * as apiClient from "./api-client";
import { createStubCodeQL } from "./codeql";
import { Config } from "./config-utils";
import { cleanupAndUploadDatabases } from "./database-upload";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { BuiltInLanguage } from "./languages";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { RepositoryNwo } from "./repository";
import {
checkExpectedLogMessages,
@@ -24,6 +26,7 @@ import {
setupTests,
} from "./testing-utils";
import {
CleanupLevel,
GitHubVariant,
HTTPError,
initializeEnvironment,
@@ -335,3 +338,191 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
);
});
});
test.serial(
"Records overlay and clear cleanup sizes when uploading an overlay-base database",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
// Track the cleanup level passed to each cleanup so that the database
// bundle stub can write a differently-sized bundle for each level.
const cleanupLevels: CleanupLevel[] = [];
let lastCleanupLevel: CleanupLevel | undefined;
const overlaySizeBytes = 100;
const clearSizeBytes = 50;
const codeql = createStubCodeQL({
async databaseCleanupCluster(_config, cleanupLevel) {
cleanupLevels.push(cleanupLevel);
lastCleanupLevel = cleanupLevel;
},
async databaseBundle(_databasePath, outputFilePath) {
const sizeBytes =
lastCleanupLevel === CleanupLevel.Overlay
? overlaySizeBytes
: clearSizeBytes;
fs.writeFileSync(outputFilePath, "x".repeat(sizeBytes));
},
});
const config = getTestConfig(tmpDir);
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
const loggedMessages: LoggedMessage[] = [];
const results = await cleanupAndUploadDatabases(
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger(loggedMessages),
);
// The database should be cleaned up at the `overlay` level for the upload
// and then re-cleaned at the `clear` level to measure its size.
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay, CleanupLevel.Clear]);
t.is(results.length, 1);
t.is(results[0].is_overlay_base, true);
t.is(results[0].zipped_upload_size_bytes, overlaySizeBytes);
t.is(results[0].clear_cleanup_zipped_size_bytes, clearSizeBytes);
t.is(typeof results[0].clear_cleanup_measurement_duration_ms, "number");
});
},
);
test.serial(
"Does not measure clear cleanup size for a regular (non-overlay-base) upload",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const cleanupLevels: CleanupLevel[] = [];
const codeql = createStubCodeQL({
async databaseCleanupCluster(_config, cleanupLevel) {
cleanupLevels.push(cleanupLevel);
},
async databaseBundle(_databasePath, outputFilePath) {
fs.writeFileSync(outputFilePath, "");
},
});
const results = await cleanupAndUploadDatabases(
testRepoName,
codeql,
getTestConfig(tmpDir),
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
);
// A regular upload is cleaned only once, at the `clear` level.
t.deepEqual(cleanupLevels, [CleanupLevel.Clear]);
t.is(results[0].is_overlay_base, false);
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
});
},
);
test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const cleanupLevels: CleanupLevel[] = [];
const codeql = createStubCodeQL({
async databaseCleanupCluster(_config, cleanupLevel) {
cleanupLevels.push(cleanupLevel);
},
async databaseBundle(_databasePath, outputFilePath) {
fs.writeFileSync(outputFilePath, "");
},
});
const config = getTestConfig(tmpDir);
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
config.debugMode = true;
const results = await cleanupAndUploadDatabases(
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
);
// In debug mode we clean up at the `overlay` level for the upload but skip
// the additional `clear` cleanup, to preserve the database for debugging.
t.deepEqual(cleanupLevels, [CleanupLevel.Overlay]);
t.is(results[0].is_overlay_base, true);
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
});
});
test.serial(
"Does not record a clear cleanup duration when the clear cleanup fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const codeql = createStubCodeQL({
async databaseCleanupCluster(_config, cleanupLevel) {
if (cleanupLevel === CleanupLevel.Clear) {
throw new Error("clear cleanup failed");
}
},
async databaseBundle(_databasePath, outputFilePath) {
fs.writeFileSync(outputFilePath, "x".repeat(100));
},
});
const config = getTestConfig(tmpDir);
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
const results = await cleanupAndUploadDatabases(
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
);
// When the `clear` cleanup fails, no size is measured, so we should not
// report a measurement duration either.
t.is(results[0].is_overlay_base, true);
t.is(results[0].clear_cleanup_zipped_size_bytes, undefined);
t.is(results[0].clear_cleanup_measurement_duration_ms, undefined);
});
},
);

View File

@@ -25,6 +25,19 @@ export interface DatabaseUploadResult {
zipped_upload_size_bytes?: number;
/** Whether the uploaded database is an overlay base. */
is_overlay_base?: boolean;
/**
* For overlay-base uploads only: the size in bytes that the zipped database
* would have been if it had been cleaned at the `clear` cleanup level instead
* of the `overlay` level.
*/
clear_cleanup_zipped_size_bytes?: number;
/**
* For overlay-base uploads only: the time in milliseconds spent measuring the
* `clear` cleanup size (cleaning up the cluster at the `clear` level and
* bundling each database). This is a cluster-wide measurement, so it is the
* same for every language in a run.
*/
clear_cleanup_measurement_duration_ms?: number;
/** Time taken to upload database in milliseconds. */
upload_duration_ms?: number;
/** If there was an error during database upload, this is its message. */
@@ -156,9 +169,89 @@ export async function cleanupAndUploadDatabases(
});
}
}
// When we upload an overlay-base database, we cleaned the databases at the `overlay` level, which
// retains more data than the `clear` level used for regular uploads. Measure what the zipped size
// would have been at the `clear` level too, so we can compare the storage cost of overlay-base
// databases against regular databases for the same repository.
//
// We skip this in debug mode, where the databases are preserved and uploaded as debug artifacts,
// since cleaning them up at the `clear` level would discard data that is useful for debugging.
if (shouldUploadOverlayBase && !config.debugMode) {
await withGroupAsync(
"Measuring database size at the clear cleanup level",
() => recordClearCleanupSizes(codeql, config, reports, logger),
);
}
return reports;
}
/**
* Cleans up the databases at the `clear` cleanup level and records the resulting zipped size for
* each language in `clear_cleanup_zipped_size_bytes`. If the cleanup succeeds, also records the
* time spent taking the measurement in `clear_cleanup_measurement_duration_ms`.
*
* This mutates the entries of `reports` in place. It must run only after all overlay-base uploads
* have completed, since the `clear` cleanup discards overlay data that the uploaded database
* depends on.
*
* Failures here are non-fatal: this is telemetry-only, so we log and move on rather than failing
* the workflow.
*/
async function recordClearCleanupSizes(
codeql: CodeQL,
config: Config,
reports: DatabaseUploadResult[],
logger: Logger,
): Promise<void> {
// Include both the cleanup and the re-bundling to record how much time taking this measurement adds
// to the run.
const startTime = performance.now();
try {
await codeql.databaseCleanupCluster(config, CleanupLevel.Clear);
} catch (e) {
// The cleanup didn't run, so there are no sizes to measure. Return without recording a
// duration, so that we don't report a measurement duration with no accompanying sizes.
logger.warning(
`Failed to clean up databases at the '${CleanupLevel.Clear}' level for ` +
`size measurement: ${util.getErrorMessage(e)}`,
);
return;
}
for (const language of config.languages) {
const report = reports.find((r) => r.language === language);
if (report === undefined) {
continue;
}
try {
const bundledDb = await bundleDb(config, language, codeql, language, {
includeDiagnostics: false,
});
report.clear_cleanup_zipped_size_bytes = fs.statSync(bundledDb).size;
logger.debug(
`Database for ${language} is ` +
`${report.clear_cleanup_zipped_size_bytes} bytes zipped at the ` +
`'${CleanupLevel.Clear}' cleanup level ` +
`(vs. ${report.zipped_upload_size_bytes ?? "unknown"} bytes at the ` +
`'${CleanupLevel.Overlay}' level).`,
);
} catch (e) {
logger.warning(
`Failed to measure the '${CleanupLevel.Clear}' cleanup database size ` +
`for ${language}: ${util.getErrorMessage(e)}`,
);
}
}
const durationMs = performance.now() - startTime;
for (const report of reports) {
report.clear_cleanup_measurement_duration_ms = durationMs;
}
}
/**
* Uploads a bundled database to the GitHub API.
*

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.25.6",
"cliVersion": "2.25.6",
"priorBundleVersion": "codeql-bundle-v2.25.5",
"priorCliVersion": "2.25.5"
"bundleVersion": "codeql-bundle-v2.26.0",
"cliVersion": "2.26.0",
"priorBundleVersion": "codeql-bundle-v2.25.6",
"priorCliVersion": "2.25.6"
}

View File

@@ -18,10 +18,16 @@ export enum EnvVar {
CLI_VERBOSITY = "CODEQL_VERBOSITY",
/**
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
* invoking `codeql version` again.
* Set by Default Setup to the base branch of the PR being analysed, if analysing a PR.
* This is needed because the `pull_request` context is not available for `dynamic` events.
*/
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
CODE_SCANNING_BASE_BRANCH = "CODE_SCANNING_BASE_BRANCH",
/**
* Set by Default Setup to the full ref being analysed, if analysing a PR.
* This is needed because the `pull_request` context is not available for `dynamic` events.
*/
CODE_SCANNING_REF = "CODE_SCANNING_REF",
/** Whether the CodeQL Action has invoked the Go autobuilder. */
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
@@ -83,6 +89,9 @@ export enum EnvVar {
/** Whether to suppress the warning if the current CLI will soon be unsupported. */
SUPPRESS_DEPRECATED_SOON_WARNING = "CODEQL_ACTION_SUPPRESS_DEPRECATED_SOON_WARNING",
/** Used to dictate or persist the temporary directory used by the CodeQL Action. */
TEMP = "CODEQL_ACTION_TEMP",
/** Whether to disable uploading SARIF results or status reports to the GitHub API */
TEST_MODE = "CODEQL_ACTION_TEST_MODE",
@@ -160,3 +169,125 @@ export enum EnvVar {
/** Used by Code Scanning Risk Assessment to communicate the assessment ID to the CodeQL Action. */
RISK_ASSESSMENT_ID = "CODEQL_ACTION_RISK_ASSESSMENT_ID",
}
/**
* Enumerates known GitHub Actions environment variables that we expect
* to be set in a GitHub Actions environment.
*/
export enum ActionsEnvVars {
GITHUB_ACTION_REPOSITORY = "GITHUB_ACTION_REPOSITORY",
GITHUB_API_URL = "GITHUB_API_URL",
GITHUB_EVENT_NAME = "GITHUB_EVENT_NAME",
GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH",
GITHUB_JOB = "GITHUB_JOB",
GITHUB_REF = "GITHUB_REF",
GITHUB_REPOSITORY = "GITHUB_REPOSITORY",
GITHUB_RUN_ATTEMPT = "GITHUB_RUN_ATTEMPT",
GITHUB_RUN_ID = "GITHUB_RUN_ID",
GITHUB_SERVER_URL = "GITHUB_SERVER_URL",
GITHUB_SHA = "GITHUB_SHA",
GITHUB_WORKFLOW = "GITHUB_WORKFLOW",
GITHUB_WORKSPACE = "GITHUB_WORKSPACE",
RUNNER_ENVIRONMENT = "RUNNER_ENVIRONMENT",
RUNNER_NAME = "RUNNER_NAME",
RUNNER_OS = "RUNNER_OS",
RUNNER_TEMP = "RUNNER_TEMP",
RUNNER_TOOL_CACHE = "RUNNER_TOOL_CACHE",
}
/** A type representing all known environment variables. */
export type KnownEnvVar = EnvVar | ActionsEnvVars;
/**
* Gets an environment variable, but throws an error if it is not set.
*/
function getRequiredEnvVar(env: NodeJS.ProcessEnv, paramName: string): string {
const value = env[paramName];
if (value === undefined || value.length === 0) {
throw new Error(`${paramName} environment variable must be set`);
}
return value;
}
/**
* Get an environment parameter, but throw an error if it is not set.
*
* @deprecated Use `getRequired` of a `ReadOnlyEnv` or `Env` instance instead.
*/
export function getRequiredEnvParam(paramName: string): string {
return getRequiredEnvVar(process.env, paramName);
}
/**
* Gets an environment variable, but returns `undefined` if it is not set or empty.
*/
function getOptionalEnvVarFrom(
env: NodeJS.ProcessEnv,
paramName: string,
): string | undefined {
const value = env[paramName];
if (value?.trim().length === 0) {
return undefined;
}
return value;
}
/**
* Get an environment variable, but return `undefined` if it is not set or empty.
*
* @deprecated Use `getOptional` of a `ReadOnlyEnv` or `Env` instance instead.
*/
export function getOptionalEnvVar(paramName: string): string | undefined {
return getOptionalEnvVarFrom(process.env, paramName);
}
/**
* An abstraction around read-only environment variables, to allow abstracting away from `process.env`
* in tests, while clearly signalling in regular code that the consumer of the `ReadOnlyEnv` instance
* will only read from it.
*/
export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
constructor(protected readonly vars: Record<string, T>) {}
/** Tries to get the value for `name` and throws if there isn't one. */
public getRequired(name: string): string {
return getRequiredEnvVar(this.vars, name);
}
/** Gets the value for `name`, or `undefined` if it isn't set or empty. */
public getOptional(name: string): string | undefined {
return getOptionalEnvVarFrom(this.vars, name);
}
/** Gets the entries of the underlying `ProcessEnv`. */
public entries(): Array<[string, T]> {
return Object.entries(this.vars);
}
}
/**
* A wrapper around an environment, to allow abstracting away from `process.env` in tests.
* Use `ReadOnlyEnv` instead if you only plan to read from the environment.
* This type allows writing to the environment.
*/
export class Env<
T extends string | undefined = string | undefined,
> extends ReadOnlyEnv<T> {
private changed: boolean = false;
/** Sets an environment variable. */
public set(name: string, value: T): void {
this.vars[name] = value;
this.changed = true;
}
/** Gets a value indicating whether `set` was called at least once. */
public hasChanged(): boolean {
return this.changed;
}
}
/** Gets an `Env` instance for `env`, which is `process.env` by default. */
export function getEnv(env: NodeJS.ProcessEnv = process.env): Env {
return new Env(env);
}

View File

@@ -30,7 +30,7 @@ export function getInvalidConfigFileMessage(
return `The configuration file "${configFile}" is invalid: ${messages.slice(0, 10).join(", ")}${andMore}`;
}
export function getConfigFileRepoFormatInvalidMessage(
export function getConfigFileRepoOldFormatInvalidMessage(
configFile: string,
): string {
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
@@ -39,6 +39,15 @@ export function getConfigFileRepoFormatInvalidMessage(
return error;
}
export function getConfigFileRepoFormatInvalidMessage(
configFile: string,
): string {
let error = `The configuration file "${configFile}" is not a supported remote file reference.`;
error += " Expected format [<owner>/]<repository>[@<ref>][:<file-path>]";
return error;
}
export function getConfigFileFormatInvalidMessage(configFile: string): string {
return `The configuration file "${configFile}" could not be read`;
}

View File

@@ -74,6 +74,8 @@ export enum Feature {
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
AllowToolcacheInput = "allow_toolcache_input",
CleanupTrapCaches = "cleanup_trap_caches",
/** Whether to allow the `config-file` input to be specified via a repository property. */
ConfigFileRepositoryProperty = "config_file_repository_property",
CppDependencyInstallation = "cpp_dependency_installation_enabled",
CsharpCacheBuildModeNone = "csharp_cache_bmn",
CsharpNewCacheKey = "csharp_new_cache_key",
@@ -90,6 +92,8 @@ export enum Feature {
ForceNightly = "force_nightly",
IgnoreGeneratedFiles = "ignore_generated_files",
JavaNetworkDebugging = "java_network_debugging",
/** Allow the new remote file address format. */
NewRemoteFileAddresses = "new_remote_file_addresses",
OverlayAnalysis = "overlay_analysis",
OverlayAnalysisCodeScanningCpp = "overlay_analysis_code_scanning_cpp",
OverlayAnalysisCodeScanningCsharp = "overlay_analysis_code_scanning_csharp",
@@ -184,6 +188,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
minimumVersion: undefined,
},
[Feature.ConfigFileRepositoryProperty]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CONFIG_FILE_REPOSITORY_PROPERTY",
minimumVersion: undefined,
},
[Feature.CppDependencyInstallation]: {
defaultValue: false,
envVar: "CODEQL_EXTRACTOR_CPP_AUTOINSTALL_DEPENDENCIES",
@@ -248,6 +257,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_JAVA_NETWORK_DEBUGGING",
minimumVersion: undefined,
},
[Feature.NewRemoteFileAddresses]: {
defaultValue: false,
envVar: "CODEQL_ACTION_NEW_REMOTE_FILE_ADDRESSES",
minimumVersion: undefined,
},
[Feature.OverlayAnalysis]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",

View File

@@ -7,9 +7,9 @@ import * as io from "@actions/io";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import {
FileCmdNotFoundError,
getActionsEnv,
getActionVersion,
getFileType,
getOptionalInput,
@@ -24,6 +24,7 @@ import {
getTotalCacheSize,
shouldRestoreCache,
} from "./caching-utils";
import { writeCommandCacheFile } from "./cli/output-cache";
import { CodeQL } from "./codeql";
import { getConfigFileInput } from "./config/file";
import * as configUtils from "./config-utils";
@@ -56,7 +57,7 @@ import {
runDatabaseInitCluster,
} from "./init";
import { JavaEnvVars, BuiltInLanguage } from "./languages";
import { getActionsLogger, Logger, withGroupAsync } from "./logging";
import { Logger, withGroupAsync } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
OverlayBaseDatabaseDownloadStats,
@@ -73,7 +74,6 @@ import {
createStatusReportBase,
getActionsStatus,
sendStatusReport,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
@@ -204,12 +204,12 @@ async function sendCompletedStatusReport(
}
}
async function run(startedAt: Date) {
async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
const actionsEnv = getActionsEnv();
const startedAt = actionState.startedAt;
const logger = actionState.logger;
let apiDetails: GitHubApiCombinedDetails;
let config: configUtils.Config | undefined;
@@ -263,7 +263,11 @@ async function run(startedAt: Date) {
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
configFile = getConfigFileInput(logger, actionsEnv, repositoryProperties);
const actionStateWithFeatures = { ...actionState, features };
configFile = await getConfigFileInput(
actionStateWithFeatures,
repositoryProperties,
);
// path.resolve() respects the intended semantics of source-root. If
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
@@ -323,6 +327,10 @@ async function run(startedAt: Date) {
toolsSource = initCodeQLResult.toolsSource;
zstdAvailability = initCodeQLResult.zstdAvailability;
// Populate the in-memory command cache with CLI version.
// This result will be persisted to disk at the end of the init action.
await codeql.getVersion();
// Check the workflow for problems. If there are any problems, they are reported
// to the workflow log. No exceptions are thrown.
await checkWorkflow(logger, codeql);
@@ -361,7 +369,7 @@ async function run(startedAt: Date) {
repositoryProperties,
);
config = await initConfig(features, {
config = await initConfig(actionStateWithFeatures, {
analysisKinds,
languagesInput: getOptionalInput("languages"),
queriesInput: getOptionalInput("queries"),
@@ -766,6 +774,9 @@ async function run(startedAt: Date) {
core.setOutput("codeql-path", config.codeQLCmd);
core.setOutput("codeql-version", (await codeql.getVersion()).version);
// Persist the command cache to disk at the end of a successful init.
writeCommandCacheFile();
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
core.setFailed(error.message);
@@ -846,19 +857,13 @@ async function recordZstdAvailability(
);
}
/** Defines the `init` Action. */
const init: Action = {
name: ActionName.Init,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`init action failed: ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.Init,
startedAt,
error,
logger,
);
}
await runInActions(init);
await checkForTimeout();
}

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

@@ -7,6 +7,7 @@ import * as github from "@actions/github";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import { ActionState } from "./action-common";
import {
getOptionalInput,
isAnalyzingPullRequest,
@@ -81,11 +82,11 @@ export async function initCodeQL(
}
export async function initConfig(
features: FeatureEnablement,
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
inputs: configUtils.InitConfigInputs,
): Promise<configUtils.Config> {
return await withGroupAsync("Load language configuration", async () => {
return await configUtils.initConfig(features, inputs);
return await configUtils.initConfig(actionState, inputs);
});
}

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

@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import {
getActionVersion,
getOptionalInput,
@@ -14,7 +15,7 @@ import { getRawLanguagesNoAutodetect } from "./config-utils";
import { EnvVar } from "./environment";
import { initFeatures } from "./feature-flags";
import { initCodeQL } from "./init";
import { getActionsLogger, Logger } from "./logging";
import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import {
@@ -24,7 +25,6 @@ import {
createStatusReportBase,
getActionsStatus,
sendStatusReport,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { ToolsDownloadStatusReport } from "./tools-download";
import {
@@ -36,7 +36,6 @@ import {
ConfigurationError,
wrapError,
checkActionVersion,
getErrorMessage,
} from "./util";
/**
@@ -88,12 +87,13 @@ async function sendCompletedStatusReport(
}
/** The main behaviour of this action. */
async function run(startedAt: Date): Promise<void> {
async function run({
startedAt,
logger,
}: ActionState<["Logger"]>): Promise<void> {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
let codeql: CodeQL;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsFeatureFlagsValid: boolean | undefined;
@@ -195,20 +195,14 @@ async function run(startedAt: Date): Promise<void> {
);
}
/** Defines the `setup-codeql` Action. */
const setupCodeQL: Action = {
name: ActionName.SetupCodeQL,
run,
};
/** Run the action and catch any unhandled errors. */
export async function runWrapper(): Promise<void> {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(`setup-codeql action failed: ${getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.SetupCodeQL,
startedAt,
error,
logger,
);
}
await runInActions(setupCodeQL);
await checkForTimeout();
}

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

@@ -12,19 +12,20 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { parseRegistriesWithoutCredentials, type Config } from "./config-utils";
import { DependencyCacheRestoreStatusReport } from "./dependency-caching";
import { CommandCacheKey, getCachedCommandOutput } from "./cli/output-cache";
import type { Config } from "./config/action-config";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
import { DocUrl } from "./doc-url";
import { EnvVar } from "./environment";
import { getRef } from "./git-utils";
import { Logger } from "./logging";
import { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
import type { Logger } from "./logging";
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
import { getRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import type { ToolsSource } from "./setup-codeql";
import {
ConfigurationError,
getRequiredEnvParam,
getCachedCodeQlVersion,
isInTestMode,
GITHUB_DOTCOM_URL,
DiskUsage,
@@ -46,6 +47,17 @@ export enum ActionName {
UploadSarif = "upload-sarif",
}
/**
* Maps an `ActionName` to its display name. Usually that is the same, except
* for `ActionName.Analyze` where it is `"analyze"` instead of `"finish"`.
*/
export function getDisplayActionName(actionName: ActionName): string {
if (actionName === ActionName.Analyze) {
return "analyze";
}
return actionName;
}
/**
* @returns a boolean indicating whether the analysis is considered to be first party.
*
@@ -283,7 +295,10 @@ export async function createStatusReportBase(
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion();
const codeQlCliVersion = getCachedCommandOutput(
CommandCacheKey.Version,
undefined,
);
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
const testingEnvironment = getTestingEnvironment();
// re-export the testing environment variable so that it is available to subsequent steps,

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,
@@ -10,14 +12,18 @@ import test, {
import nock from "nock";
import * as sinon from "sinon";
import { ActionState, StateFeature } from "./action-common";
import { ActionsEnv, getActionVersion } from "./actions-util";
import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client";
import { GitHubApiDetails } from "./api-client";
import { CachingKind } from "./caching-utils";
import type { VersionInfo } from "./cli/output-cache";
import { resetCachedCommandOutputs } from "./cli/output-cache";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { Env, ActionsEnvVars } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -26,13 +32,14 @@ import {
} from "./feature-flags";
import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { ActionName } from "./status-report";
import {
DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME,
getEnv,
GitHubVariant,
GitHubVersion,
HTTPError,
resetCachedCodeQlVersion,
} from "./util";
export const SAMPLE_DOTCOM_API_DETAILS = {
@@ -102,9 +109,9 @@ export function setupTests(testFn: TestFn<any>) {
// unless the test explicitly sets one up.
codeql.setCodeQL({});
// Reset the in-process CodeQL version cache so that it doesn't leak between
// tests, which each represent a separate Actions step in production.
resetCachedCodeQlVersion();
// Reset the in-process CodeQL command-output cache so that it doesn't leak
// between tests, which each represent a separate Actions step in production.
resetCachedCommandOutputs();
// Replace stdout and stderr so we can record output during tests
t.context.testOutput = "";
@@ -172,6 +179,11 @@ export function makeMacro<Args extends unknown[]>(
return wrapper;
}
export function getTestEnv(): Env {
const testEnv: NodeJS.ProcessEnv = {};
return getEnv(testEnv);
}
/**
* Gets an `ActionsEnv` instance for use in tests.
*/
@@ -181,6 +193,264 @@ export function getTestActionsEnv(): ActionsEnv {
};
}
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
type AllState = ["Logger", "Env", "Actions", "FeatureFlags"];
/** Initialise a fresh `ActionState<AllState>` value. */
export function initAllState(
overrides?: Partial<ActionState<AllState>>,
): ActionState<AllState> {
return {
name: ActionName.Init,
startedAt: new Date(),
logger: new RecordingLogger(),
env: getTestEnv(),
actions: getTestActionsEnv(),
features: createFeatures([]),
...overrides,
};
}
type DelayedCheck<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
export type ValueOrMutation<T> = T | ((val: T) => void);
/**
* Wraps a function that accepts an `ActionState` for testing in different environments.
*/
abstract class BaseEnvBuilder<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> {
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
private logger: RecordingLogger;
protected state: ActionState<AllState>;
protected checks: Array<DelayedCheck<Args, R, Fs>>;
constructor(
fn: (state: ActionState<Fs>, ...args: Args) => R,
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
) {
this.fn = fn;
this.logger = new RecordingLogger();
this.state =
cloneFrom !== undefined
? ({
...cloneFrom.state,
env: Object.create(cloneFrom.state.env),
actions: Object.create(cloneFrom.state.actions),
logger: this.logger,
} satisfies ActionState<AllState>)
: initAllState({ logger: this.logger });
this.checks = [...(cloneFrom?.checks ?? [])];
}
/**
* Creates a clone of this object. Used internally.
* Must be overridden by subclasses.
*/
protected abstract clone(): this;
public getLogger(): RecordingLogger {
return this.logger;
}
public getState(): ActionState<AllState> {
return this.state;
}
public withArgs(...args: Args): CallableEnvBuilder<Args, R, Fs> {
const result = new CallableEnvBuilder(this.fn, args, this.clone());
return result;
}
public withFeatures(enabled: Feature[]): this {
const result = this.clone();
result.state.features = createFeatures(enabled);
return result;
}
/**
* Sets environment variables that are always available to GitHub Actions,
* excluding some that are expected to be set to paths.
*
* @param overrides Overrides for the defaults.
*/
public withDefaultActionsEnv(overrides?: ActionVarOverrides): this {
const result = this.clone();
setupBaseActionsVars(overrides, result.state.env);
return result;
}
/**
* Sets environment variables that are always available to GitHub Actions.
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
* @param overrides Overrides for the defaults.
*/
public withActionsEnv(
tempDir: string,
toolsDir: string,
overrides?: ActionVarOverrides,
): this {
const result = this.clone();
setupActionsVars(tempDir, toolsDir, overrides, result.state.env);
return result;
}
public withEnv(arg: ValueOrMutation<Env>): this {
const result = this.clone();
if (typeof arg === "function") {
arg(result.state.env);
} else {
result.state.env = arg;
}
return result;
}
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
const result = this.clone();
if (typeof arg === "function") {
arg(result.state.actions);
} else {
result.state.actions = arg;
}
return result;
}
/**
* Adds a delayed check that `messages` are logged. The check will be
* performed after the main assertion passes.
*/
public logs(t: ExecutionContext<unknown>, ...messages: string[]): this {
const result = this.clone();
result.checks.push(async (env) => {
checkExpectedLogMessages(t, env.getLogger().messages, messages);
});
return result;
}
/**
* Adds a delayed check that `messages` are not logged. The check will be
* performed after the main assertion passes.
*/
public notLogs(t: ExecutionContext<unknown>, ...messages: string[]): this {
const result = this.clone();
result.checks.push(async (env) => {
checkUnexpectedLogMessages(t, env.getLogger().messages, messages);
});
return result;
}
}
class EnvBuilder<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> extends BaseEnvBuilder<Args, R, Fs> {
protected clone(): this {
return new EnvBuilder(this.fn, this) as this;
}
}
class CallableEnvBuilder<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> extends BaseEnvBuilder<Args, R, Fs> {
private args: Args;
constructor(
fn: (state: ActionState<Fs>, ...args: Args) => R,
args: Args,
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
) {
super(fn, cloneFrom);
this.args = args;
}
protected clone(): this {
return new CallableEnvBuilder(this.fn, this.args, this) as this;
}
public getArgs(): Args {
return this.args;
}
call(): R {
return this.fn(this.state as unknown as ActionState<Fs>, ...this.args);
}
/**
* Calls the underlying function in the configured environment and passes
* the result to `assertion` along with extra `assertionArgs`.
*
* @param assertion The assertion to apply to the result.
* @param assertionArgs Extra arguments for the assertion.
* @returns The result of the assertion.
*/
public async passes<AArgs extends readonly any[], AResult>(
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
...assertionArgs: AArgs
): Promise<AResult> {
// this.call() may or may not return a promise,
// `Promise.resolve` turns the result into one if it isn't already,
// and we then await it. That ensures that `result` is an `Awaited<R>`.
const result = await Promise.resolve(this.call());
// Run the main assertion on the `result`.
const assertionResult = await assertion(result, ...assertionArgs);
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
}
// Return the result of the main assertion.
return assertionResult;
}
/**
* Asserts that calling the underlying function should throw an exception.
*
* @param t The execution context for the assertion.
* @param expectations Expectations for the error.
* @returns The error that was thrown.
*/
public async throws<ErrorType extends ErrorConstructor | Error>(
t: ExecutionContext<unknown>,
expectations?: ThrowsExpectation<ErrorType>,
): Promise<ThrownError<ErrorType>> {
// Run the main assertion.
const error = await t.throwsAsync(
async () => Promise.resolve(this.call()),
expectations,
);
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
}
// Return the error.
return error;
}
}
/** Utility function to construct a `TestEnv`. */
export function callee<
Args extends readonly any[],
R,
Fs extends readonly StateFeature[],
>(fn: (state: ActionState<Fs>, ...args: Args) => R): EnvBuilder<Args, R, Fs> {
return new EnvBuilder(fn);
}
/**
* Default values for environment variables typically set in an Actions
* environment. Tests can override individual variables by passing them in the
@@ -200,7 +470,7 @@ export const DEFAULT_ACTIONS_VARS = {
GITHUB_WORKFLOW: "test-workflow",
RUNNER_NAME: "my-runner",
RUNNER_OS: "Linux",
} as const satisfies Record<string, string>;
} as const satisfies Partial<Record<ActionsEnvVars, string>>;
/** Partial mappings from GitHub Actions environment variables to values. */
export type ActionVarOverrides = Partial<
@@ -212,11 +482,15 @@ export type ActionVarOverrides = Partial<
* excluding some that are expected to be set to paths. See `setupActionsVars`.
*
* @param overrides Overrides for the defaults.
* @param env The environment to set the variables for.
*/
export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
export function setupBaseActionsVars(
overrides?: ActionVarOverrides,
env: Env = getEnv(),
) {
const vars = { ...DEFAULT_ACTIONS_VARS, ...overrides };
for (const [key, value] of Object.entries(vars)) {
process.env[key] = value;
env.set(key, value);
}
}
@@ -226,16 +500,18 @@ export function setupBaseActionsVars(overrides?: ActionVarOverrides) {
* @param tempDir A value for `RUNNER_TEMP` and `GITHUB_WORKSPACE`.
* @param toolsDir A value for `RUNNER_TOOL_CACHE`.
* @param overrides Overrides for the defaults.
* @param env The environment to set the variables for.
*/
export function setupActionsVars(
tempDir: string,
toolsDir: string,
overrides?: ActionVarOverrides,
env: Env = getEnv(),
) {
setupBaseActionsVars(overrides);
process.env["RUNNER_TEMP"] = tempDir;
process.env["RUNNER_TOOL_CACHE"] = toolsDir;
process.env["GITHUB_WORKSPACE"] = tempDir;
setupBaseActionsVars(overrides, env);
env.set(ActionsEnvVars.RUNNER_TEMP, tempDir);
env.set(ActionsEnvVars.RUNNER_TOOL_CACHE, toolsDir);
env.set(ActionsEnvVars.GITHUB_WORKSPACE, tempDir);
}
type LogLevel = "debug" | "info" | "warning" | "error";
@@ -369,6 +645,34 @@ export function checkExpectedLogMessages(
}
}
/**
* Checks that `messages` contains none of `unexpectedMessages`.
*/
export function checkUnexpectedLogMessages(
t: ExecutionContext<any>,
messages: LoggedMessage[],
unexpectedMessages: string[],
) {
const presentMessages: string[] = [];
for (const unexpectedMessage of unexpectedMessages) {
if (hasLoggedMessage(messages, unexpectedMessage)) {
presentMessages.push(unexpectedMessage);
}
}
if (presentMessages.length > 0) {
const listify = (lines: string[]) =>
lines.map((m) => ` - '${m}'`).join("\n");
t.fail(
`Did not expect\n\n${listify(presentMessages)}\n\nin the logger output, but found them in:\n\n${messages.map((m) => ` - '${m.message}'`).join("\n")}`,
);
} else {
t.pass();
}
}
/**
* Asserts that `message` should not have been logged to `logger`.
*/
@@ -479,7 +783,7 @@ export const makeVersionInfo = (
version: string,
features?: { [name: string]: boolean },
overlayVersion?: number,
): codeql.VersionInfo => ({
): VersionInfo => ({
version,
features,
overlayVersion,

View File

@@ -1,6 +1,6 @@
import * as semver from "semver";
import type { VersionInfo } from "./codeql";
import type { VersionInfo } from "./cli/output-cache";
export enum ToolsFeature {
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",

View File

@@ -1,17 +1,17 @@
import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import * as actionsUtil from "./actions-util";
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { getGitHubVersion } from "./api-client";
import { initFeatures } from "./feature-flags";
import { Logger, getActionsLogger } from "./logging";
import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
import { InvalidSarifUploadError } from "./sarif";
import {
createStatusReportBase,
sendStatusReport,
sendUnhandledErrorStatusReport,
StatusReportBase,
getActionsStatus,
ActionName,
@@ -23,7 +23,6 @@ import {
ConfigurationError,
checkActionVersion,
checkDiskUsage,
getErrorMessage,
initializeEnvironment,
shouldSkipSarifUpload,
wrapError,
@@ -55,12 +54,9 @@ async function sendSuccessStatusReport(
}
}
async function run(startedAt: Date) {
async function run({ startedAt, logger }: ActionState<["Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
try {
initializeEnvironment(getActionVersion());
@@ -165,20 +161,12 @@ async function run(startedAt: Date) {
}
}
/** Defines the `upload-sarif` Action. */
const uploadSarif: Action = {
name: ActionName.UploadSarif,
run,
};
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await run(startedAt);
} catch (error) {
core.setFailed(
`codeql/upload-sarif action failed: ${getErrorMessage(error)}`,
);
await sendUnhandledErrorStatusReport(
ActionName.UploadSarif,
startedAt,
error,
logger,
);
}
await runInActions(uploadSarif);
}

View File

@@ -532,58 +532,3 @@ test("Failure.orElse returns the default value for a failure result", (t) => {
const result = new util.Failure(new Error("test error"));
t.is(result.orElse("default value"), "default value");
});
test.serial(
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0" },
});
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql"), {
version: "2.20.0",
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
});
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
},
);
test.serial(
"getCachedCodeQlVersion ignores a malformed persisted value",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = "not valid json";
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
(t) => {
for (const value of [
JSON.stringify({ cmd: "/path/to/codeql" }),
JSON.stringify({ cmd: "/path/to/codeql", version: {} }),
JSON.stringify({ cmd: "/path/to/codeql", version: { version: 2 } }),
JSON.stringify({ version: { version: "2.20.0" } }),
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0", overlayVersion: "1" },
}),
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0", features: "nope" },
}),
]) {
process.env[EnvVar.CODEQL_VERSION_INFO] = value;
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined, value);
}
},
);

View File

@@ -10,14 +10,17 @@ import * as yaml from "js-yaml";
import * as semver from "semver";
import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL, VersionInfo } from "./codeql";
import type { CodeQL } from "./codeql";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import { EnvVar } from "./environment";
import { EnvVar, getRequiredEnvParam } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger } from "./logging";
// Re-export for backwards compatibility to avoid updating a lot of imports elsewhere.
export { getRequiredEnvParam, getOptionalEnvVar, getEnv } from "./environment";
/**
* The name of the file containing the base database OIDs, as stored in the
* root of the database location.
@@ -566,28 +569,6 @@ export function initializeEnvironment(version: string) {
core.exportVariable(EnvVar.VERSION, version);
}
/**
* Get an environment parameter, but throw an error if it is not set.
*/
export function getRequiredEnvParam(paramName: string): string {
const value = process.env[paramName];
if (value === undefined || value.length === 0) {
throw new Error(`${paramName} environment variable must be set`);
}
return value;
}
/**
* Get an environment variable, but return `undefined` if it is not set or empty.
*/
export function getOptionalEnvVar(paramName: string): string | undefined {
const value = process.env[paramName];
if (value?.trim().length === 0) {
return undefined;
}
return value;
}
export class HTTPError extends Error {
public status: number;
@@ -617,90 +598,6 @@ export function asHTTPError(arg: any): HTTPError | undefined {
return undefined;
}
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
/**
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
* which exercise multiple "steps" within a single process.
*/
export function resetCachedCodeQlVersion(): void {
cachedCodeQlVersion = undefined;
}
/** The persisted version together with the CLI path it was obtained from. */
interface PersistedVersionInfo {
cmd: string;
version: VersionInfo;
}
function isVersionInfo(x: unknown): x is VersionInfo {
const candidate = x as Partial<VersionInfo> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.version === "string" &&
(candidate.features === undefined ||
(typeof candidate.features === "object" &&
candidate.features !== null)) &&
(candidate.overlayVersion === undefined ||
typeof candidate.overlayVersion === "number")
);
}
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
const candidate = x as Partial<PersistedVersionInfo> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.cmd === "string" &&
isVersionInfo(candidate.version)
);
}
export function cacheCodeQlVersion(cmd: string, version: VersionInfo): void {
if (cachedCodeQlVersion !== undefined) {
throw new Error("cacheCodeQlVersion() should be called only once");
}
cachedCodeQlVersion = version;
// Persist the version so that subsequent Actions steps, which run in separate
// processes, can reuse it rather than invoking `codeql version` again. We
// record the CLI path so that a different step using a different CodeQL bundle
// doesn't pick up a stale version.
core.exportVariable(
EnvVar.CODEQL_VERSION_INFO,
JSON.stringify({ cmd, version }),
);
}
export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo {
if (cachedCodeQlVersion !== undefined) {
return cachedCodeQlVersion;
}
// Fall back to the value persisted by an earlier Actions step, if any. This is
// best-effort: any malformed or mismatched value is ignored so that the caller
// invokes `codeql version` instead.
const serialized = process.env[EnvVar.CODEQL_VERSION_INFO];
if (!serialized) {
return undefined;
}
let persisted: unknown;
try {
persisted = JSON.parse(serialized);
} catch {
return undefined;
}
if (
!isPersistedVersionInfo(persisted) ||
(cmd !== undefined && persisted.cmd !== cmd)
) {
return undefined;
}
// Memoize the parsed value so that subsequent calls in this process don't
// re-parse the environment variable.
cachedCodeQlVersion = persisted.version;
return cachedCodeQlVersion;
}
export async function codeQlVersionAtLeast(
codeql: CodeQL,
requiredVersion: string,
@@ -712,7 +609,13 @@ export function getBaseDatabaseOidsFilePath(config: Config): string {
return path.join(config.dbLocation, BASE_DATABASE_OIDS_FILE_NAME);
}
// Create a bundle for the given DB, if it doesn't already exist
/**
* Bundles the database for the given language into a `.zip` file, returning the path to it.
*
* If a bundle for `dbName` already exists (e.g. from an earlier call), it is deleted and
* re-created, so each call produces a fresh bundle reflecting the current database contents and the
* given `includeDiagnostics` value.
*/
export async function bundleDb(
config: Config,
language: Language,