Compare commits

..
315 Commits
Author SHA1 Message Date
Michael B. Gale 08d09a53f0 Merge pull request #4029 from github/backport-v3.37.2-e0647621c
Merge releases/v4 into releases/v3
2026-07-21 16:06:02 +01:00
Michael B. Gale 74406f0856 Rebuild 2026-07-21 15:31:11 +01:00
github-actions[bot] 8dfbacbba5 Update version and changelog for v3.37.2 2026-07-21 14:28:11 +00:00
github-actions[bot] ab2931b320 Merge remote-tracking branch 'origin/releases/v4' into backport-v3.37.2-e0647621c
# Conflicts:
#	lib/entry-points.js
2026-07-21 14:28:11 +00:00
github-actions[bot] c87e7fae54 Revert "Rebuild"
This reverts commit 1138fefa2b.
2026-07-21 14:28:11 +00:00
github-actions[bot] 5a1ba3bdf6 Revert "Update version and changelog for v3.37.1"
This reverts commit 3fa858af16.
2026-07-21 14:28:10 +00:00
Michael B. Gale e0647621c2 Merge pull request #4027 from github/update-v4.37.2-385bcdc5a
Merge main into releases/v4
2026-07-21 15:25:12 +01:00
Michael B. Gale e0faed8391 Add a couple of change notes 2026-07-21 15:06:15 +01:00
github-actions[bot] 73aad0eaa9 Update changelog for v4.37.2 2026-07-21 13:47:43 +00:00
Michael B. Gale 385bcdc5af Merge pull request #4026 from github/dependabot/npm_and_yarn/tar-7.5.20
Bump tar from 7.5.16 to 7.5.20
2026-07-21 13:29:48 +00:00
Michael B. Gale de0229cea1 Merge branch 'main' into dependabot/npm_and_yarn/tar-7.5.20 2026-07-21 14:15:53 +01:00
Michael B. Gale 115e8cbbe9 Merge pull request #4010 from github/mbg/ts/update-release-branch
Convert `update-release-branch.py` to TypeScript
2026-07-21 12:57:10 +00:00
dependabot[bot] dbdf0b0c7d Bump tar from 7.5.16 to 7.5.20
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.20.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.20)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.20
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-21 11:39:14 +00:00
Michael B. Gale 830c23121c Merge pull request #4023 from github/mbg/ff/remove-new-remote-file-addresses-ff
Promote `NewRemoteFileAddresses` FF
2026-07-21 11:25:34 +00:00
Michael B. Gale 69fd9e97ba Merge pull request #4024 from github/mbg/private-registry/make-docker-available
Always make `docker_registry` registries available
2026-07-21 10:58:49 +00:00
Michael B. Gale b85568788a Always make docker_registry registries available 2026-07-20 19:56:46 +01:00
Michael B. Gale dd35309c87 Remove explicit FF from start-proxy check 2026-07-20 11:23:22 +01:00
Michael B. Gale da21ad6a71 Remove NewRemoteFileAddresses FF and default to its behaviour 2026-07-20 11:22:34 +01:00
Michael B. Gale cf463419fd Merge pull request #4007 from github/mbg/use-registry-proxy-for-repo-auth
Use private registry proxy for API requests when available
2026-07-17 14:03:28 +00:00
Michael B. Gale 7db34ae6f4 Refactor looking up proxy env vars into getRegistryProxyConfig.
This makes that function testable.
2026-07-17 13:48:49 +01:00
Michael B. Gale 8125f87336 Remove unused getApiFetch 2026-07-16 21:34:10 +01:00
Óscar San José b7351df727 Merge pull request #4022 from github/backport-v3.37.1-7188fc363
Merge releases/v4 into releases/v3
2026-07-16 20:17:18 +02:00
Óscar San José 27e669d3d6 run pr-checks/sync-back.ts 2026-07-16 18:57:25 +02:00
Óscar San José 1138fefa2b Rebuild 2026-07-16 18:36:30 +02:00
Óscar San José 7c4a258544 Merge pull request #4021 from github/mergeback/v4.37.1-to-main-7188fc36
Mergeback v4.37.1 refs/heads/releases/v4 into main
2026-07-16 16:22:12 +00:00
github-actions[bot] 3fa858af16 Update version and changelog for v3.37.1 2026-07-16 15:38:11 +00:00
github-actions[bot] 4b646a0e5d Merge remote-tracking branch 'origin/releases/v4' into backport-v3.37.1-7188fc363
# Conflicts:
#	lib/entry-points.js
2026-07-16 15:38:11 +00:00
github-actions[bot] 44c5914b66 Revert "Rebuild"
This reverts commit af1b30ee40.
2026-07-16 15:38:10 +00:00
github-actions[bot] 0d531cfd92 Revert "Update version and changelog for v3.37.0"
This reverts commit 530c909a55.
2026-07-16 15:38:10 +00:00
github-actions[bot] 0297913805 Rebuild 2026-07-16 15:35:21 +00:00
github-actions[bot] 1226301537 Update changelog and version after v4.37.1 2026-07-16 15:35:09 +00:00
Óscar San José 7188fc3636 Merge pull request #4020 from github/update-v4.37.1-9e7c07009
Merge main into releases/v4
2026-07-16 17:33:33 +02:00
github-actions[bot] c8b5f69be6 Update changelog for v4.37.1 2026-07-16 15:02:41 +00:00
Michael B. Gale 9e7c070092 Merge pull request #4014 from github/mbg/explicit-remote-prefix
Add support for an explicit remote path prefix
2026-07-16 11:58:30 +00:00
Michael B. Gale 4292bd7215 Remove unneeded type assertion now that ApiClient exists 2026-07-16 12:54:34 +01:00
Michael B. Gale 7b19180f8d Merge remote-tracking branch 'origin/main' into mbg/use-registry-proxy-for-repo-auth 2026-07-16 12:48:24 +01:00
Michael B. Gale 3492b7e9ab Change REMOTE_PATH_PREFIX to remote= 2026-07-16 12:43:48 +01:00
Michael B. Gale 3654baa924 Merge remote-tracking branch 'origin/main' into mbg/explicit-remote-prefix 2026-07-16 12:41:37 +01:00
Michael B. Gale 2d682ac05f Merge pull request #4017 from github/dependabot/github_actions/dot-github/workflows/actions-minor-059448b906
Bump ruby/setup-ruby from 1.315.0 to 1.316.0 in /.github/workflows in the actions-minor group across 1 directory
2026-07-16 11:08:08 +00:00
Michael B. Gale 23f6a50753 Merge pull request #4009 from github/mbg/action-state/additions
Add `Api`, `ReadOnlyEnv`, and `Base` state features
2026-07-16 10:20:43 +00:00
Michael B. Gale 1ee3c75d19 Merge pull request #4018 from github/dependabot/github_actions/dot-github/workflows/actions/setup-node-7.0.0
Bump actions/setup-node from 6.4.0 to 7.0.0 in /.github/workflows
2026-07-16 10:11:14 +00:00
Michael B. Gale e053684dc5 Merge pull request #4015 from github/dependabot/npm_and_yarn/npm-minor-fd2e8365b1
Bump the npm-minor group across 1 directory with 4 updates
2026-07-16 10:03:24 +00:00
Óscar San José 6803c5671d Merge pull request #4019 from github/update-bundle/codeql-bundle-v2.26.1
Update default bundle to 2.26.1
2026-07-16 09:24:40 +00:00
github-actions[bot] 8507f884db Add changelog note 2026-07-16 09:08:17 +00:00
github-actions[bot] c31b06d582 Update default bundle to codeql-bundle-v2.26.1 2026-07-16 09:08:11 +00:00
github-actions[bot] cc5c777dcd Rebuild 2026-07-15 17:59:50 +00:00
github-actions[bot] c224750821 Rebuild 2026-07-15 17:59:29 +00:00
dependabot[bot] db76a66b98 Bump actions/setup-node from 6.4.0 to 7.0.0 in /.github/workflows
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 17:57:52 +00:00
dependabot[bot] c263ed7d83 Bump ruby/setup-ruby
Bumps the actions-minor group with 1 update in the /.github/workflows directory: [ruby/setup-ruby](https://github.com/ruby/setup-ruby).


Updates `ruby/setup-ruby` from 1.315.0 to 1.316.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/0dafeac902942906541bc140009cdbf32665b601...d45b1a4e94b71acab930e56e79c6aa188764e7f9)

---
updated-dependencies:
- dependency-name: ruby/setup-ruby
  dependency-version: 1.316.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-15 17:57:33 +00:00
github-actions[bot] 60e79ccb20 Rebuild 2026-07-15 17:56:43 +00:00
dependabot[bot] b82a08934e 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-github](https://github.com/github/eslint-plugin-github), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [tsx](https://github.com/privatenumber/tsx).


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

Updates `eslint-plugin-github` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/github/eslint-plugin-github/releases)
- [Commits](https://github.com/github/eslint-plugin-github/compare/v6.0.0...v6.1.0)

Updates `typescript-eslint` from 8.62.1 to 8.63.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.63.0/packages/typescript-eslint)

Updates `tsx` from 4.22.4 to 4.23.0
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.4...v4.23.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: eslint-plugin-github
  dependency-version: 6.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: tsx
  dependency-version: 4.23.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-15 17:54:50 +00:00
Michael B. Gale 247ce88c8f Gate changes behind new format FF 2026-07-15 16:14:55 +01:00
Michael B. Gale decb87044a Rename helper functions 2026-07-15 16:13:02 +01:00
Michael B. Gale abce6483ea Fix typo in comment 2026-07-15 16:11:50 +01:00
Michael B. Gale 23339952d6 Add expliciit prefix for remote file addresses 2026-07-15 15:59:23 +01:00
Michael B. Gale 57ca205769 Add AssertableTarget interface 2026-07-15 15:58:24 +01:00
Michael B. Gale 001d4ae610 Move the local path prefix into a constant 2026-07-15 15:56:52 +01:00
Michael B. Gale 6d3bde3729 Split up and document isLocal 2026-07-15 14:32:14 +01:00
Michael B. Gale 9832a9df15 Refactor parseNewRemoteFileAddress out of parseRemoteFileAddress 2026-07-15 14:32:07 +01:00
Michael B. Gale c73d965e34 Add failing test for the issue 2026-07-15 14:31:49 +01:00
Michael B. Gale 9c0c11a49e Add tests for loadUserConfig 2026-07-15 14:31:39 +01:00
Michael B. Gale 423e3416b1 Delete python version 2026-07-15 11:58:26 +01:00
Michael B. Gale 41c7af7cf0 Use paginate 2026-07-15 11:58:08 +01:00
Michael B. Gale e58424170f Merge pull request #4011 from github/mbg/add-diagnostic-tags
Add support for diagnostic tags, and use them for `mergeDefaultSetupAndUserConfigs`
2026-07-15 09:28:34 +00:00
Michael B. Gale 1a0eca3555 Validate proxy argument in tests 2026-07-14 19:33:01 +01:00
Michael B. Gale d705aa30b5 Merge remote-tracking branch 'origin/main' into mbg/use-registry-proxy-for-repo-auth 2026-07-14 19:27:25 +01:00
Michael B. Gale 405f67d326 Add internal-error tags to diagnostics in mergeDefaultSetupAndUserConfigs 2026-07-14 19:22:35 +01:00
Michael B. Gale f3d47397e8 Add tags parameter to makeTelemetryDiagnostic 2026-07-14 19:22:35 +01:00
Michael B. Gale 8aa21b44a3 Split DiagnosticMessage into BaseDiagnosticMessage 2026-07-14 19:22:35 +01:00
Michael B. Gale 358d197a13 Refactor source type into DiagnosticSource 2026-07-14 19:22:34 +01:00
Michael B. Gale 8bc3738008 Add DiagnosticTag type and tags property 2026-07-14 19:22:34 +01:00
Michael B. Gale a1c676d2f6 Test proxy usage for getRemoteConfig 2026-07-14 17:02:22 +01:00
Michael B. Gale 47e9c29998 Log whether the proxy is picked up on 2026-07-14 17:02:07 +01:00
Michael B. Gale b946565527 Add and use clone method for ReadOnlyEnv 2026-07-14 16:59:51 +01:00
Michael B. Gale 205b37b035 Use ActionState for getRegistryProxy 2026-07-14 16:17:51 +01:00
Michael B. Gale 6d70593fb7 Gate proxy usage behind FF 2026-07-14 16:12:38 +01:00
Michael B. Gale 80599cc5d9 Allow proxy to be threaded into createApiClientWithDetails
This will make it easier to gate the `getRegistryProxy` with a FF,
without the significant refactoring that is needed to thread the FFs
into `createApiClientWithDetails`
2026-07-14 16:07:08 +01:00
Michael B. Gale ae48798f3b Make versions.ts testable and add basic tests 2026-07-14 15:50:42 +01:00
Michael B. Gale a464bf19e9 Move package.json helpers into their own file 2026-07-14 15:33:11 +01:00
Michael B. Gale 9b314f4394 Make replaceVersionInPackageJson dry-run-aware 2026-07-14 15:30:26 +01:00
Michael B. Gale d694648fd8 Make changelog.ts more testable and add tests 2026-07-14 15:28:19 +01:00
Michael B. Gale 29d5e4cb18 Merge pull request #3994 from github/mbg/config/merge
Allow merging Default Setup `config` with config file
2026-07-14 14:23:09 +00:00
Michael B. Gale 583bf3e8c5 Fix comment typos 2026-07-14 14:50:45 +01:00
Michael B. Gale f9a9f4862b Run npm ci in release-initialise workflow.
Remove now unneeded `npm ci` from `release-branches` action
2026-07-14 14:48:06 +01:00
Michael B. Gale d4b3323463 Use TS script in workflow 2026-07-14 14:48:06 +01:00
Michael B. Gale fd0ae66c1e Restore some comments 2026-07-14 14:48:05 +01:00
Michael B. Gale e2472fc5f9 Make processChangelogForBackports dry-run-aware 2026-07-14 14:48:05 +01:00
Michael B. Gale 4b861b89fc Move changelog-related definitions into their own module 2026-07-14 14:48:01 +01:00
Michael B. Gale 4f688dedd0 Make dependencies explicit 2026-07-14 14:45:17 +01:00
Michael B. Gale 28a0813a12 Fix comment 2026-07-14 14:44:54 +01:00
Michael B. Gale 3d2713f64e Merge remote-tracking branch 'origin/main' into mbg/config/merge 2026-07-14 14:37:49 +01:00
Michael B. Gale 2c45c8158b Add DryRunOption interface 2026-07-14 14:18:45 +01:00
Michael B. Gale 5c030f4a48 Add summary comment 2026-07-14 14:18:44 +01:00
Michael B. Gale 78d71fb252 Create PR 2026-07-14 14:18:44 +01:00
Michael B. Gale 5e212030b8 Add prepareNewBranch 2026-07-14 14:18:44 +01:00
Michael B. Gale 639fc5d7ea Add changelog helpers 2026-07-14 14:18:44 +01:00
Michael B. Gale c8ed70e459 Add rebuildAction function 2026-07-14 14:18:43 +01:00
Michael B. Gale 212aa33f48 Add runCommand 2026-07-14 14:18:43 +01:00
Michael B. Gale 460cc0c970 Add dry run support and push branch 2026-07-14 14:18:43 +01:00
Michael B. Gale 557921759b Check whether the new branch exists 2026-07-14 14:18:43 +01:00
Michael B. Gale c3da0a9ad3 Fetch commit info 2026-07-14 14:18:43 +01:00
Michael B. Gale 4ca9f5301b Add runGit and use it to obtain the source branch SHA 2026-07-14 14:18:42 +01:00
Michael B. Gale 440cebc19d Add getCurrentVersion 2026-07-14 14:18:42 +01:00
Michael B. Gale 1b146d1b6a Validate target branch and extract major version 2026-07-14 14:18:42 +01:00
Michael B. Gale d0b11cae68 Make script executable 2026-07-14 14:18:42 +01:00
Michael B. Gale a10d7a7891 Parse command line options 2026-07-14 14:18:42 +01:00
Michael B. Gale 14952376dc Add getGitHubToken 2026-07-14 14:18:41 +01:00
Michael B. Gale 85052938f8 Add constants 2026-07-14 14:18:41 +01:00
Michael B. Gale 4c2bf01170 Scaffold basic update-release-branch.ts 2026-07-14 14:18:41 +01:00
Michael B. Gale 2f9048cfbd Require BaseState to be declared explicitly 2026-07-14 10:12:44 +01:00
Michael B. Gale 2dbfdcaa83 Add Api to ActionState 2026-07-14 10:09:03 +01:00
Michael B. Gale 1542951d09 Type the API client 2026-07-14 10:09:03 +01:00
Michael B. Gale 4fb6147969 Add ReadOnlyEnv to FeatureState 2026-07-14 10:07:42 +01: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 50b3687dd7 Specify custom config-file in start-proxy check 2026-07-13 16:24:54 +01:00
Michael B. Gale b6d92e33f7 Set CODEQL_PROXY_* vars for init step in test workflow 2026-07-13 16:16:00 +01:00
Michael B. Gale b0eaa56a8f Set language inputs in start-proxy check workflow 2026-07-13 16:16:00 +01:00
Michael B. Gale e387ec1de1 Move init step at the end of start-proxy test workflow 2026-07-13 16:16:00 +01:00
Michael B. Gale a29dee455c Add minimal proxy init code 2026-07-13 16:15:58 +01:00
Michael B. Gale 5172487de5 Allow getTestEnv to be parameterised over initial env 2026-07-13 16:15:40 +01:00
Michael B. Gale 8763bac625 Add RegistryProxyVars enum 2026-07-13 16:15:40 +01:00
Michael B. Gale dab0ab7cb7 Make undici an explicit dependency 2026-07-13 16:15:36 +01: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 75773288da Return function result from passes alongside assertion result 2026-07-13 10:27:29 +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 f181d70282 Update determineUserConfig tests to use new framework 2026-07-13 09:58:52 +01:00
Michael B. Gale 2773684c29 Update test for changed test API 2026-07-13 09:31:38 +01:00
Michael B. Gale 66c15f441a Merge remote-tracking branch 'origin/main' into mbg/config/merge 2026-07-13 09:25:36 +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 f630f8fd04 Report invalid keys separately 2026-07-10 14:39:08 +01:00
Michael B. Gale 26cbf9f504 Don't include nested keys that are unknown 2026-07-10 14:34:33 +01:00
Michael B. Gale e26215c5d8 fixup! Track invalid keys, and use more standard JSON path notation 2026-07-10 14:25:15 +01:00
Michael B. Gale 0b10dee3ee Propagate CheckSchemaOptions to allow failFast to work as intended 2026-07-10 14:24:33 +01:00
Michael B. Gale 512bf6f1a3 Track invalid keys, and use more standard JSON path notation 2026-07-10 14:21:41 +01:00
Michael B. Gale 8d8f054dfa Add convenience functions to produce CheckSchemaResult values 2026-07-10 13:18:45 +01:00
Michael B. Gale e4752e74e1 Remove keys from unrecognised set as soon as found 2026-07-10 13:10:25 +01:00
Michael B. Gale cee0056fc0 Allow nested checking 2026-07-10 00:43:05 +01:00
Michael B. Gale 7c961fa357 Add diagnostic for unrecognised keys 2026-07-10 00:14:19 +01:00
Michael B. Gale 847e7af699 Log warning for unrecognised keys in Default Setup config 2026-07-10 00:11:17 +01: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 9573f05654 Improve type inference for object and validateSchema 2026-07-10 00:01:13 +01:00
Michael B. Gale 98dbd66eae Add an object Validator 2026-07-09 23:42:28 +01:00
Michael B. Gale 1f91ec8f3c Add an array Validator 2026-07-09 23:36:50 +01:00
Michael B. Gale dc2d916874 Add checkSchema function 2026-07-09 23:28:02 +01:00
Michael B. Gale 6973946465 Check file on disk and mutated inputs 2026-07-09 23:09:14 +01:00
Michael B. Gale 6829d6ce38 Remove obsolete JSDoc parameter for loadUserConfig 2026-07-09 23:02:58 +01:00
Michael B. Gale d2472aa525 Merge remote-tracking branch 'origin/main' into mbg/config/merge 2026-07-09 23:01:05 +01: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
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](https://github.com/actions/setup-java/compare/1bcf9fb12cf4aa7d266a90ae39939e61372fe520...0f481fcb613427c0f801b606911222b5b6f3083a)

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](https://github.com/ruby/setup-ruby/compare/9eb537ca036ebaed86729dcb9309076e4c5c3b74...0dafeac902942906541bc140009cdbf32665b601)

---
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 06898d7b8b Rename mergeUserConfigs 2026-07-08 17:46:02 +01:00
Michael B. Gale 7ccd988125 Merge branch 'main' into mbg/config/merge 2026-07-08 17:31:19 +01: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
Michael B. Gale 02c5e83432 Merge pull request #3998 from github/backport-v3.37.0-99df26d4f
Merge releases/v4 into releases/v3
2026-07-08 14:15:11 +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
Michael B. Gale af1b30ee40 Rebuild 2026-07-08 13:43:18 +01:00
github-actions[bot] 530c909a55 Update version and changelog for v3.37.0 2026-07-08 12:11:58 +00:00
github-actions[bot] 0507814306 Merge remote-tracking branch 'origin/releases/v4' into backport-v3.37.0-99df26d4f
# Conflicts:
#	lib/entry-points.js
2026-07-08 12:11:58 +00:00
github-actions[bot] 157d34d3cf Revert "Rebuild"
This reverts commit d56b7d789e.
2026-07-08 12:11:58 +00:00
github-actions[bot] b8c8426f7c Revert "Update version and changelog for v3.36.3"
This reverts commit 66185e671d.
2026-07-08 12:11:58 +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 8be707bd2c Merge remote-tracking branch 'origin/main' into mbg/config/merge 2026-07-06 15:56:19 +01: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 8476401b09 Make lazy FF check less ambiguous 2026-07-06 14:45:41 +01:00
Michael B. Gale 4509fb3c4b Fix JSDoc for determineUserConfig 2026-07-06 14:43:49 +01:00
Michael B. Gale 44d6b3b016 Initialise env in actions-util when not provided
This makes the implementations more consistent for now, and less error-prone
2026-07-06 14:41:55 +01: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 9a06fa6b38 Set the required workspacePath input 2026-07-06 14:10:30 +01:00
Michael B. Gale 764f470b74 Test inputs.configFile mutation in tests 2026-07-06 13:50:40 +01:00
Michael B. Gale d149f93d92 Return mergedConfig instead of loading it again 2026-07-06 13:50:39 +01:00
Michael B. Gale 3707b44eaa Document that inputs might be mutated 2026-07-06 13:50:39 +01:00
Michael B. Gale 42c0c6aedd Allow merging Default Setup config with config file 2026-07-06 13:50:39 +01:00
Michael B. Gale 31577749b3 Give determineUserConfig access to the environment 2026-07-06 13:50:39 +01:00
Michael B. Gale 18d20b7c42 Allow setting environment variables 2026-07-06 13:50:39 +01:00
Michael B. Gale d1db924e0b Allow env input for relevant functions in actions-util.ts 2026-07-06 13:50:38 +01:00
Michael B. Gale 0188f395e2 Add unit tests for existing behaviour of determineUserConfig 2026-07-06 13:50:38 +01:00
Michael B. Gale 6860cc1f50 Move validateConfig FF query 2026-07-06 13:50:38 +01:00
Michael B. Gale 53a488ce2e Add JSDoc for loadUserConfig 2026-07-06 13:50:38 +01:00
Michael B. Gale 132634ddfd Add FF for configuration merging 2026-07-06 13:50:37 +01:00
Michael B. Gale a52a9662e9 Add mergeUserConfigs with tests 2026-07-06 13:50:37 +01:00
Michael B. Gale cd604f831b Refactor determineUserConfig out of initConfig 2026-07-06 13:50:37 +01:00
Michael B. Gale c5128610cf Extend UserConfig type to be aware of Default Setup properties 2026-07-06 13:50:36 +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
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 CamposandMichael B. Gale 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 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](https://github.com/actions/setup-dotnet/compare/9a946fdbd5fb07b82b2f5a4466058b876ab72bb2...26b0ec14cb23fa6904739307f278c14f94c95bf1)

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

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](https://github.com/ruby/setup-ruby/compare/89f90524b88a01fe6e0b732220432cc6142926af...9eb537ca036ebaed86729dcb9309076e4c5c3b74)

---
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
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 ffb3423848 Add isNumber/number validator 2026-06-30 16:35:16 -05:00
Michael B. Gale 708e106533 Index ActionState over the types of state used 2026-06-30 17:19:20 +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
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
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
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
92 changed files with 6846 additions and 3159 deletions

No files matched your search

@@ -22,7 +22,6 @@ runs:
MAJOR_VERSION: ${{ inputs.major_version }}
LATEST_TAG: ${{ inputs.latest_tag }}
run: |
npm ci
npx tsx ./pr-checks/release-branches.ts \
--major-version "$MAJOR_VERSION" \
--latest-tag "$LATEST_TAG"
@@ -21,6 +21,10 @@ runs:
node-version: 24
cache: 'npm'
- name: Install JavaScript dependencies
shell: bash
run: npm ci
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
-474
View File
@@ -1,474 +0,0 @@
import argparse
import datetime
import fileinput
import re
from github import Github
import json
import os
import subprocess
EMPTY_CHANGELOG = """# CodeQL Action Changelog
## [UNRELEASED]
No user facing changes.
"""
# NB: This exact commit message is used to find commits for reverting during backports.
# Changing it requires a transition period where both old and new versions are supported.
BACKPORT_COMMIT_MESSAGE = 'Update version and changelog for v'
# Commit message used for rebuild commits, both those produced by this script and those produced
# by the `Rebuild Action` workflow (`.github/workflows/rebuild.yml`).
REBUILD_COMMIT_MESSAGE = 'Rebuild'
# Name of the remote
ORIGIN = 'origin'
# Environment variables to check for a GitHub API token.
TOKEN_ENVIRONMENT_VARIABLES = ('GH_TOKEN', 'GITHUB_TOKEN')
# Gets a GitHub API token from one of the supported environment variables.
def get_github_token():
for variable_name in TOKEN_ENVIRONMENT_VARIABLES:
token = os.environ.get(variable_name, '').strip()
if token:
return token
raise Exception('Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.')
# Runs git with the given args and returns the stdout.
# Raises an error if git does not exit successfully (unless passed
# allow_non_zero_exit_code=True).
def run_git(*args, allow_non_zero_exit_code=False):
cmd = ['git', *args]
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if not allow_non_zero_exit_code and p.returncode != 0:
raise Exception(f'Call to {" ".join(cmd)} exited with code {p.returncode} stderr: {p.stderr.decode("ascii")}.')
return p.stdout.decode('ascii')
# Runs the given command, streaming output to the console.
# Raises an error if the command does not exit successfully.
def run_command(*args):
cmd = list(args)
print(f'Running `{" ".join(cmd)}`.')
subprocess.run(cmd, check=True)
# Rebuilds the action and commits any changes.
def rebuild_action():
# For backports, the only source-level change vs the source branch is the new version number,
# so we just need to refresh the version embedded in `lib/`.
run_command('npm', 'ci')
run_command('npm', 'run', 'build')
run_git('add', '--all')
# `git diff --cached --quiet` exits 0 if there are no staged changes, 1 if there are.
if subprocess.run(['git', 'diff', '--cached', '--quiet']).returncode == 0:
print('Rebuild produced no changes; skipping Rebuild commit.')
else:
run_git('commit', '-m', REBUILD_COMMIT_MESSAGE)
print('Created Rebuild commit.')
# Returns true if the given branch exists on the origin remote
def branch_exists_on_remote(branch_name):
return run_git('ls-remote', '--heads', ORIGIN, branch_name).strip() != ''
# Opens a PR from the given branch to the target branch
def open_pr(
repo, all_commits, source_branch_short_sha, new_branch_name, source_branch, target_branch,
conductor, is_primary_release, conflicted_files):
# Sort the commits into the pull requests that introduced them,
# and any commits that don't have a pull request
pull_requests = []
commits_without_pull_requests = []
for commit in all_commits:
pr = get_pr_for_commit(commit)
if pr is None:
commits_without_pull_requests.append(commit)
elif not any(p for p in pull_requests if p.number == pr.number):
pull_requests.append(pr)
print(f'Found {len(pull_requests)} pull requests.')
print(f'Found {len(commits_without_pull_requests)} commits not in a pull request.')
# Sort PRs and commits by age
pull_requests = sorted(pull_requests, key=lambda pr: pr.number)
commits_without_pull_requests = sorted(commits_without_pull_requests, key=lambda c: c.commit.author.date)
# Start constructing the body text
body = []
body.append(f'Merging {source_branch_short_sha} into `{target_branch}`.')
body.append('')
body.append(f'Conductor for this PR is @{conductor}.')
# List all PRs merged
if len(pull_requests) > 0:
body.append('')
body.append('Contains the following pull requests:')
for pr in pull_requests:
# Use PR author if they are GitHub staff, otherwise use the merger
display_user = get_pr_author_if_staff(pr) or get_merger_of_pr(repo, pr)
body.append(f'- #{pr.number} (@{display_user})')
# List all commits not part of a PR
if len(commits_without_pull_requests) > 0:
body.append('')
body.append('Contains the following commits not from a pull request:')
for commit in commits_without_pull_requests:
author_description = f' (@{commit.author.login})' if commit.author is not None else ''
body.append(f'- {commit.sha} - {get_truncated_commit_message(commit)}{author_description}')
body.append('')
body.append('Please do the following:')
if len(conflicted_files) > 0:
body.append(' - [ ] Ensure `package.json` file contains the correct version.')
body.append(' - [ ] Add a commit to this branch to resolve the merge conflicts ' +
'in the following files:')
body.extend([f' - `{file}`' for file in conflicted_files])
body.append(' - [ ] Rebuild the Action locally (`npm run build`) and push any changes to the ' +
f'built output in `lib` as a separate commit named exactly `{REBUILD_COMMIT_MESSAGE}`.')
body.append(' - [ ] Ensure another maintainer has reviewed the additional commits you added to this ' +
'branch to resolve the merge conflicts.')
body.append(' - [ ] Ensure the CHANGELOG displays the correct version and date.')
body.append(' - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.')
body.append(f' - [ ] Check that there are not any unexpected commits being merged into the `{target_branch}` branch.')
body.append(' - [ ] Ensure the docs team is aware of any documentation changes that need to be released.')
body.append(' - [ ] Approve running the full set of PR checks if you have not pushed any changes.')
body.append(' - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.')
if is_primary_release:
body.append(' - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.')
body.append(' - [ ] Merge all backport PRs to older release branches, that will automatically be created once this PR is merged.')
title = f'Merge {source_branch} into {target_branch}'
# Create the pull request
pr = repo.create_pull(title=title, body='\n'.join(body), head=new_branch_name, base=target_branch)
print(f'Created PR #{str(pr.number)}')
# Assign the conductor
pr.add_to_assignees(conductor)
print(f'Assigned PR to {conductor}')
# Gets a list of the SHAs of all commits that have happened on the source branch
# since the last release to the target branch.
# This will not include any commits that exist on the target branch
# that aren't on the source branch.
def get_commit_difference(repo, source_branch, target_branch):
# Passing split nothing means that the empty string splits to nothing: compare `''.split() == []`
# to `''.split('\n') == ['']`.
commits = run_git('log', '--pretty=format:%H', f'{ORIGIN}/{target_branch}..{ORIGIN}/{source_branch}').strip().split()
# Convert to full-fledged commit objects
commits = [repo.get_commit(c) for c in commits]
# Filter out merge commits for PRs
return list(filter(lambda c: not is_pr_merge_commit(c), commits))
# Is the given commit the automatic merge commit from when merging a PR
def is_pr_merge_commit(commit):
return commit.committer is not None and commit.committer.login == 'web-flow' and len(commit.parents) > 1
# Gets a copy of the commit message that should display nicely
def get_truncated_commit_message(commit):
message = commit.commit.message.split('\n')[0]
if len(message) > 60:
return f'{message[:57]}...'
else:
return message
# Converts a commit into the PR that introduced it to the source branch.
# Returns the PR object, or None if no PR could be found.
def get_pr_for_commit(commit):
prs = commit.get_pulls()
if prs.totalCount > 0:
# In the case that there are multiple PRs, return the earliest one
prs = list(prs)
sorted_prs = sorted(prs, key=lambda pr: int(pr.number))
return sorted_prs[0]
else:
return None
# Get the person who merged the pull request.
# For most cases this will be the same as the author, but for PRs opened
# by external contributors getting the merger will get us the GitHub
# employee who reviewed and merged the PR.
def get_merger_of_pr(repo, pr):
return repo.get_commit(pr.merge_commit_sha).author.login
# Get the PR author if they are GitHub staff, otherwise None.
def get_pr_author_if_staff(pr):
if pr.user is None:
return None
if getattr(pr.user, 'site_admin', False):
return pr.user.login
return None
def get_current_version():
with open('package.json', 'r') as f:
return json.load(f)['version']
# `npm version` doesn't always work because of merge conflicts, so we
# replace the version in package.json textually.
def replace_version_package_json(prev_version, new_version):
prev_line_is_codeql = False
for line in fileinput.input('package.json', inplace = True, encoding='utf-8'):
if prev_line_is_codeql and f'\"version\": \"{prev_version}\"' in line:
print(line.replace(prev_version, new_version), end='')
else:
prev_line_is_codeql = False
print(line, end='')
if '\"name\": \"codeql\",' in line:
prev_line_is_codeql = True
def get_today_string():
today = datetime.datetime.today()
return '{:%d %b %Y}'.format(today)
def process_changelog_for_backports(source_branch_major_version, target_branch_major_version):
# changelog entries can use the following format to indicate
# that they only apply to newer versions
some_versions_only_regex = re.compile(r'\[v(\d+)\+ only\]')
output = ''
with open('CHANGELOG.md', 'r') as f:
# until we find the first section, just duplicate all lines
found_first_section = False
while not found_first_section:
line = f.readline()
if not line:
raise Exception('Could not find any change sections in CHANGELOG.md') # EOF
if line.startswith('## '):
line = line.replace(f'## {source_branch_major_version}', f'## {target_branch_major_version}')
found_first_section = True
output += line
# found_content tracks whether we hit two headings in a row
found_content = False
output += '\n'
while True:
line = f.readline()
if not line:
break # EOF
line = line.rstrip('\n')
# filter out changenote entries that apply only to newer versions
match = some_versions_only_regex.search(line)
if match:
if int(target_branch_major_version) < int(match.group(1)):
continue
if line.startswith('## '):
line = line.replace(f'## {source_branch_major_version}', f'## {target_branch_major_version}')
if found_content == False:
# we have found two headings in a row, so we need to add the placeholder message.
output += 'No user facing changes.\n'
found_content = False
output += f'\n{line}\n\n'
else:
if line.strip() != '':
found_content = True
# we use the original line here, rather than the stripped version
# so that we preserve indentation
output += line + '\n'
with open('CHANGELOG.md', 'w') as f:
f.write(output)
def update_changelog(version):
if (os.path.exists('CHANGELOG.md')):
content = ''
with open('CHANGELOG.md', 'r') as f:
content = f.read()
else:
content = EMPTY_CHANGELOG
newContent = content.replace('[UNRELEASED]', f'{version} - {get_today_string()}', 1)
with open('CHANGELOG.md', 'w') as f:
f.write(newContent)
def main():
parser = argparse.ArgumentParser('update-release-branch.py')
parser.add_argument(
'--repository-nwo',
type=str,
required=True,
help='The nwo of the repository, for example github/codeql-action.'
)
parser.add_argument(
'--source-branch',
type=str,
required=True,
help='Source branch for release branch update.'
)
parser.add_argument(
'--target-branch',
type=str,
required=True,
help='Target branch for release branch update.'
)
parser.add_argument(
'--is-primary-release',
action='store_true',
default=False,
help='Whether this update is the primary release for the current major version.'
)
parser.add_argument(
'--conductor',
type=str,
required=True,
help='The GitHub handle of the person who is conducting the release process.'
)
args = parser.parse_args()
source_branch = args.source_branch
target_branch = args.target_branch
is_primary_release = args.is_primary_release
repo = Github(get_github_token()).get_repo(args.repository_nwo)
# the target branch will be of the form releases/vN, where N is the major version number
target_branch_major_version = target_branch.strip('releases/v')
# split version into major, minor, patch
_, v_minor, v_patch = get_current_version().split('.')
version = f"{target_branch_major_version}.{v_minor}.{v_patch}"
# Print what we intend to go
print(f'Considering difference between {source_branch} and {target_branch}...')
source_branch_short_sha = run_git('rev-parse', '--short', f'{ORIGIN}/{source_branch}').strip()
print(f'Current head of {source_branch} is {source_branch_short_sha}.')
# See if there are any commits to merge in
commits = get_commit_difference(repo=repo, source_branch=source_branch, target_branch=target_branch)
if len(commits) == 0:
print(f'No commits to merge from {source_branch} to {target_branch}.')
return
# define distinct prefix in order to support specific pr checks on backports
branch_prefix = 'update' if is_primary_release else 'backport'
# The branch name is based off of the name of branch being merged into
# and the SHA of the branch being merged from. Thus if the branch already
# exists we can assume we don't need to recreate it.
new_branch_name = f'{branch_prefix}-v{version}-{source_branch_short_sha}'
print(f'Branch name is {new_branch_name}.')
# Check if the branch already exists. If so we can abort as this script
# has already run on this combination of branches.
if branch_exists_on_remote(new_branch_name):
print(f'Branch {new_branch_name} already exists. Nothing to do.')
return
# Create the new branch and push it to the remote
print(f'Creating branch {new_branch_name}.')
# The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved
# conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to
# reconstruct the release manually)
conflicted_files = []
if not is_primary_release:
# the source branch will be of the form releases/vN, where N is the major version number
source_branch_major_version = source_branch.strip('releases/v')
# If we're performing a backport, start from the target branch
print(f'Creating {new_branch_name} from the {ORIGIN}/{target_branch} branch')
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{target_branch}')
# Revert the commit that we made as part of the last release that updated the version number and
# changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and
# package.json files when we merge in the v{latest} branch.
# This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we
# use `git log --grep` to conditionally revert the commit.
print('Reverting the version number and changelog updates from the last release to avoid conflicts')
vOlder_update_commits = run_git('log', '--grep', f'^{BACKPORT_COMMIT_MESSAGE}', '--format=%H').split()
if len(vOlder_update_commits) > 0:
print(f' Reverting {vOlder_update_commits[0]}')
# Only revert the newest commit as older ones will already have been reverted in previous
# releases.
run_git('revert', vOlder_update_commits[0], '--no-edit')
# Also revert the "Rebuild" commit, whether created by this script or by the
# `Rebuild Action` workflow.
rebuild_commit = run_git('log', '--grep', f'^{REBUILD_COMMIT_MESSAGE}$', '--format=%H').split()[0]
print(f' Reverting {rebuild_commit}')
run_git('revert', rebuild_commit, '--no-edit')
else:
print(' Nothing to revert.')
print(f'Merging {ORIGIN}/{source_branch} into the release prep branch')
# Commit any conflicts (see the comment for `conflicted_files`)
run_git('merge', f'{ORIGIN}/{source_branch}', allow_non_zero_exit_code=True)
conflicted_files = run_git('diff', '--name-only', '--diff-filter', 'U').splitlines()
if len(conflicted_files) > 0:
run_git('add', '.')
run_git('commit', '--no-edit')
# Migrate the package version number from a vLatest version number to a vOlder version number.
# `package-lock.json` is updated as part of the subsequent rebuild step (see `rebuild_action`).
print(f'Setting version number to {version} in package.json')
replace_version_package_json(get_current_version(), version)
run_git('add', 'package.json')
# Migrate the changelog notes from vLatest version numbers to vOlder version numbers
print(f'Migrating changelog notes from v{source_branch_major_version} to v{target_branch_major_version}')
process_changelog_for_backports(source_branch_major_version, target_branch_major_version)
# Amend the commit generated by `npm version` to update the CHANGELOG
run_git('add', 'CHANGELOG.md')
run_git('commit', '-m', f'{BACKPORT_COMMIT_MESSAGE}{version}')
else:
# If we're performing a standard release, there won't be any new commits on the target branch,
# as these will have already been merged back into the source branch. Therefore we can just
# start from the source branch.
run_git('checkout', '-b', new_branch_name, f'{ORIGIN}/{source_branch}')
print('Updating changelog')
update_changelog(version)
# Create a commit that updates the CHANGELOG
run_git('add', 'CHANGELOG.md')
run_git('commit', '-m', f'Update changelog for v{version}')
if not is_primary_release:
if len(conflicted_files) == 0:
print('Rebuilding the Action.')
rebuild_action()
else:
print(f'Skipping automatic rebuild because the merge produced conflicts in {conflicted_files}.')
run_git('push', ORIGIN, new_branch_name)
# Open a PR to update the branch
open_pr(
repo,
commits,
source_branch_short_sha,
new_branch_name,
source_branch=source_branch,
target_branch=target_branch,
conductor=args.conductor,
is_primary_release=is_primary_release,
conflicted_files=conflicted_files
)
if __name__ == '__main__':
main()
+1 -1
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
+1 -1
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
+1 -1
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
@@ -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
+1 -1
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
+1 -1
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
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
@@ -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
@@ -80,7 +80,7 @@ jobs:
go-version: ${{ inputs.go-version || '>=1.21.0' }}
cache: false
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
+2 -2
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
@@ -80,7 +80,7 @@ jobs:
go-version: ${{ inputs.go-version || '>=1.21.0' }}
cache: false
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
+2 -2
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
@@ -80,7 +80,7 @@ jobs:
go-version: ${{ inputs.go-version || '>=1.21.0' }}
cache: false
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
+2 -2
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
@@ -80,7 +80,7 @@ jobs:
go-version: ${{ inputs.go-version || '>=1.21.0' }}
cache: false
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: npm
+1 -1
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
+1 -1
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@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration
+1 -1
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
+12 -5
View File
@@ -57,15 +57,11 @@ jobs:
version: ${{ matrix.version }}
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- uses: ./../action/init
with:
languages: csharp
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Setup proxy for registries
id: proxy
uses: ./../action/start-proxy
with:
language: java
registry_secrets: |
[
{
@@ -94,5 +90,16 @@ jobs:
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo.maven.apache.org/maven2/')
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo1.maven.org/maven2')
run: exit 1
- uses: ./../action/init
env:
CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }}
CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }}
CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }}
with:
languages: java
tools: ${{ steps.prepare-test.outputs.tools-url }}
config-file: codeql-action@main:tests/multi-language-repo/.github/codeql/custom-queries.yml
env:
CODEQL_ACTION_PROXY_API_REQUESTS: 'true'
CODEQL_ACTION_TEST_MODE: true
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
@@ -57,7 +57,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
@@ -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
+1 -1
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
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # ensure we have all tags and can push commits
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
@@ -91,7 +91,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: npm
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
ref: ${{ env.HEAD_REF }}
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
+1 -1
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
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
python-version: '3.12'
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
+2 -2
View File
@@ -69,7 +69,7 @@ jobs:
run: |
echo SOURCE_BRANCH=${REF_NAME}
echo TARGET_BRANCH=releases/${MAJOR_VERSION}
python .github/update-release-branch.py \
npx tsx ./pr-checks/update-release-branch.ts \
--repository-nwo ${{ github.repository }} \
--source-branch '${{ env.REF_NAME }}' \
--target-branch 'releases/${{ env.MAJOR_VERSION }}' \
@@ -113,7 +113,7 @@ jobs:
run: |
echo SOURCE_BRANCH=${SOURCE_BRANCH}
echo TARGET_BRANCH=${TARGET_BRANCH}
python .github/update-release-branch.py \
npx tsx ./pr-checks/update-release-branch.ts \
--repository-nwo ${{ github.repository }} \
--source-branch ${SOURCE_BRANCH} \
--target-branch ${TARGET_BRANCH} \
@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
+15
View File
@@ -2,6 +2,21 @@
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
## 3.37.2 - 21 Jul 2026
- The new address format for the `config-file` input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the `remote=` prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. [#4023](https://github.com/github/codeql-action/pull/4023)
- The CodeQL Action can now make use of [configured private registries](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries) in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. [#4007](https://github.com/github/codeql-action/pull/4007)
## 3.37.1 - 16 Jul 2026
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956)
- Update default CodeQL bundle version to [2.26.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1). [#4019](https://github.com/github/codeql-action/pull/4019)
## 3.37.0 - 08 Jul 2026
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
- 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)
## 3.36.3 - 01 Jul 2026
No user facing changes.
+4 -4
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.1",
"cliVersion": "2.26.1",
"priorBundleVersion": "codeql-bundle-v2.26.0",
"priorCliVersion": "2.26.0"
}
+2315 -1713
View File
File diff suppressed because it is too large. Load diff
+121 -187
View File
@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.36.3",
"version": "3.37.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.36.3",
"version": "3.37.2",
"license": "MIT",
"workspaces": [
"pr-checks"
@@ -22,18 +22,22 @@
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/plugin-retry": "^8.1.0",
"archiver": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.0.0",
"js-yaml": "^5.2.1",
"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",
"undici": "^6.24.0",
"uuid": "^14.0.1"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
@@ -47,21 +51,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-github": "^6.1.0",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.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.63.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2274,13 +2278,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 +2576,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 +2591,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.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.63.0",
"@typescript-eslint/type-utils": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -2617,7 +2614,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.61.1",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -2633,16 +2630,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.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.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3"
},
"engines": {
@@ -2676,14 +2673,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.61.1",
"@typescript-eslint/types": "^8.61.1",
"@typescript-eslint/tsconfig-utils": "^8.63.0",
"@typescript-eslint/types": "^8.63.0",
"debug": "^4.4.3"
},
"engines": {
@@ -2716,14 +2713,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1"
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2734,9 +2731,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2751,15 +2748,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -2794,9 +2791,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2808,16 +2805,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.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.63.0",
"@typescript-eslint/tsconfig-utils": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/visitor-keys": "8.63.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2846,9 +2843,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 +2890,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1"
"@typescript-eslint/scope-manager": "8.63.0",
"@typescript-eslint/types": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2917,13 +2914,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.63.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/types": "8.63.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -4991,13 +4988,13 @@
}
},
"node_modules/eslint-plugin-github": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.0.0.tgz",
"integrity": "sha512-J8MvUoiR/TU/Y9NnEmg1AnbvMUj9R6IO260z47zymMLLvso7B4c80IKjd8diqmqtSmeXXlbIus4i0SvK84flag==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-6.1.0.tgz",
"integrity": "sha512-+mA0K1/I1JSE9AOiJ4ifMDGu7NplRZX0e3Uy0SjbwyXb2rsDfo5yfT0UCUO+TiOJ/99R1YxFRltizP/PZuB4PQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint/compat": "^1.2.3",
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.14.0",
"@github/browserslist-config": "^1.0.0",
@@ -5014,79 +5011,18 @@
"eslint-plugin-no-only-tests": "^3.0.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-rule-documentation": ">=1.0.0",
"globals": "^16.0.0",
"globals": "^17.7.0",
"jsx-ast-utils": "^3.3.2",
"prettier": "^3.0.0",
"svg-element-attributes": "^1.3.1",
"typescript": "^5.7.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.14.0"
},
"bin": {
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
},
"peerDependencies": {
"eslint": "^8 || ^9"
}
},
"node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz",
"integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"peerDependencies": {
"eslint": "^8.40 || 9"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
}
},
"node_modules/eslint-plugin-github/node_modules/@eslint/core": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/eslint-plugin-github/node_modules/globals": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-github/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
"eslint": "^8 || ^9 || ^10"
}
},
"node_modules/eslint-plugin-i18n-text": {
@@ -5132,13 +5068,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 +6078,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 +6921,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.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
"funding": [
{
"type": "github",
@@ -7451,9 +7386,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 +8302,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 +8839,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": {
@@ -8983,9 +8918,9 @@
}
},
"node_modules/tar": {
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==",
"version": "7.5.20",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
"integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -9173,9 +9108,9 @@
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
"integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9325,16 +9260,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.63.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.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.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/typescript-estree": "8.63.0",
"@typescript-eslint/utils": "8.63.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -9368,10 +9303,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==",
"license": "MIT",
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
"engines": {
"node": ">=18.17"
}
@@ -9498,9 +9432,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,12 +9752,12 @@
"@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": {
"@types/node": "^20.19.43",
"tsx": "^4.22.4"
"tsx": "^4.23.0"
}
}
}
+14 -10
View File
@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "3.36.3",
"version": "3.37.2",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -30,18 +30,22 @@
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/plugin-retry": "^8.1.0",
"archiver": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.0.0",
"js-yaml": "^5.2.1",
"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",
"undici": "^6.24.0"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
@@ -55,21 +59,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-github": "^6.1.0",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.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.63.0"
},
"overrides": {
"@actions/tool-cache": {
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env npx tsx
/**
* Tests for `changelog.ts`.
*/
import * as assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
EMPTY_CHANGELOG,
getReleaseDateString,
processChangelogForBackports,
setVersionAndDate,
} from "./changelog";
const testDate = new Date(2026, 7, 14);
describe("getReleaseDateString", async () => {
await it("formats dates as expected", async () => {
assert.equal(getReleaseDateString(testDate), "14 Aug 2026");
});
});
const emptyChangelogExpected = `# CodeQL Action Changelog
## 9.99.9 - 14 Aug 2026
No user facing changes.
`;
describe("setVersionAndDate", async () => {
await it("replaces the placeholder", async () => {
const result = setVersionAndDate("9.99.9", EMPTY_CHANGELOG, testDate);
assert.equal(result, emptyChangelogExpected);
});
});
const testChangelog = `# CodeQL Action Changelog
## 4.12.3 - 14 Aug 2026
No user facing changes.
`;
const testChangelogResult: string = `# CodeQL Action Changelog
## 3.12.3 - 14 Aug 2026
No user facing changes.
`;
describe("processChangelogForBackports", async () => {
await it("replaces major versions", async () => {
const result = processChangelogForBackports("4", "3", testChangelog);
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
});
});
+135
View File
@@ -0,0 +1,135 @@
import * as fs from "node:fs";
import { CHANGELOG_FILE, DryRunOption } from "./config";
/** Placeholder changelog content for a new release. */
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
## [UNRELEASED]
No user facing changes.
`;
/** Returns `date` formatted as `DD Mon YYYY`. */
export function getReleaseDateString(today: Date = new Date()): string {
return today.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
});
}
export interface OpenChangelogOptions {
initChangelog?: boolean;
}
export function withChangelog(
transformer: (contents: string) => string,
options: DryRunOption & OpenChangelogOptions,
): void {
let content: string;
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
content = EMPTY_CHANGELOG;
} else {
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
}
if (!options.dryRun) {
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
} else {
console.info(`[DRY RUN] Would have written updated changelog.`);
}
}
/**
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
* and today's date.
*/
export function setVersionAndDate(
version: string,
content: string,
date: Date = new Date(),
): string {
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
return content.replace("[UNRELEASED]", versionAndDate);
}
/**
* Processes changelog entries for a backport, converting version references
* from the source major version to the target major version and filtering
* entries that only apply to newer versions.
*/
export function processChangelogForBackports(
sourceBranchMajorVersion: string,
targetBranchMajorVersion: string,
content: string,
): string {
const lines = content.split("\n");
// Changelog entries can use the following format to indicate
// that they only apply to newer versions
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
let output = "";
let i = 0;
// Copy lines until we find the first section heading.
let foundFirstSection = false;
while (!foundFirstSection && i < lines.length) {
let line = lines[i];
if (line.startsWith("## ")) {
line = line.replace(
`## ${sourceBranchMajorVersion}`,
`## ${targetBranchMajorVersion}`,
);
foundFirstSection = true;
}
output += `${line}\n`;
i++;
}
if (!foundFirstSection) {
throw new Error("Could not find any change sections in CHANGELOG.md");
}
// Process remaining lines.
// `foundContent` tracks whether we hit two headings in a row
let foundContent = false;
output += "\n";
while (i < lines.length) {
let line = lines[i];
i++;
// Filter out changelog entries that only apply to newer versions.
const match = someVersionsOnlyRegex.exec(line);
if (match) {
if (
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
) {
continue;
}
}
if (line.startsWith("## ")) {
line = line.replace(
`## ${sourceBranchMajorVersion}`,
`## ${targetBranchMajorVersion}`,
);
if (!foundContent) {
output += "No user facing changes.\n";
}
foundContent = false;
output += `\n${line}\n\n`;
} else {
if (line.trim() !== "") {
foundContent = true;
output += `${line}\n`;
}
}
}
return output;
}
+1 -1
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@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration
+13 -5
View File
@@ -6,16 +6,14 @@ operatingSystems:
- windows
versions:
- linked
env:
CODEQL_ACTION_PROXY_API_REQUESTS: "true"
steps:
- uses: ./../action/init
with:
languages: csharp
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Setup proxy for registries
id: proxy
uses: ./../action/start-proxy
with:
language: java
registry_secrets: |
[
{
@@ -44,3 +42,13 @@ steps:
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo.maven.apache.org/maven2/')
|| !contains(steps.proxy.outputs.proxy_urls, 'https://repo1.maven.org/maven2')
run: exit 1
- uses: ./../action/init
env:
CODEQL_PROXY_HOST: ${{ steps.proxy.outputs.proxy_host }}
CODEQL_PROXY_PORT: ${{ steps.proxy.outputs.proxy_port }}
CODEQL_PROXY_CA_CERTIFICATE: ${{ steps.proxy.outputs.proxy_ca_certificate }}
with:
languages: java
tools: ${{ steps.prepare-test.outputs.tools-url }}
config-file: codeql-action@main:tests/multi-language-repo/.github/codeql/custom-queries.yml
+12
View File
@@ -12,6 +12,12 @@ export const REPO_ROOT = path.join(PR_CHECKS_DIR, "..");
/** The path of the file configuring which checks shouldn't be required. */
export const PR_CHECK_EXCLUDED_FILE = path.join(PR_CHECKS_DIR, "excluded.yml");
/** The path of the main `package.json`. */
export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
/** The path of the changelog. */
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
/** The path to the esbuild metadata file. */
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
@@ -30,3 +36,9 @@ export const API_COMPATIBILITY_FILE = path.join(
SOURCE_ROOT,
"api-compatibility.json",
);
/** A common interface for operations that support dry runs. */
export interface DryRunOption {
/** A value indicating whether to perform operations with side effects. */
dryRun?: boolean;
}
+2 -2
View File
@@ -7,11 +7,11 @@
"@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": {
"@types/node": "^20.19.43",
"tsx": "^4.22.4"
"tsx": "^4.23.0"
}
}
+6 -6
View File
@@ -211,8 +211,8 @@ const languageSetups: LanguageSetups = {
name: "Install Node.js",
uses: pinnedUses(
"actions/setup-node",
"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e",
"v6.4.0",
"820762786026740c76f36085b0efc47a31fe5020",
"v7.0.0",
),
with: {
"node-version": defaultLanguageVersions.javascript,
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
name: "Install Java",
uses: pinnedUses(
"actions/setup-java",
"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}' }}`,
+840
View File
@@ -0,0 +1,840 @@
#!/usr/bin/env npx tsx
/**
* Creates a release preparation branch and opens a PR to merge changes from a
* source branch into a target release branch.
*
* For primary releases this merges `main` into the latest `releases/vN` branch.
* For backports this merges a newer release branch into an older one, handling
* version number and changelog migration automatically.
*
* Usage:
* update-release-branch.ts \
* --repository-nwo github/codeql-action \
* --source-branch main \
* --target-branch releases/v4 \
* --conductor username \
* [--is-primary-release] \
* [--dry-run]
*/
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
import { parseArgs } from "node:util";
import { type ApiClient, getApiClient } from "./api-client";
import * as changelog from "./changelog";
import { DryRunOption, REPO_ROOT } from "./config";
import {
getCurrentVersion,
replaceVersionInPackageJson,
withPackageJson,
} from "./versions";
/**
* NB: This exact commit message is used to find commits for reverting during backports.
* Changing it requires a transition period where both old and new versions are supported.
*/
export const BACKPORT_COMMIT_MESSAGE = "Update version and changelog for v";
/**
* Commit message used for rebuild commits, both those produced by this script and those produced
* by the `Rebuild Action` workflow (`.github/workflows/rebuild.yml`).
*/
export const REBUILD_COMMIT_MESSAGE = "Rebuild";
/** The name of the git remote. */
const ORIGIN = "origin";
/** Environment variables checked (in order) for a GitHub API token. */
const TOKEN_ENVIRONMENT_VARIABLES = ["GH_TOKEN", "GITHUB_TOKEN"] as const;
/** The expected prefix for release branch names. */
const RELEASE_BRANCH_PREFIX = "releases/v";
/**
* Gets a GitHub API token from one of the supported environment variables.
* @throws If none of the supported environment variables is set.
*/
export function getGitHubToken(): string {
for (const name of TOKEN_ENVIRONMENT_VARIABLES) {
const token = process.env[name]?.trim();
if (token) {
return token;
}
}
throw new Error("Missing GitHub token. Set GITHUB_TOKEN or GH_TOKEN.");
}
/** Options for {@link runCommand}. */
export interface RunCommandOptions extends DryRunOption {
/** Options for `execFileSync`. */
execOptions?: ExecFileSyncOptions;
}
/**
* Runs a command, streaming output to the console by default.
*
* @param command The name of the command to run.
* @param args The arguments for the command.
* @throws When the process exits with a non-zero exit code.
* @param options How to run the command.
*/
export function runCommand(
command: string,
args: string[],
options?: RunCommandOptions,
) {
if (!options?.dryRun) {
console.log(`Running \`${command} ${args.join(" ")}\`.`);
return execFileSync(command, args, {
stdio: "inherit",
cwd: REPO_ROOT,
...options?.execOptions,
});
} else {
console.info(
`[DRY RUN] Would have executed '${command} ${args.join(" ")}'`,
);
return "";
}
}
/** Options for {@link runGit}. */
export interface RunGitOptions extends DryRunOption {
/** When true, non-zero exit codes will not throw. */
allowNonZeroExitCode?: boolean;
}
/**
* Runs `git` with the given `args` and returns the stdout.
*
* @param args - Arguments to pass to `git`.
* @param options - Optional settings.
* @throws If `git` does not exit successfully, unless
* `options.allowNonZeroExitCode` is `true`.
* @returns The trimmed stdout output.
*/
export function runGit(args: string[], options?: RunGitOptions): string {
const execOptions: ExecFileSyncOptions = {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
};
try {
const result = runCommand("git", args, {
dryRun: options?.dryRun,
execOptions,
}) as string;
return result.trimEnd();
} catch (error: unknown) {
if (options?.allowNonZeroExitCode) {
// execFileSync throws an object with `stdout` when the process exits
// with a non-zero code.
const execError = error as { stdout?: Buffer | string };
if (typeof execError.stdout === "string") {
return execError.stdout.trimEnd();
}
if (Buffer.isBuffer(execError.stdout)) {
return execError.stdout.toString("utf8").trimEnd();
}
return "";
}
throw error;
}
}
/** Returns true if the given branch exists on the origin remote. */
export function branchExistsOnRemote(branchName: string): boolean {
const result = runGit(["ls-remote", "--heads", ORIGIN, branchName]);
return result !== "";
}
/** Represents commits returned by the GitHub API (relevant fields only). */
export interface GitHubCommit {
sha: string;
commit: { message: string; author: { date?: string } | null };
author: { login: string } | null;
committer: { login: string } | null;
parents: Array<{ sha: string }>;
}
/** Returns true if the commit is an automatic PR merge commit made by GitHub. */
export function isPrMergeCommit(commit: GitHubCommit): boolean {
return commit.committer?.login === "web-flow" && commit.parents.length > 1;
}
/**
* Gets a list of commits on the source branch that are not on the target branch,
* excluding automatic PR merge commits. This will not include any commits that
* exist on the target branch that aren't on the source branch.
*
* Uses `git log` to find the SHAs, then fetches each commit from the GitHub API
* to obtain full metadata (author, parents, associated PRs, etc.).
*
* @param client - An authenticated GitHub API client.
* @param owner - The repository owner.
* @param repo - The repository name.
* @param sourceBranch - The source branch name (without `origin/` prefix).
* @param targetBranch - The target branch name (without `origin/` prefix).
* @returns The list of non-merge commits unique to the source branch.
*/
export async function getCommitDifference(
client: ApiClient,
owner: string,
repo: string,
sourceBranch: string,
targetBranch: string,
): Promise<GitHubCommit[]> {
const logOutput = runGit([
"log",
"--pretty=format:%H",
`${ORIGIN}/${targetBranch}..${ORIGIN}/${sourceBranch}`,
]);
// An empty log output means no commits to merge.
if (logOutput === "") {
return [];
}
const shas = logOutput.split("\n");
// Fetch full commit objects from the API.
console.info(
`Fetching information about ${shas.length} commits from the API...`,
);
const commits: GitHubCommit[] = [];
for (const sha of shas) {
const { data } = await client.rest.repos.getCommit({
owner,
repo,
ref: sha,
});
commits.push(data as GitHubCommit);
}
// Filter out automatic PR merge commits.
return commits.filter((c) => !isPrMergeCommit(c));
}
/** Truncates a commit message for display. */
export function getTruncatedCommitMessage(message: string): string {
const firstLine = message.split("\n")[0];
if (firstLine.length > 60) {
return `${firstLine.slice(0, 57)}...`;
}
return firstLine;
}
/** Represents pull requests associated with a commit (relevant fields only). */
export interface AssociatedPullRequest {
number: number;
user: { login: string; site_admin: boolean } | null;
merge_commit_sha: string | null;
}
/**
* Gets the pull request that introduced a commit to the source branch.
* Returns the earliest PR by number if multiple are associated.
*/
export async function getPrForCommit(
client: ApiClient,
owner: string,
repo: string,
commit: GitHubCommit,
): Promise<AssociatedPullRequest | undefined> {
const prs = await client.paginate(
client.rest.repos.listPullRequestsAssociatedWithCommit,
{
owner,
repo,
commit_sha: commit.sha,
},
);
if (prs.length === 0) {
return undefined;
}
// Return the earliest PR by number.
const sorted = [...prs].sort((a, b) => a.number - b.number);
return sorted[0];
}
/**
* Get the login of the person who merged a pull request.
* Falls back to the commit author of the merge commit.
* For most cases this will be the same as the author, but for PRs opened
* by external contributors getting the merger will get us the GitHub
* employee who reviewed and merged the PR.
*/
export async function getMergerOfPr(
client: ApiClient,
owner: string,
repo: string,
pr: AssociatedPullRequest,
): Promise<string> {
if (!pr.merge_commit_sha) {
return "unknown";
}
const { data: commit } = await client.rest.repos.getCommit({
owner,
repo,
ref: pr.merge_commit_sha,
});
return commit.author?.login ?? "unknown";
}
/**
* Returns the PR author's login if they are GitHub staff (site_admin),
* otherwise undefined.
*/
export function getPrAuthorIfStaff(
pr: AssociatedPullRequest,
): string | undefined {
if (pr.user?.site_admin) {
return pr.user.login;
}
return undefined;
}
/** Parameters for {@link openPr}. */
interface OpenPrParams {
client: ApiClient;
owner: string;
repo: string;
commits: GitHubCommit[];
sourceBranchShortSha: string;
newBranchName: string;
sourceBranch: string;
targetBranch: string;
conductor: string;
isPrimaryRelease: boolean;
conflictedFiles: string[];
dryRun: boolean;
}
/**
* Opens a pull request from the new branch to the target branch and assigns
* the conductor.
*/
export async function openPr(params: OpenPrParams): Promise<void> {
const {
client,
owner,
repo,
commits,
sourceBranchShortSha,
newBranchName,
sourceBranch,
targetBranch,
conductor,
isPrimaryRelease,
conflictedFiles,
dryRun,
} = params;
// Sort the commits into those with and without associated PRs.
const pullRequests: AssociatedPullRequest[] = [];
const commitsWithoutPrs: GitHubCommit[] = [];
console.info(`Finding PRs for ${commits.length} commits...`);
for (const commit of commits) {
const pr = await getPrForCommit(client, owner, repo, commit);
if (!pr) {
commitsWithoutPrs.push(commit);
} else if (!pullRequests.some((p) => p.number === pr.number)) {
pullRequests.push(pr);
}
}
console.log(`Found ${pullRequests.length} pull requests.`);
console.log(
`Found ${commitsWithoutPrs.length} commits not in a pull request.`,
);
// Sort PRs by number (ascending) and commits by date.
pullRequests.sort((a, b) => a.number - b.number);
commitsWithoutPrs.sort((a, b) => {
const dateA = a.commit.author?.date ?? "";
const dateB = b.commit.author?.date ?? "";
return dateA.localeCompare(dateB);
});
// Build the PR body.
const body: string[] = [];
body.push(`Merging ${sourceBranchShortSha} into \`${targetBranch}\`.`);
body.push("");
body.push(`Conductor for this PR is @${conductor}.`);
if (pullRequests.length > 0) {
body.push("");
body.push("Contains the following pull requests:");
for (const pr of pullRequests) {
const displayUser =
getPrAuthorIfStaff(pr) ??
(await getMergerOfPr(client, owner, repo, pr));
body.push(`- #${pr.number} (@${displayUser})`);
}
}
if (commitsWithoutPrs.length > 0) {
body.push("");
body.push("Contains the following commits not from a pull request:");
for (const commit of commitsWithoutPrs) {
const authorDesc = commit.author ? ` (@${commit.author.login})` : "";
body.push(
`- ${commit.sha} - ${getTruncatedCommitMessage(commit.commit.message)}${authorDesc}`,
);
}
}
body.push("");
body.push("Please do the following:");
if (conflictedFiles.length > 0) {
body.push(
" - [ ] Ensure `package.json` file contains the correct version.",
);
body.push(
" - [ ] Add a commit to this branch to resolve the merge conflicts in the following files:",
);
for (const file of conflictedFiles) {
body.push(` - \`${file}\``);
}
body.push(
` - [ ] Rebuild the Action locally (\`npm run build\`) and push any changes to the built output in \`lib\` as a separate commit named exactly \`${REBUILD_COMMIT_MESSAGE}\`.`,
);
body.push(
" - [ ] Ensure another maintainer has reviewed the additional commits you added to this branch to resolve the merge conflicts.",
);
}
body.push(
" - [ ] Ensure the CHANGELOG displays the correct version and date.",
);
body.push(
" - [ ] Ensure the CHANGELOG includes all relevant, user-facing changes since the last release.",
);
body.push(
` - [ ] Check that there are not any unexpected commits being merged into the \`${targetBranch}\` branch.`,
);
body.push(
" - [ ] Ensure the docs team is aware of any documentation changes that need to be released.",
);
body.push(
" - [ ] Approve running the full set of PR checks if you have not pushed any changes.",
);
body.push(
" - [ ] Approve and merge this PR. Make sure `Create a merge commit` is selected rather than `Squash and merge` or `Rebase and merge`.",
);
if (isPrimaryRelease) {
body.push(
" - [ ] Merge the mergeback PR that will automatically be created once this PR is merged.",
);
body.push(
" - [ ] Merge all backport PRs to older release branches, that will automatically be created once this PR is merged.",
);
}
const title = `Merge ${sourceBranch} into ${targetBranch}`;
if (dryRun) {
console.info(`[DRY RUN] Would create PR: "${title}" with body:`);
for (const line of body) {
console.info(`[DRY RUN] > ${line}`);
}
console.info(`[DRY RUN] and assign it to @${conductor}`);
return;
}
// Create the pull request.
const { data: pr } = await client.rest.pulls.create({
owner,
repo,
title,
body: body.join("\n"),
head: newBranchName,
base: targetBranch,
});
console.log(`Created PR #${pr.number}`);
// Assign the conductor.
await client.rest.issues.addAssignees({
owner,
repo,
issue_number: pr.number,
assignees: [conductor],
});
console.log(`Assigned PR to ${conductor}`);
}
interface MainOptions {
dryRun: boolean;
repositoryNwo: string;
sourceBranch: string;
targetBranch: string;
isPrimaryRelease: boolean;
conductor: string;
}
function parseCliOptions(): MainOptions {
const { values } = parseArgs({
options: {
"dry-run": { type: "boolean", default: false },
"repository-nwo": { type: "string" },
"source-branch": { type: "string" },
"target-branch": { type: "string" },
"is-primary-release": { type: "boolean", default: false },
conductor: { type: "string" },
},
strict: true,
});
if (!values["repository-nwo"]) {
throw new Error("--repository-nwo is required");
}
if (!values["source-branch"]) {
throw new Error("--source-branch is required");
}
if (!values["target-branch"]) {
throw new Error("--target-branch is required");
}
if (!values["conductor"]) {
throw new Error("--conductor is required");
}
return {
dryRun: values["dry-run"],
repositoryNwo: values["repository-nwo"],
sourceBranch: values["source-branch"],
targetBranch: values["target-branch"],
isPrimaryRelease: values["is-primary-release"] ?? false,
conductor: values["conductor"],
};
}
/**
* Rebuilds the action (npm ci + npm run build) and commits any changes.
*/
export function rebuildAction(options: MainOptions): void {
// For backports, the only source-level change vs the source branch is the new version number,
// so we just need to refresh the version embedded in `lib/`.
runCommand("npm", ["ci"]);
runCommand("npm", ["run", "build"]);
runGit(["add", "--all"], { dryRun: options.dryRun });
// `git diff --cached --quiet` exits 0 if there are no staged changes.
try {
execFileSync("git", ["diff", "--cached", "--quiet"]);
console.log("Rebuild produced no changes; skipping Rebuild commit.");
} catch {
runGit(["commit", "-m", REBUILD_COMMIT_MESSAGE], {
dryRun: options.dryRun,
});
console.log("Created Rebuild commit.");
}
}
/**
* Prepares the new update/backport branch.
*
* @param options The options we are running with.
* @param newBranchName The name of the new branch to create.
* @param targetBranchMajorVersion The target branch's major version.
* @param version The target version.
*/
export async function prepareNewBranch(
options: MainOptions,
newBranchName: string,
targetBranchMajorVersion: string,
version: string,
): Promise<string[]> {
// The process of creating the v{Older} release can run into merge conflicts. We commit the unresolved
// conflicts so a maintainer can easily resolve them (vs erroring and requiring maintainers to
// reconstruct the release manually)
let conflictedFiles: string[] = [];
if (!options.isPrimaryRelease) {
// For backports, the source branch is also a release branch.
const sourceBranchMajorVersion = options.sourceBranch.replace(
RELEASE_BRANCH_PREFIX,
"",
);
// Start from the target branch.
console.log(
`Creating ${newBranchName} from the ${ORIGIN}/${options.targetBranch} branch`,
);
runGit(
["checkout", "-b", newBranchName, `${ORIGIN}/${options.targetBranch}`],
{ dryRun: options.dryRun },
);
// Revert the commit that we made as part of the last release that updated the version number and
// changelog to refer to {older}.x.x variants. This avoids merge conflicts in the changelog and
// package.json files when we merge in the v{latest} branch.
// This commit will not exist the first time we release the v{N-1} branch from the v{N} branch, so we
// use `git log --grep` to conditionally revert the commit.
console.log(
"Reverting the version number and changelog updates from the last release to avoid conflicts",
);
const vOlderUpdateCommits = runGit([
"log",
"--grep",
`^${BACKPORT_COMMIT_MESSAGE}`,
"--format=%H",
])
.split("\n")
.filter((s) => s !== "");
if (vOlderUpdateCommits.length > 0) {
// Only revert the newest commit as older ones will already have been
// reverted in previous releases.
console.log(` Reverting ${vOlderUpdateCommits[0]}`);
runGit(["revert", vOlderUpdateCommits[0], "--no-edit"], {
dryRun: options.dryRun,
});
// Also revert the "Rebuild" commit, whether created by this script or
// by the `Rebuild Action` workflow.
const rebuildCommits = runGit([
"log",
"--grep",
`^${REBUILD_COMMIT_MESSAGE}$`,
"--format=%H",
])
.split("\n")
.filter((s) => s !== "");
const rebuildCommit = rebuildCommits[0];
console.log(` Reverting ${rebuildCommit}`);
runGit(["revert", rebuildCommit, "--no-edit"], {
dryRun: options.dryRun,
});
} else {
console.log(" Nothing to revert.");
}
// Merge the source branch into the release prep branch.
console.log(
`Merging ${ORIGIN}/${options.sourceBranch} into the release prep branch`,
);
runGit(["merge", `${ORIGIN}/${options.sourceBranch}`], {
allowNonZeroExitCode: true,
dryRun: options.dryRun,
});
conflictedFiles = runGit(["diff", "--name-only", "--diff-filter", "U"])
.split("\n")
.filter((s) => s !== "");
if (conflictedFiles.length > 0) {
runGit(["add", "."], {
dryRun: options.dryRun,
});
runGit(["commit", "--no-edit"], {
dryRun: options.dryRun,
});
}
// Migrate the package version number.
console.log(`Setting version number to '${version}' in package.json`);
withPackageJson((content) => {
const currentPkgVersion = getCurrentVersion(content);
if (currentPkgVersion) {
return {
content: replaceVersionInPackageJson(
currentPkgVersion,
version,
content,
),
value: currentPkgVersion,
};
}
return { value: currentPkgVersion };
}, options);
runGit(["add", "package.json"], {
dryRun: options.dryRun,
});
// Migrate the changelog notes from the source major version to the target.
console.log(
`Migrating changelog notes from v${sourceBranchMajorVersion} to v${targetBranchMajorVersion}`,
);
changelog.withChangelog(
(contents) =>
changelog.processChangelogForBackports(
sourceBranchMajorVersion,
targetBranchMajorVersion,
contents,
),
options,
);
runGit(["add", "CHANGELOG.md"], {
dryRun: options.dryRun,
});
runGit(["commit", "-m", `${BACKPORT_COMMIT_MESSAGE}${version}`], {
dryRun: options.dryRun,
});
} else {
// For a standard (primary) release, there won't be new commits on the
// target branch that aren't already on the source branch, so we can just
// start from the source branch.
runGit(
["checkout", "-b", newBranchName, `${ORIGIN}/${options.sourceBranch}`],
{
dryRun: options.dryRun,
},
);
console.log("Updating changelog");
changelog.withChangelog(
(contents) => changelog.setVersionAndDate(version, contents),
{ ...options, initChangelog: true },
);
runGit(["add", "CHANGELOG.md"], {
dryRun: options.dryRun,
});
runGit(["commit", "-m", `Update changelog for v${version}`], {
dryRun: options.dryRun,
});
}
// For backports, rebuild the action unless there were merge conflicts.
if (!options.isPrimaryRelease) {
if (conflictedFiles.length === 0) {
console.log("Rebuilding the Action.");
rebuildAction(options);
} else {
console.log(
`Skipping automatic rebuild because the merge produced conflicts in: ${conflictedFiles.join(", ")}`,
);
}
}
return conflictedFiles;
}
async function main(): Promise<void> {
const options = parseCliOptions();
const token = getGitHubToken();
const client = getApiClient(token);
if (!options.targetBranch.startsWith(RELEASE_BRANCH_PREFIX)) {
throw new Error(
`Expected target branch to start with '${RELEASE_BRANCH_PREFIX}', but got '${options.targetBranch}'.`,
);
}
if (
!options.isPrimaryRelease &&
!options.sourceBranch.startsWith(RELEASE_BRANCH_PREFIX)
) {
throw new Error(
`Expected source branch to start with '${RELEASE_BRANCH_PREFIX}' for backports, but got '${options.sourceBranch}'.`,
);
}
if (!options.repositoryNwo.includes("/")) {
throw new Error(
`Expected repository name with owner in 'owner/repo' format, but got '${options.repositoryNwo}'`,
);
}
const targetBranchMajorVersion = options.targetBranch.replace(
RELEASE_BRANCH_PREFIX,
"",
);
const currentVersion = withPackageJson((content) => {
return { value: getCurrentVersion(content) };
}, options);
if (!currentVersion) {
throw new Error("Failed to read current version from package.json");
}
const [, vMinor, vPatch] = currentVersion.split(".");
const version = `${targetBranchMajorVersion}.${vMinor}.${vPatch}`;
console.log(
`Considering difference between ${options.sourceBranch} and ${options.targetBranch}...`,
);
const sourceBranchShortSha = runGit([
"rev-parse",
"--short",
`${ORIGIN}/${options.sourceBranch}`,
]);
console.log(
`Current head of ${options.sourceBranch} is ${sourceBranchShortSha}.`,
);
const [owner, repo] = options.repositoryNwo.split("/");
const commits = await getCommitDifference(
client,
owner,
repo,
options.sourceBranch,
options.targetBranch,
);
if (commits.length === 0) {
console.log(
`No commits to merge from ${options.sourceBranch} to ${options.targetBranch}.`,
);
return;
}
// Use a distinct branch prefix to support specific PR checks on backports.
const branchPrefix = options.isPrimaryRelease ? "update" : "backport";
// The branch name is based on the target version and the SHA of the source
// branch head. If the branch already exists we can assume this script has
// already run for this combination.
const newBranchName = `${branchPrefix}-v${version}-${sourceBranchShortSha}`;
console.log(`Branch name is '${newBranchName}'.`);
// Check if the branch already exists. If so we can abort as this script
// has already run on this combination of branches.
if (branchExistsOnRemote(newBranchName)) {
console.log(`Branch '${newBranchName}' already exists. Nothing to do.`);
return;
}
// Prepare the update/backport branch.
const conflictedFiles = await prepareNewBranch(
options,
newBranchName,
targetBranchMajorVersion,
version,
);
// Push the new branch to the remote.
console.log(`Creating branch ${newBranchName}.`);
runGit(["push", ORIGIN, newBranchName], { dryRun: options.dryRun });
// Open a PR to merge the new branch into the target branch.
await openPr({
client,
owner,
repo,
commits,
sourceBranchShortSha,
newBranchName,
sourceBranch: options.sourceBranch,
targetBranch: options.targetBranch,
conductor: options.conductor,
isPrimaryRelease: options.isPrimaryRelease,
conflictedFiles,
dryRun: options.dryRun,
});
}
// Only call `main` if this script was run directly.
if (require.main === module) {
void main();
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env npx tsx
/**
* Tests for `versions.ts`.
*/
import * as assert from "node:assert/strict";
import { describe, it } from "node:test";
import { getCurrentVersion, replaceVersionInPackageJson } from "./versions";
describe("getCurrentVersion", async () => {
await it("reads versions", async () => {
const result = getCurrentVersion(`{ "version": "1.23.4" }`);
assert.deepEqual(result, "1.23.4");
});
});
const packageJsonContents = `{
"name": "codeql",
"version": "1.23.4"
}
`;
const packageJsonContentsExpected = `{
"name": "codeql",
"version": "2.23.4"
}
`;
describe("replaceVersionInPackageJson", async () => {
await it("replaces versions", async () => {
const result = replaceVersionInPackageJson(
"1.23.4",
"2.23.4",
packageJsonContents,
);
assert.deepEqual(
result.split("\n"),
packageJsonContentsExpected.split("\n"),
);
assert.deepEqual(JSON.parse(result), { name: "codeql", version: "2.23.4" });
});
});
+54
View File
@@ -0,0 +1,54 @@
import * as fs from "node:fs";
import { DryRunOption, PACKAGE_JSON } from "./config";
export function withPackageJson<T>(
transformer: (content: string) => { value: T; content?: string },
options: DryRunOption,
): T {
const content = fs.readFileSync(PACKAGE_JSON, "utf8");
const result = transformer(content);
if (result.content !== undefined) {
if (!options.dryRun) {
fs.writeFileSync(PACKAGE_JSON, result.content, "utf8");
} else {
console.info(`[DRY RUN] Would have written an updated package.json`);
}
}
return result.value;
}
/** Reads the current version from `package.json`. */
export function getCurrentVersion(content: string): string | undefined {
const pkg: { version: string } = JSON.parse(content);
return pkg.version;
}
/**
* Replaces the version in `package.json` textually. Only updates the version
* field that immediately follows the `"name": "codeql"` line.
* `npm version` doesn't always work because of merge conflicts, so we
* replace the version in package.json textually.
*/
export function replaceVersionInPackageJson(
prevVersion: string,
newVersion: string,
content: string,
): string {
const lines = content.split("\n");
let prevLineIsCodeql = false;
const output: string[] = [];
for (const line of lines) {
if (prevLineIsCodeql && line.includes(`"version": "${prevVersion}"`)) {
output.push(line.replace(prevVersion, newVersion));
} else {
output.push(line);
}
prevLineIsCodeql = line.includes('"name": "codeql",');
}
return output.join("\n");
}
+104
View File
@@ -0,0 +1,104 @@
import * as core from "@actions/core";
import { ActionsEnv, getActionsEnv } from "./actions-util";
import type { ApiClient } from "./api-client";
import { Env, ReadOnlyEnv } from "./environment";
import type { FeatureEnablement } from "./feature-flags";
import { getActionsLogger, Logger } from "./logging";
import {
ActionName,
getDisplayActionName,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { getEnv, getErrorMessage } from "./util";
/** Base state that is available to an Action on startup. */
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 {
Base: BaseState;
Logger: {
/** The logger that is in use. */
logger: Logger;
};
Env: {
/** Information about environment variables. */
env: Env;
};
ReadOnlyEnv: {
env: ReadOnlyEnv;
};
Actions: {
/** Access to Actions-related functionality. */
actions: ActionsEnv;
};
Api: {
/** A GitHub API client. */
apiClient: ApiClient;
};
FeatureFlags: {
/** Information about enabled feature flags. */
features: FeatureEnablement;
};
}
/** 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 []
? Record<never, never>
: 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<["Base", "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);
}
}
+52 -39
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);
+9 -16
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<["Base", "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();
}
+1 -1
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`
);
})
+54 -9
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, RegistryProxyVars } from "./environment";
import { callee, getTestEnv, setupTests } from "./testing-utils";
import * as util from "./util";
setupTests(test);
@@ -20,23 +21,23 @@ test.serial("getApiClient", async (t) => {
const githubStub: sinon.SinonStub = sinon.stub();
pluginStub.returns(githubStub);
const env = getTestEnv();
env.set(ActionsEnvVars.GITHUB_SERVER_URL, "http://github.localhost");
env.set(ActionsEnvVars.GITHUB_API_URL, "http://api.github.localhost");
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");
api.getApiClient();
const apiClient = api.getApiClient(env);
t.truthy(apiClient);
t.true(githubStub.calledOnce);
t.assert(
githubStub.calledOnceWithExactly({
auth: "token xyz",
baseUrl: "http://api.github.localhost",
log: sinon.match.any,
userAgent: `CodeQL-Action/${actionsUtil.getActionVersion()}`,
request: sinon.match.any,
retry: {
doNotRetry: DO_NOT_RETRY_STATUSES,
},
@@ -206,3 +207,47 @@ test.serial(
}
},
);
test("getRegistryProxy - returns undefined if the proxy is not configured", async (t) => {
const target = callee(api.getRegistryProxy).withArgs();
// Empty environment.
await target.passes(t.is, undefined);
// Only the host.
await target
.withEnv(getTestEnv({ [RegistryProxyVars.PROXY_HOST]: "localhost" }))
.passes(t.is, undefined);
// Only the port.
await target
.withEnv(getTestEnv({ [RegistryProxyVars.PROXY_PORT]: "1234" }))
.passes(t.is, undefined);
});
test("getRegistryProxy - returns value when both vars are set", async (t) => {
await callee(api.getRegistryProxy)
.withArgs()
.withEnv(
getTestEnv({
[RegistryProxyVars.PROXY_HOST]: "localhost",
[RegistryProxyVars.PROXY_PORT]: "1234",
}),
)
.passes(t.truthy);
});
test("getRegistryProxyConfig - gets the configuration from the env vars", async (t) => {
const host = "localhost";
const port = "1234";
const ca = "cert";
await callee(api.getRegistryProxyConfig)
.withArgs()
.withEnv(
getTestEnv({
[RegistryProxyVars.PROXY_HOST]: host,
[RegistryProxyVars.PROXY_PORT]: port,
[RegistryProxyVars.PROXY_CA_CERTIFICATE]: ca,
}),
)
.passes(t.like, { host, port, ca });
});
+99 -9
View File
@@ -1,9 +1,26 @@
import * as core from "@actions/core";
import * as githubUtils from "@actions/github/lib/utils";
import { type Octokit } from "@octokit/core";
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
import * as retry from "@octokit/plugin-retry";
import { RequestRequestOptions } from "@octokit/types";
import {
ProxyAgent,
RequestInfo,
RequestInit,
fetch as undiciFetch,
} from "undici";
import type { ActionState } from "./action-common";
import { getActionVersion, getRequiredInput } from "./actions-util";
import { EnvVar } from "./environment";
import {
ActionsEnvVars,
EnvVar,
ReadOnlyEnv,
RegistryProxyVars,
getEnv,
} from "./environment";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
import {
@@ -43,13 +60,84 @@ export interface GitHubApiExternalRepoDetails {
apiURL: string | undefined;
}
/**
* Gets the configuration for the private registry authentication proxy,
* if it is available in the environment.
*
* @param action The required Action state.
* @returns The hostname, port, and CA retrieved from the corresponding environment variables.
*/
export function getRegistryProxyConfig(action: ActionState<["ReadOnlyEnv"]>) {
return {
host: action.env.getOptional(RegistryProxyVars.PROXY_HOST),
port: action.env.getOptional(RegistryProxyVars.PROXY_PORT),
ca: action.env.getOptional(RegistryProxyVars.PROXY_CA_CERTIFICATE),
};
}
/**
* Gets the configuration for the private registry authentication proxy,
* and uses it to initialise a corresponding `ProxyAgent`.
*
* @param action The required Action state.
* @returns A `ProxyAgent` corresponding to the private registry proxy,
* or `undefined` if we couldn't retrieve the host and port.
*/
export function getRegistryProxy(
action: ActionState<["Logger", "ReadOnlyEnv"]>,
): ProxyAgent | undefined {
const { host, port, ca } = getRegistryProxyConfig(action);
if (host && port) {
const uri = `http://${host}:${port}`;
action.logger.debug(
`Using private registry proxy at '${uri}' for API client.`,
);
return new ProxyAgent({
uri,
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10,
requestTls: ca ? { ca } : undefined,
});
}
return undefined;
}
/**
* Constructs a `RequestRequestOptions` with a custom `fetch` implementation
* that uses `dispatcher` as a proxy for requests.
*
* @param dispatcher The proxy to use.
*/
export function makeProxyRequestOptions(
dispatcher: ProxyAgent,
): RequestRequestOptions {
return {
fetch: (req: RequestInfo, init?: RequestInit) => {
return undiciFetch(req, { ...init, dispatcher });
},
};
}
/** The type of GitHub API client we use. */
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
/** Options for `createApiClientWithDetails`. */
interface CreateApiClientOptions {
allowExternal?: boolean;
proxy?: ProxyAgent;
}
function createApiClientWithDetails(
apiDetails: GitHubApiCombinedDetails,
{ allowExternal = false } = {},
) {
{ allowExternal = false, proxy = undefined }: CreateApiClientOptions = {},
): ApiClient {
const auth =
(allowExternal && apiDetails.externalRepoAuth) || apiDetails.auth;
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry);
const requestOptions =
proxy === undefined ? undefined : makeProxyRequestOptions(proxy);
return new retryingOctokit(
githubUtils.getOctokitOptions(auth, {
baseUrl: apiDetails.apiURL,
@@ -60,6 +148,7 @@ function createApiClientWithDetails(
warn: core.warning,
error: core.error,
},
request: requestOptions,
retry: {
doNotRetry: DO_NOT_RETRY_STATUSES,
},
@@ -67,22 +156,23 @@ 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(
apiDetails: GitHubApiCombinedDetails,
proxy?: ProxyAgent,
) {
return createApiClientWithDetails(apiDetails, { allowExternal: true });
return createApiClientWithDetails(apiDetails, { allowExternal: true, proxy });
}
/**
+1 -1
View File
@@ -1 +1 @@
{"maximumVersion": "3.22", "minimumVersion": "3.16"}
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
+10 -18
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<["Base", "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);
}
+2 -2
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(
+46 -51
View File
@@ -272,17 +272,17 @@ const CODEQL_MINIMUM_VERSION = "2.19.4";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
*/
const CODEQL_NEXT_MINIMUM_VERSION = "2.19.4";
const CODEQL_NEXT_MINIMUM_VERSION = "2.20.7";
/**
* This is the version of GHES that was most recently deprecated.
*/
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.15";
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.16";
/**
* This is the deprecation date for the version of GHES that was most recently deprecated.
*/
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-04-09";
const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
/** The CLI verbosity level to use for extraction in debug mode. */
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
@@ -514,16 +514,13 @@ async function getCodeQLForCmd(
async getVersion() {
let result = util.getCachedCodeQlVersion(cmd);
if (result === undefined) {
const output = await runCli(cmd, ["version", "--format=json"], {
noStreamStdout: true,
});
try {
result = JSON.parse(output) as VersionInfo;
} catch {
throw Error(
`Invalid JSON output from \`version --format=json\`: ${output}`,
);
}
result = await runCliJson<VersionInfo>(
cmd,
["version", "--format=json"],
{
noStreamStdout: true,
},
);
util.cacheCodeQlVersion(cmd, result);
}
return result;
@@ -731,26 +728,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 +757,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 +959,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 +973,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 +1131,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.
*
+495 -62
View File
@@ -6,12 +6,14 @@ import test, { ExecutionContext } from "ava";
import * as yaml from "js-yaml";
import * as sinon from "sinon";
import { ActionState } from "./action-common";
import * as actionsUtil from "./actions-util";
import { AnalysisKind, supportedAnalysisKinds } from "./analyses";
import * as api from "./api-client";
import { CachingKind } from "./caching-utils";
import { createStubCodeQL } from "./codeql";
import { UserConfig } from "./config/db-config";
import * as file from "./config/file";
import * as configUtils from "./config-utils";
import * as errorMessages from "./error-messages";
import { Feature } from "./feature-flags";
@@ -36,6 +38,10 @@ import {
mockCodeQLVersion,
createTestConfig,
makeMacro,
initAllState,
callee,
SAMPLE_DOTCOM_API_DETAILS,
AssertableTarget,
} from "./testing-utils";
import {
GitHubVariant,
@@ -160,8 +166,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 +209,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 +288,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 +330,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 +383,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 +413,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 +436,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 +443,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,
@@ -482,6 +467,24 @@ test.serial("load non-existent input", async (t) => {
});
});
/** A non-empty, but fairly minimal configuration file. */
const simpleConfigFileContents = `
name: my config
queries:
- uses: ./foo_file`;
/** A less minimal configuration file. */
const otherConfigFileContents = `
name: my config
disable-default-queries: true
queries:
- uses: ./foo
paths-ignore:
- a
- b
paths:
- c/d`;
test.serial("load non-empty input", async (t) => {
return await withTmpDir(async (tempDir) => {
setupActionsVars(tempDir, tempDir);
@@ -496,18 +499,6 @@ test.serial("load non-empty input", async (t) => {
},
});
// Just create a generic config object with non-default values for all fields
const inputFileContents = `
name: my config
disable-default-queries: true
queries:
- uses: ./foo
paths-ignore:
- a
- b
paths:
- c/d`;
fs.mkdirSync(path.join(tempDir, "foo"));
const userConfig: UserConfig = {
@@ -534,10 +525,11 @@ test.serial("load non-empty input", async (t) => {
});
const languagesInput = "javascript";
const configFilePath = createConfigFile(inputFileContents, tempDir);
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);
const state = initAllState();
const actualConfig = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
buildModeInput: "none",
@@ -559,14 +551,12 @@ test.serial(
"Using config input and file together, config input should be used.",
async (t) => {
return await withTmpDir(async (tempDir) => {
process.env["RUNNER_TEMP"] = tempDir;
process.env["GITHUB_WORKSPACE"] = tempDir;
setupActionsVars(tempDir, tempDir);
const inputFileContents = `
name: my config
queries:
- uses: ./foo_file`;
const configFilePath = createConfigFile(inputFileContents, tempDir);
const configFilePath = createConfigFile(
simpleConfigFileContents,
tempDir,
);
const configInput = `
name: my config
@@ -595,8 +585,9 @@ test.serial(
// Only JS, python packs will be ignored
const languagesInput = "javascript";
const state = initAllState({ env: util.getEnv() });
const config = await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
languagesInput,
configFile: configFilePath,
@@ -647,8 +638,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 +661,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 +692,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 +723,10 @@ test.serial("No detected languages", async (t) => {
},
});
const state = initAllState();
try {
await configUtils.initConfig(
createFeatures([]),
state,
createTestInitConfigInputs({
tempDir,
codeql,
@@ -752,9 +747,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,
@@ -2272,3 +2268,440 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
{ exclude: { tags: "exclude-from-incremental" } },
]);
});
test("determineUserConfig - empty config when neither input is specified", async (t) => {
await withTmpDir(async (tmpDir) => {
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withFeatures([])
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: undefined,
configFile: undefined,
workspacePath: tmpDir,
}),
);
// The returned configuration should be empty.
await target
// The fact that no configuration was provided should have been logged,
.logs(t, "No configuration file was provided")
// But not the messages for the two input sources
// or the warning about both inputs.
.notLogs(
t,
"Using config from action input:",
"Using configuration file:",
"Both a config file and config input were provided. Ignoring config file.",
)
.passes(t.deepEqual, {});
});
});
test("determineUserConfig - loads config file", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
const inputs = createTestInitConfigInputs({
configInput: undefined,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);
await target
// The path of the input config file should have been logged,
.logs(t, `Using configuration file: ${configFilePath}`)
.notLogs(
t,
// The other two origin messages and the warning about both inputs should
// not have been logged.
"No configuration file was provided",
"Using config from action input:",
"Both a config file and config input were provided. Ignoring config file.",
)
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
// The `configFile` input should not have changed.
t.is(inputs.configFile, configFilePath);
});
});
test("determineUserConfig - loads config input", async (t) => {
await withTmpDir(async (tmpDir) => {
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
const inputs = createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: undefined,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);
await target
// The input source and path of the generated config file should have been logged.
.logs(
t,
"Using config from action input:",
`Using configuration file: ${expectedConfigPath}`,
)
// The message about no configuration input and
// the warning about both inputs should not have been logged.
.notLogs(
t,
"No configuration file was provided",
"Both a config file and config input were provided. Ignoring config file.",
)
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
// The `configFile` input should have been mutated to the generated path.
t.is(inputs.configFile, expectedConfigPath);
});
});
test("determineUserConfig - ignores config file input when both specified", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
const inputs = createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);
await target
// The path of the generated config file and
// the warning about both inputs should have been logged.
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
// The `configFile` input should have been mutated to the generated path.
t.is(inputs.configFile, expectedConfigPath);
});
});
/** A `config` input that we might get from Default Setup. */
const defaultSetupConfigInput = `
threat-models: [local, remote]
default-setup:
org:
model-packs: [foo, bar]`;
test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
const inputs = createTestInitConfigInputs({
configInput: defaultSetupConfigInput,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
.withFeatures([Feature.AllowMergeConfigFiles])
.withArgs(tmpDir, inputs);
// The loaded configuration should match the result of merging
// `defaultSetupConfigInput` and `simpleConfigFileContents`.
const expectedConfig = {
name: "my config",
queries: [{ uses: "./foo_file" }],
"threat-models": ["local", "remote"],
"default-setup": {
org: {
"model-packs": ["foo", "bar"],
},
},
} satisfies UserConfig;
await target
.logs(
t,
`Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`,
)
.notLogs(
t,
`Using configuration file: ${expectedConfigPath}`,
"No configuration file was provided",
`Using config from action input: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.passes(t.deepEqual, expectedConfig);
// The `configFile` input should have been mutated to the generated path.
t.is(inputs.configFile, expectedConfigPath);
// Since `result` is the result of merging the configurations in-memory,
// also check whether loading the configuration from disk that was written
// by `determineUserConfig` matches our expectations.
const loadedFromDisk = configUtils.getLocalConfig(
getRunnerLogger(true),
expectedConfigPath,
false,
);
t.deepEqual(loadedFromDisk, expectedConfig);
});
});
test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
}),
);
await target
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
});
});
test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withFeatures([Feature.AllowMergeConfigFiles])
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
}),
);
await target
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
});
});
test("loadUserConfig - loads local configuration files", async (t) => {
await withTmpDir(async (workspaceDir) => {
await withTmpDir(async (tmpDir) => {
// Construct the test target.
const loadUserConfig = (
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
filePath: string,
) =>
configUtils.loadUserConfig(
actionState,
filePath,
workspaceDir,
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
);
const target = callee(loadUserConfig);
// `loadUserConfig` should load local configuration files if they are inside the workspace:
const insideOfWorkspace = path.join(workspaceDir, "some-file.yml");
fs.writeFileSync(insideOfWorkspace, "test-key: present", "utf8");
await target
.withArgs(insideOfWorkspace)
.passes(t.deepEqual, { "test-key": "present" });
// `loadUserConfig` should normally throw if the path is outside of the workspace:
const outsideOfWorkspace = path.join(
tmpDir,
"not-the-generated-file.yml",
);
fs.writeFileSync(outsideOfWorkspace, "test-key: present", "utf8");
await target
.withArgs(outsideOfWorkspace)
.throws(t, { instanceOf: ConfigurationError });
// `loadUserConfig` does not throw if the path is the result of `userConfigFromActionPath`:
const generatedPath = configUtils.userConfigFromActionPath(tmpDir);
fs.writeFileSync(generatedPath, "test-key: present", "utf8");
await target
.withArgs(generatedPath)
.passes(t.deepEqual, { "test-key": "present" });
});
});
});
test.serial("loadUserConfig - loads remote configuration files", async (t) => {
await withTmpDir(async (tmpDir) => {
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
const remoteAddress = "owner/repo/file@ref";
await callee(configUtils.loadUserConfig)
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
.passes(t.deepEqual, {});
t.true(
getRemoteConfig.calledOnceWithExactly(
sinon.match.any,
remoteAddress,
SAMPLE_DOTCOM_API_DETAILS,
),
);
});
});
test.serial(
"loadUserConfig - loads remote configuration files (new format, partial)",
async (t) => {
await withTmpDir(async (tmpDir) => {
const getRemoteConfig = sinon.stub(file, "getRemoteConfig").resolves({});
// Construct the basic test target.
const target = callee(configUtils.loadUserConfig).withDefaultActionsEnv();
// Utility function to assert that `targetWithArgs` has identified
// the input as a remote file address.
const checkIsRemote =
(address: string) =>
async <R>(targetWithArgs: AssertableTarget<R>) => {
// We have stubbed `getRemoteConfig` to resolve to `{}`, so we
// expect that result.
await targetWithArgs.passes(t.deepEqual, {});
// And `getRemoteConfig` should have been called exactly once.
t.is(getRemoteConfig.callCount, 1);
// Get the arguments for the call and check that there were three.
// We don't care about the first, but check that the other two
// match our expectations. We break it down like this to get
// more useful test output.
const args = getRemoteConfig.getCalls()[0].args;
t.is(args.length, 3);
t.deepEqual(args[1], address);
t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS);
};
// Utility function to assert that `targetWithArgs` has not identified
// the input as a remote file address.
const checkIsNotRemote = async <R>(
targetWithArgs: AssertableTarget<R>,
) => {
// We expect `loadUserConfig` to have thrown if it thinks the path is local,
// since the inputs we provide aren't for files that exist.
await targetWithArgs.throws(t);
// Additionally, we expect that `getRemoteConfig` wasn't called.
t.is(getRemoteConfig.callCount, 0);
};
// Utility function to add the explicit `REMOTE_PATH_PREFIX` to the input.
const withExplicitPrefix = (str: string) =>
`${file.REMOTE_PATH_PREFIX}${str}`;
// Utility to set up a call to `loadUserConfig` with the provided `address`
// and pass it to `assertion`.
const testTargetWith = async (
address: string,
assertion: (
targetWithArgs: AssertableTarget<Promise<UserConfig>>,
) => Promise<any>,
) => {
// Reset the stub's history since we re-use it.
getRemoteConfig.resetHistory();
// Log the input we are testing so that, in the event of a failure,
// it is easier to see which input was responsible.
t.log(`testTargetWith("${address}")`);
// Prepare the test call to `loadUserConfig`.
const targetWithArgs = target.withArgs(
address,
tmpDir,
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
);
// Pass it to the provided assertion function.
await assertion(targetWithArgs);
};
// Since this input contains an '@' character, it is treated as a remote path
// by the old logic even without the explicit prefix.
const remoteWithoutPrefix = "repo@main";
await testTargetWith(
remoteWithoutPrefix,
checkIsRemote(remoteWithoutPrefix),
);
await testTargetWith(
withExplicitPrefix(remoteWithoutPrefix),
checkIsRemote(remoteWithoutPrefix),
);
// It is only treated as a local path with the corresponding prefix.
await testTargetWith(`./${remoteWithoutPrefix}`, checkIsNotRemote);
// The following test inputs are examples of ambiguous paths. They could refer to
// valid local or remote paths. For each, we check that they are treated as remote
// paths if the explicit remote file prefix is used and as local paths otherwise.
const testInputs = ["repo:file", "input", "../input"];
for (const testInput of testInputs) {
for (const addPrefix of [true, false]) {
await testTargetWith(
addPrefix ? withExplicitPrefix(testInput) : testInput,
addPrefix ? checkIsRemote(testInput) : checkIsNotRemote,
);
}
}
});
},
);
+198 -254
View File
@@ -5,10 +5,12 @@ 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,
isAnalyzingPullRequest,
isDefaultSetup,
isDynamicWorkflow,
} from "./actions-util";
import {
@@ -18,15 +20,27 @@ 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,
generateCodeScanningConfig,
mergeDefaultSetupAndUserConfigs,
parseUserConfig,
UserConfig,
} from "./config/db-config";
import {
getRemoteConfig,
LOCAL_PATH_PREFIX,
REMOTE_PATH_PREFIX,
} from "./config/file";
import {
parseRegistries,
type RegistryConfigNoCredentials,
type RegistryConfigWithCredentials,
} from "./config/pack-registries";
import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
@@ -79,6 +93,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 +133,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,
@@ -598,13 +472,22 @@ async function downloadCacheWithTime(
return { trapCaches, trapCacheDownloadTime };
}
async function loadUserConfig(
logger: Logger,
/**
* Loads a CLI configuration file from `configFile`.
*
* @param actionState The Action state.
* @param configFile The address of the configuration file.
* @param workspacePath The workspace path, used to check that the configuration file exists relative to it.
* @param apiDetails Information for how to access the API to fetch remote files.
* @param tempDir The temporary directory which may contain a CodeQL Action-generated configuration file.
* @returns The loaded configuration file, if successful.
*/
export async function loadUserConfig(
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
configFile: string,
workspacePath: string,
apiDetails: api.GitHubApiCombinedDetails,
tempDir: string,
validateConfig: boolean,
): Promise<UserConfig> {
if (isLocal(configFile)) {
if (configFile !== userConfigFromActionPath(tempDir)) {
@@ -617,14 +500,18 @@ 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 {
// Drop the explicit prefix if it is present. Since `REMOTE_PATH_PREFIX` is chosen
// to not conflict with permissible characters in "owner" or "repo" components,
// this does not risk removing valid parts of either component by accident.
if (isExplicitRemotePath(configFile)) {
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
}
return await getRemoteConfig(actionState, configFile, apiDetails);
}
}
@@ -1071,7 +958,11 @@ function dbLocationOrDefault(
return dbLocation || path.resolve(tempDir, "codeql_databases");
}
function userConfigFromActionPath(tempDir: string): string {
/**
* Gets the path for the CodeQL Action-generated configuration file,
* which is used to store the `config` input.
*/
export function userConfigFromActionPath(tempDir: string): string {
return path.resolve(tempDir, "user-config-from-action.yml");
}
@@ -1131,6 +1022,110 @@ export async function applyIncrementalAnalysisSettings(
}
}
/**
* Determines where to load the `UserConfig` for the CLI from and loads it.
*
* @param inputs The Action inputs. The `configFile` value will be mutated
* if a CodeQL Action-generated file should be used.
*
* @returns The loaded `UserConfig`, which might be empty if no configuration
* was specified.
*/
export async function determineUserConfig(
action: ActionState<["Logger", "Env", "FeatureFlags"]>,
tempDir: string,
inputs: InitConfigInputs,
): Promise<UserConfig> {
const validateConfig = await action.features.getValue(
Feature.ValidateDbConfig,
);
// We have the following cases:
// 1. A `config` or `config-file` input is provided, but not both: use the provided one.
// 2. Both are provided and we are in an advanced workflow: ignore the `config-file` input.
// 3. Both are provided and we are in Default Setup: the `config` input uses a limited
// set of options, which are supported by `mergeDefaultSetupAndUserConfigs`,
// and we merge the two configs.
if (inputs.configInput) {
const computedConfigPath = userConfigFromActionPath(tempDir);
// Get a function which enables us to determine whether the FF that allows us to
// merge supported configuration file properties is enabled. We only execute
// this lazily if the other checks pass.
const allowMergeConfigs = () =>
action.features.getValue(Feature.AllowMergeConfigFiles);
// Check whether we also have a `config-file` input and decide what to do.
if (
inputs.configFile &&
isDefaultSetup(action.env) &&
(await allowMergeConfigs())
) {
// If the FF is enabled and we are in Default Setup, combine the supported
// configuration file properties and write the result to disk.
const fromConfigInput = parseUserConfig(
action.logger,
"`config` input",
inputs.configInput,
validateConfig,
);
const fromConfigFile = await loadUserConfig(
action,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
);
// Write the merged configuration to disk so that it can be loaded subsequently by
// the CLI or other CodeQL Action steps.
const mergedConfig = mergeDefaultSetupAndUserConfigs(
action.logger,
fromConfigInput,
fromConfigFile,
);
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
action.logger.debug(
`Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`,
);
inputs.configFile = computedConfigPath;
return mergedConfig;
} else {
// If we are in this branch and there is a `config-file` input, then it means
// we didn't meet the conditions for merging the configurations. Warn the user
// that the configuration file will be ignored.
if (inputs.configFile) {
action.logger.warning(
`Both a config file and config input were provided. Ignoring config file.`,
);
}
// Write the `config` input straight to disk.
fs.writeFileSync(computedConfigPath, inputs.configInput);
inputs.configFile = computedConfigPath;
action.logger.debug(
`Using config from action input: ${inputs.configFile}`,
);
}
}
// Load whatever configuration file we have, if any.
if (!inputs.configFile) {
action.logger.debug("No configuration file was provided");
return {};
} else {
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
return await loadUserConfig(
action,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
);
}
}
/**
* Load and return the config.
*
@@ -1138,38 +1133,13 @@ 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) {
if (inputs.configFile) {
logger.warning(
`Both a config file and config input were provided. Ignoring config file.`,
);
}
inputs.configFile = userConfigFromActionPath(tempDir);
fs.writeFileSync(inputs.configFile, inputs.configInput);
logger.debug(`Using config from action input: ${inputs.configFile}`);
}
let userConfig: UserConfig = {};
if (!inputs.configFile) {
logger.debug("No configuration file was provided");
} else {
logger.debug(`Using configuration file: ${inputs.configFile}`);
const validateConfig = await features.getValue(Feature.ValidateDbConfig);
userConfig = await loadUserConfig(
logger,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
validateConfig,
);
}
const userConfig = await determineUserConfig(actionState, tempDir, inputs);
const config = await initActionState(inputs, userConfig);
@@ -1317,39 +1287,61 @@ 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.",
);
}
/**
* Determines if `configPath` is explicitly local. That is, it starts with `LOCAL_PATH_PREFIX`.
* A configuration file path that starts with `LOCAL_PATH_PREFIX` is always treated as a local path.
*
* @param configPath The path to test.
*/
function isExplicitLocalPath(configPath: string): boolean {
return configPath.startsWith(LOCAL_PATH_PREFIX);
}
export function parseRegistriesWithoutCredentials(
registriesInput?: string,
): RegistryConfigNoCredentials[] | undefined {
return parseRegistries(registriesInput)?.map((r) => {
const { url, packages, kind } = r;
return { url, packages, kind };
});
/**
* Determines if `configPath` starts with the prefix used to explicitly mark a path
* as a remote path (`REMOTE_PATH_PREFIX`).
*
* @param configPath The path to test.
*/
function isExplicitRemotePath(configPath: string): boolean {
return configPath.startsWith(REMOTE_PATH_PREFIX);
}
/**
* Determines if `configPath` contains a '@' character.
*
* @param configPath The path to test.
*/
function containsAtRef(configPath: string): boolean {
return configPath.includes("@");
}
/**
* Determines if `configPath` refers to a local configuration file.
*
* @param configPath The path to test.
* @returns True if it is local, or false otherwise.
*/
function isLocal(configPath: string): boolean {
// If the path starts with ./, look locally
if (configPath.indexOf("./") === 0) {
// If the path starts with `LOCAL_PATH_PREFIX`, it is explicitly local.
// This allows local paths that would otherwise contain '@'
// to be used with a `LOCAL_PATH_PREFIX` prefix.
if (isExplicitLocalPath(configPath)) {
return true;
}
// If the path starts with `REMOTE_PATH_PREFIX`, it is explicitly remote.
// This allows users to resolve ambiguity by specifying `REMOTE_PATH_PREFIX`.
if (isExplicitRemotePath(configPath)) {
return false;
}
return configPath.indexOf("@") === -1;
// Otherwise, the path is also local if it does not contain '@'.
// This assumes the `OLD_REMOTE_ADDRESS_FORMAT` which must contain a '@'
// character for remote addresses.
return !containsAtRef(configPath);
}
function getLocalConfig(
export function getLocalConfig(
logger: Logger,
configFile: string,
validateConfig: boolean,
@@ -1369,54 +1361,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
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;
}
+137
View File
@@ -8,6 +8,7 @@ import {
getRecordingLogger,
LoggedMessage,
makeMacro,
RecordingLogger,
} from "../testing-utils";
import { ConfigurationError, prettyPrintPack } from "../util";
@@ -488,3 +489,139 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail,
),
);
});
test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{ "threat-models": ["a", "b"] },
{ "threat-models": ["local", "remote"] },
);
const threatModels = result["threat-models"];
if (t.truthy(threatModels)) {
t.deepEqual(threatModels, ["a", "b", "local", "remote"]);
}
});
test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{},
{ "default-setup": {} },
);
// User-supplied value is ignored.
t.deepEqual(result, {});
// Warning is logged.
t.true(
logger.hasMessage(
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
),
);
});
test("mergeDefaultSetupAndUserConfigs - keeps default setup key from 'config' input", async (t) => {
const logger = new RecordingLogger();
const expected: dbConfig.DefaultSetupConfig = {
org: { "model-packs": ["some-pack"] },
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{ "default-setup": expected },
{},
);
// Result matches the input.
t.deepEqual(result["default-setup"], expected);
// No warning is logged.
t.false(
logger.hasMessage(
"The 'default-setup' configuration key is not supported in user-supplied configuration files",
),
);
});
test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplied configuration", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{},
configFile,
);
t.deepEqual(result, configFile);
});
test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {
"query-filters": [{ exclude: { a: "b" } }],
"paths-ignore": ["path"],
};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
borg: [],
org: {
unknown: "foo",
"model-packs": [],
},
} as unknown as dbConfig.DefaultSetupConfig,
"paths-ignore": ["other-path"],
},
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [] } },
});
const expectedUnrecognisedKeys = [
".default-setup.org.unknown",
".default-setup.borg",
".paths-ignore",
].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`,
]);
});
test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => {
const logger = new RecordingLogger();
const configFile: dbConfig.UserConfig = {};
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
logger,
{
"default-setup": {
org: {
"model-packs": [123],
},
} as unknown as dbConfig.DefaultSetupConfig,
},
configFile,
);
t.deepEqual(result, {
...configFile,
"default-setup": { org: { "model-packs": [123] } },
});
const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", ");
checkExpectedLogMessages(t, logger.messages, [
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
+133
View File
@@ -4,11 +4,16 @@ import * as yaml from "js-yaml";
import * as jsonschema from "jsonschema";
import * as semver from "semver";
import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
} from "../diagnostics";
import * as errorMessages from "../error-messages";
import {
RepositoryProperties,
RepositoryPropertyName,
} from "../feature-flags/properties";
import * as json from "../json";
import { Language } from "../languages";
import { Logger } from "../logging";
import { cloneObject, ConfigurationError, prettyPrintPack } from "../util";
@@ -28,6 +33,21 @@ export interface QuerySpec {
uses: string;
}
const ORG_SCHEMA = {
/** An array of model pack names. */
"model-packs": json.optional(json.array(json.string)),
} as const satisfies json.Schema;
/** Not intended to be provided directly by a user. */
export type OrgType = json.FromSchema<typeof ORG_SCHEMA>;
const DEFAULT_SETUP_SCHEMA = {
org: json.optional<OrgType>(json.object(ORG_SCHEMA)),
} as const satisfies json.Schema;
/** Not intended to be provided directly by a user. */
export type DefaultSetupConfig = json.FromSchema<typeof DEFAULT_SETUP_SCHEMA>;
/**
* Format of the config file supplied by the user.
*/
@@ -46,6 +66,119 @@ export interface UserConfig {
// Set of query filters to include and exclude extra queries based on
// codeql query suite `include` and `exclude` properties
"query-filters"?: QueryFilter[];
/** An array (possibly empty or absent) of threat models to use. */
"threat-models"?: string[];
/**
* Configuration options that are reserved for us in Default Setup and
* not intended to be supplied directly by users.
*/
"default-setup"?: DefaultSetupConfig;
}
/** A subset of the `UserConfig` schema that is used by Default Setup. */
const DEFAULT_SETUP_CONFIG_SCHEMA = {
"threat-models": json.optional(json.array(json.string)),
"default-setup": json.optional<DefaultSetupConfig>(
json.object(DEFAULT_SETUP_SCHEMA),
),
} as const satisfies json.Schema;
/**
* Merges supported properties from two configuration files. This is intended only for
* use with merging the `config` input provided by Default Setup with a potentially
* richer configuration file provided by a user.
*
* @param logger The logger to use.
* @param fromConfigInput The configuration from Default Setup.
* @param fromConfigFile The user-supplied configuration.
* @returns The combination of both configuration files.
*/
export function mergeDefaultSetupAndUserConfigs(
logger: Logger,
fromConfigInput: UserConfig,
fromConfigFile: UserConfig,
): UserConfig {
logger.debug(
"Combining configuration files from 'config' and 'config-file' inputs",
);
// Check for unexpected keys in the configuration from the `config` input
// that was provided by Default Setup. This should only contain the keys
// we would expect to receive from Default Setup.
const schemaCheckResult = json.checkSchema(
DEFAULT_SETUP_CONFIG_SCHEMA,
fromConfigInput as json.UnvalidatedObject<any>,
);
// Report any invalid or unrecognised keys.
if (schemaCheckResult.invalidKeys.length > 0) {
logger.warning(
`Invalid keys in Default Setup configuration: ${schemaCheckResult.invalidKeys.join(", ")}`,
);
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/invalid-default-setup-config-keys",
"Invalid Default Setup configuration keys",
{
invalidKeys: schemaCheckResult.invalidKeys,
},
["internal-error"],
),
);
}
if (schemaCheckResult.unknownKeys.length > 0) {
logger.warning(
`Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`,
);
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/unrecognised-default-setup-config-keys",
"Unrecognised Default Setup configuration keys",
{
unrecognisedKeys: schemaCheckResult.unknownKeys,
},
["internal-error"],
),
);
}
// Combine all specified threat models from both sources.
const threatModels = new Set(fromConfigInput["threat-models"] || []);
for (const configFileThreatModel of fromConfigFile["threat-models"] || []) {
threatModels.add(configFileThreatModel);
}
// Warn if there is a 'default-setup' configuration key in the user-supplied configuration,
// since it is not meant to be used and we therefore ignore it here.
if (fromConfigFile["default-setup"]) {
logger.warning(
`The 'default-setup' configuration key is not supported in user-supplied configuration files and will be ignored.`,
);
}
// Since we expect the `fromConfigInput` configuration to be provided by Default Setup,
// we expect a limited set of options. Therefore, we base the overall configuration on
// the one provided via the `config-file` input, which may be richer.
const result = { ...fromConfigFile };
delete result["threat-models"];
delete result["default-setup"];
if (fromConfigInput["default-setup"]?.org?.["model-packs"]) {
result["default-setup"] = {
org: {
"model-packs": fromConfigInput["default-setup"].org["model-packs"],
},
};
}
if (threatModels.size > 0) {
result["threat-models"] = Array.from(threatModels);
}
return result;
}
/**
+96 -72
View File
@@ -1,22 +1,26 @@
import * as github from "@actions/github";
import test from "ava";
import sinon from "sinon";
import * as api from "../api-client";
import { RegistryProxyVars } from "../environment";
import { Feature } from "../feature-flags";
import { RepositoryPropertyName } from "../feature-flags/properties";
import {
getTestActionsEnv,
RecordingLogger,
callee,
SAMPLE_DOTCOM_API_DETAILS,
setupTests,
} from "../testing-utils";
import { getConfigFileInput } from "./file";
import { getConfigFileInput, getRemoteConfig } from "./file";
setupTests(test);
test("getConfigFileInput returns undefined by default", async (t) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
const result = getConfigFileInput(logger, actionsEnv, {}, true);
t.is(result, undefined);
await callee(getConfigFileInput)
.withArgs({})
.withFeatures([Feature.ConfigFileRepositoryProperty])
.passes(t.is, undefined);
});
const repositoryProperties = {
@@ -24,86 +28,106 @@ 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,
true,
);
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,
true,
);
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]: " ",
},
true,
);
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) => {
const logger = new RecordingLogger();
const actionsEnv = getTestActionsEnv();
// Since the FF is off, we should ignore the repository property value.
const result = getConfigFileInput(
logger,
actionsEnv,
repositoryProperties,
false,
);
t.is(result, undefined);
t.false(
logger.hasMessage(
"Using configuration file input from repository property",
),
);
t.true(
logger.hasMessage(
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);
});
test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => {
const client = github.getOctokit("123");
const response = {
data: {
content: Buffer.from("disable-default-queries: false").toString("base64"),
},
};
sinon
.stub(client.rest.repos, "getContent")
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.resolves(response as any);
// We stub `getApiClientWithExternalAuth` so that it throws if no
// proxy is provided and returns the client otherwise. This allows us
// to verify the result in the following test cases.
const errorMessage = "No `proxy` was provided by the caller.";
sinon
.stub(api, "getApiClientWithExternalAuth")
.callsFake((_details, proxy) => {
// Throw if proxy isn't defined.
if (proxy === undefined) {
throw new Error(errorMessage);
}
// Otherwise return the client object.
return client;
});
const target = callee(getRemoteConfig)
.withDefaultActionsEnv()
.withArgs("file.yml", SAMPLE_DOTCOM_API_DETAILS);
// Should use it when the FF is enabled and the environment variables are set.
await target
.withFeatures([Feature.ProxyApiRequests])
.withEnv((env) => {
env.set(RegistryProxyVars.PROXY_HOST, "localhost");
env.set(RegistryProxyVars.PROXY_PORT, "1234");
})
.logs(t, "Using private registry proxy at 'http://localhost:1234'")
.passes(t.truthy);
// But not when the FF is not enabled.
await target
.withEnv((env) => {
env.set(RegistryProxyVars.PROXY_HOST, "localhost");
env.set(RegistryProxyVars.PROXY_PORT, "1234");
})
.notLogs(t, "Using private registry proxy at 'http://localhost:1234'")
.throws(t, { message: errorMessage });
// And not when the environment variables aren't set.
await target
.withFeatures([Feature.ProxyApiRequests])
.notLogs(t, "Using private registry proxy at 'http://localhost:1234'")
.throws(t, { message: errorMessage });
});
+88 -7
View File
@@ -1,19 +1,40 @@
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";
/**
* The prefix that can be specified to indicate that a path should be treated as a local file address.
*/
export const LOCAL_PATH_PREFIX = "./";
/**
* The prefix that can be specified to indicate that a path should be treated as a remote file address.
* The new remote file address format must start with either an owner or repository name. Both
* are restricted to ASCII characters, '.', and '-'. The prefix chosen here does not interfere with
* those (since it contains an `=`) and is _unlikely_ (but not impossible) to appear in a local file path.
*/
export const REMOTE_PATH_PREFIX = "remote=";
/**
* Gets the value that is configured for the configuration file, if any.
*/
export function getConfigFileInput(
logger: Logger,
actions: ActionsEnv,
export async function getConfigFileInput(
{
logger,
actions,
features,
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
repositoryProperties: Partial<RepositoryProperties>,
useRepositoryProperty: boolean,
): string | undefined {
): Promise<string | undefined> {
const input = actions.getOptionalInput("config-file");
if (input !== undefined) {
@@ -26,6 +47,10 @@ export function getConfigFileInput(
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
// Only use the repository property value if the FF is enabled.
const useRepositoryProperty = await features.getValue(
Feature.ConfigFileRepositoryProperty,
);
if (useRepositoryProperty) {
logger.info(
`Using configuration file input from repository property: ${propertyValue}`,
@@ -40,3 +65,59 @@ export function getConfigFileInput(
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 shouldProxyRequest = await actionState.features.getValue(
Feature.ProxyApiRequests,
);
const proxy = shouldProxyRequest
? api.getRegistryProxy(actionState)
: undefined;
const response = await api
.getApiClientWithExternalAuth(apiDetails, proxy)
.rest.repos.getContent({
owner: address.owner,
repo: address.repo,
path: address.path,
ref: address.ref,
});
let fileContents: string;
if ("content" in response.data && response.data.content !== undefined) {
fileContents = response.data.content;
} else if (Array.isArray(response.data)) {
throw new ConfigurationError(
errorMessages.getConfigFileDirectoryGivenMessage(configFile),
);
} else {
throw new ConfigurationError(
errorMessages.getConfigFileFormatInvalidMessage(configFile),
);
}
const validateConfig = await actionState.features.getValue(
Feature.ValidateDbConfig,
);
return parseUserConfig(
actionState.logger,
configFile,
Buffer.from(fileContents, "base64").toString("binary"),
validateConfig,
);
}
+49
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 };
});
}
+229
View File
@@ -0,0 +1,229 @@
import test from "ava";
import sinon from "sinon";
import { ActionsEnvVars } from "../environment";
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);
await targetWithArgs.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);
await targetWithArgs.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.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);
await targetWithArgs.passes(t.deepEqual, testCase.expected);
}
});
test("parseRemoteFileAddress accepts remote address without a ref", async (t) => {
const target = callee(parseRemoteFileAddress).withArgs("owner/repo:path");
await target.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);
await targetWithArgs.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,
});
}
});
+153
View File
@@ -0,0 +1,153 @@
import { ActionState } from "../action-common";
import { ActionsEnvVars, ReadOnlyEnv } from "../environment";
import * as errorMessages from "../error-messages";
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: ReadOnlyEnv): 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 `input` as a `RemoteFileAddress` using the new format.
*
* @param env The read-only environment to obtain the owner name from if needed.
* @param configFile The input to try and parse.
* @returns A `RemoteFileAddress` value if successful or `undefined` otherwise.
*/
export function parseNewRemoteFileAddress(
env: ReadOnlyEnv,
configFile: string,
): Result<RemoteFileAddress, undefined> {
// retrieve the various parts of the config location, and ensure they're present
const format = new RegExp(
"^((?<owner>[^:@/]+)/)?(?<repo>[^:@/]+)(@(?<ref>[^:]+))?(:(?<path>.+))?$",
);
const pieces = format.exec(configFile.trim());
const repo: string | undefined = pieces?.groups?.repo?.trim();
// Check that the regular expression matched and that we have at least the repo name.
if (!pieces?.groups || !repo || repo.length === 0) {
return new Failure(undefined);
}
const owner: string | undefined = pieces.groups.owner?.trim();
const path: string | undefined = pieces.groups.path?.trim();
const ref: string | undefined = pieces.groups.ref?.trim();
return new Success({
owner: owner || getDefaultOwner(env),
repo,
path: path || DEFAULT_CONFIG_FILE_NAME,
ref: ref || DEFAULT_CONFIG_FILE_REF,
});
}
/**
* Attempts to parse `configFile` into an array of `RemoteFileAddress` components.
*
* @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;
}
// retrieve the various parts of the config location, and ensure they're present
const newFormatAddressResult = parseNewRemoteFileAddress(
actionState.env,
configFile,
);
if (newFormatAddressResult.isFailure()) {
// Neither the old format nor the new format worked. Throw an error that
// explains the format we accept. We only mention the new format, since that's
// what we want to be used going forward.
throw new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
);
}
const address = newFormatAddressResult.value;
// Ensure that the path is a relative path.
if (address.path.startsWith("/")) {
throw new ConfigurationError(
`The path component of '${configFile}' cannot be an absolute path.`,
);
}
return address;
}
+4 -4
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.1",
"cliVersion": "2.26.1",
"priorBundleVersion": "codeql-bundle-v2.26.0",
"priorCliVersion": "2.26.0"
}
+52 -19
View File
@@ -6,24 +6,45 @@ import { Language } from "./languages";
import { getActionsLogger } from "./logging";
import { getCodeQLDatabasePath } from "./util";
/** Represents a diagnostic message for the tool status page, etc. */
export interface DiagnosticMessage {
/**
* Known tags for diagnostics. There is currently only "internal-error",
* but others may be added in the future.
*/
export type DiagnosticTag = "internal-error";
/** Optional information about the origin of a diagnostic. */
export type DiagnosticSourceOptions = {
/**
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
* descriptor object should be nested under in SARIF.
*/
extractorName?: string;
/** An array of tags for the diagnostic. */
tags?: DiagnosticTag[];
};
/** Represents information about the origin of a diagnostic. */
export type DiagnosticSource = {
/**
* An identifier under which it makes sense to group this diagnostic message.
* This is used to build the SARIF reporting descriptor object.
*/
id: string;
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
name: string;
} & DiagnosticSourceOptions;
/**
* Represents a diagnostic message for the tool status page, etc.
*
* Unlike {@link DiagnosticMessage}, properties which can automatically
* be populated are optional in this type.
*/
export type DiagnosticMessageOptions = {
/** ISO 8601 timestamp */
timestamp: string;
source: {
/**
* An identifier under which it makes sense to group this diagnostic message.
* This is used to build the SARIF reporting descriptor object.
*/
id: string;
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
name: string;
/**
* Name of the CodeQL extractor. This is used to identify which tool component the reporting
* descriptor object should be nested under in SARIF.
*/
extractorName?: string;
};
timestamp?: string;
/** Information about the origin of the diagnostic. */
source?: DiagnosticSourceOptions;
/** GitHub flavored Markdown formatted message. Should include inline links to any help pages. */
markdownMessage?: string;
/** Plain text message. Used by components where the string processing needed to support Markdown is cumbersome. */
@@ -53,7 +74,15 @@ export interface DiagnosticMessage {
};
/** Structured metadata about the diagnostic message */
attributes?: { [key: string]: any };
}
};
/** Represents a diagnostic message for the tool status page, etc. */
export type DiagnosticMessage = DiagnosticMessageOptions & {
/** ISO 8601 timestamp */
timestamp: string;
/** Information about the origin of the diagnostic. */
source: DiagnosticSource;
};
/** Represents a diagnostic message that has not yet been written to the database. */
interface UnwrittenDiagnostic {
@@ -90,7 +119,7 @@ let diagnosticCounter = 0;
export function makeDiagnostic(
id: string,
name: string,
data: Partial<DiagnosticMessage> | undefined = undefined,
data: DiagnosticMessageOptions | undefined = undefined,
): DiagnosticMessage {
return {
...data,
@@ -243,6 +272,7 @@ export function makeTelemetryDiagnostic(
id: string,
name: string,
attributes: { [key: string]: any },
tags?: DiagnosticTag[],
): DiagnosticMessage {
return makeDiagnostic(id, name, {
attributes,
@@ -251,5 +281,8 @@ export function makeTelemetryDiagnostic(
statusPage: false,
telemetry: true,
},
source: {
tags,
},
});
}
+152
View File
@@ -1,3 +1,13 @@
/**
* Environment variables used by Default Setup to communicate the private registry proxy configuration.
*/
export enum RegistryProxyVars {
PROXY_HOST = "CODEQL_PROXY_HOST",
PROXY_PORT = "CODEQL_PROXY_PORT",
PROXY_CA_CERTIFICATE = "CODEQL_PROXY_CA_CERTIFICATE",
PROXY_URLS = "CODEQL_PROXY_URLS",
}
/**
* Environment variables used by the CodeQL Action.
*
@@ -17,6 +27,18 @@ export enum EnvVar {
*/
CLI_VERBOSITY = "CODEQL_VERBOSITY",
/**
* Set by Default Setup to the base branch of the PR being analysed, if analysing a PR.
* This is needed because the `pull_request` context is not available for `dynamic` events.
*/
CODE_SCANNING_BASE_BRANCH = "CODE_SCANNING_BASE_BRANCH",
/**
* Set by Default Setup to the full ref being analysed, if analysing a PR.
* This is needed because the `pull_request` context is not available for `dynamic` events.
*/
CODE_SCANNING_REF = "CODE_SCANNING_REF",
/**
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
* invoking `codeql version` again.
@@ -83,6 +105,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 +185,130 @@ 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 | RegistryProxyVars;
/**
* 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>) {}
/** Clones the object while detaching the underlying environment from the original. */
public clone(): this {
return Object.create(this, { vars: { value: { ...this.vars } } }) as this;
}
/** 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);
}
+10 -1
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`;
}
+14
View File
@@ -70,6 +70,8 @@ export interface CodeQLDefaultVersionInfo {
* Legacy features should end with `_enabled`.
*/
export enum Feature {
/** Allows supported properties of configuration files to be merged. */
AllowMergeConfigFiles = "allow_merge_config_files",
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
AllowToolcacheInput = "allow_toolcache_input",
@@ -134,6 +136,8 @@ export enum Feature {
/** Controls whether overlay build failures on the default branch are stored in the Actions cache. */
OverlayAnalysisStatusSave = "overlay_analysis_status_save",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Routes (some) API requests through the registry proxy. */
ProxyApiRequests = "proxy_api_requests",
/** Note that this currently only disables baseline file coverage information. */
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
StartProxyUseFeaturesRelease = "start_proxy_use_features_release",
@@ -171,6 +175,11 @@ export type FeatureConfig = {
};
export const featureConfig = {
[Feature.AllowMergeConfigFiles]: {
defaultValue: false,
envVar: "CODEQL_ACTION_ALLOW_MERGE_CONFIG_FILES",
minimumVersion: undefined,
},
[Feature.AllowMultipleAnalysisKinds]: {
defaultValue: false,
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
@@ -375,6 +384,11 @@ export const featureConfig = {
legacyApi: true,
minimumVersion: undefined,
},
[Feature.ProxyApiRequests]: {
defaultValue: false,
envVar: "CODEQL_ACTION_PROXY_API_REQUESTS",
minimumVersion: undefined,
},
[Feature.SkipFileCoverageOnPrs]: {
defaultValue: false,
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
+18 -27
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,
@@ -56,7 +56,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 +73,6 @@ import {
createStatusReportBase,
getActionsStatus,
sendStatusReport,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
@@ -204,12 +203,14 @@ async function sendCompletedStatusReport(
}
}
async function run(startedAt: Date) {
async function run(
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
const actionsEnv = getActionsEnv();
const startedAt = actionState.startedAt;
const logger = actionState.logger;
let apiDetails: GitHubApiCombinedDetails;
let config: configUtils.Config | undefined;
@@ -263,14 +264,10 @@ async function run(startedAt: Date) {
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
const useConfigFileProperty = await features.getValue(
Feature.ConfigFileRepositoryProperty,
);
configFile = getConfigFileInput(
logger,
actionsEnv,
const actionStateWithFeatures = { ...actionState, features };
configFile = await getConfigFileInput(
actionStateWithFeatures,
repositoryProperties,
useConfigFileProperty,
);
// path.resolve() respects the intended semantics of source-root. If
@@ -369,7 +366,7 @@ async function run(startedAt: Date) {
repositoryProperties,
);
config = await initConfig(features, {
config = await initConfig(actionStateWithFeatures, {
analysisKinds,
languagesInput: getOptionalInput("languages"),
queriesInput: getOptionalInput("queries"),
@@ -854,19 +851,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();
}
+2 -1
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 });
+3 -2
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);
});
}
+102 -6
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(),
);
});
+241 -13
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;
}
+13 -19
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<["Base", "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();
}
+3 -2
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([
+55 -6
View File
@@ -120,9 +120,20 @@ 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",
};
const dockerRegistryCredential = {
type: "docker_registry",
host: "https://registry.example.com",
token: "pqr",
};
test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
const registryCredentials = Buffer.from(
JSON.stringify([
@@ -241,7 +252,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 +295,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 +635,12 @@ 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,
dockerRegistryCredential,
]);
const credentials = startProxyExports.getCredentials(
getRunnerLogger(true),
@@ -633,7 +648,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) {
+20 -5
View File
@@ -187,9 +187,19 @@ 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",
"docker_registry",
] 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 +243,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 +303,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)
) {
+8 -8
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. */
+17 -5
View File
@@ -12,15 +12,16 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { parseRegistriesWithoutCredentials, type Config } from "./config-utils";
import { DependencyCacheRestoreStatusReport } from "./dependency-caching";
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,
@@ -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.
*
+342 -7
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,6 +12,7 @@ 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";
@@ -18,6 +21,7 @@ import { CachingKind } from "./caching-utils";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
import { Env, ActionsEnvVars } from "./environment";
import {
CodeQLDefaultVersionInfo,
Feature,
@@ -26,9 +30,11 @@ 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,
@@ -172,6 +178,10 @@ export function makeMacro<Args extends unknown[]>(
return wrapper;
}
export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
return getEnv(testEnv);
}
/**
* Gets an `ActionsEnv` instance for use in tests.
*/
@@ -181,6 +191,297 @@ export function getTestActionsEnv(): ActionsEnv {
};
}
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
type AllState = [
"Base",
"Logger",
"Env",
"ReadOnlyEnv",
"Actions",
"Api",
"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(),
apiClient: github.getOctokit("123"),
features: createFeatures([]),
...overrides,
};
}
type DelayedCheck<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
export type ValueOrMutation<T> = T | ((val: T) => void);
/**
* Wraps a function that accepts an `ActionState` for testing in different environments.
*/
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: cloneFrom.state.env.clone(),
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;
}
}
export interface PassedAssertion<R, T> {
result: Awaited<R>;
assertionResult: T;
}
/**
* A more minimal, exported interface for `CallableEnvBuilder`. This makes it easier to
* define helper functions in tests which expect a value of a compatible type.
*/
export interface AssertableTarget<R> {
passes<AArgs extends readonly any[], AResult>(
assertion: (val: Awaited<R>, ...assertionArgs: AArgs) => AResult,
...assertionArgs: AArgs
): Promise<PassedAssertion<R, AResult>>;
throws<ErrorType extends ErrorConstructor | Error>(
t: ExecutionContext<unknown>,
expectations?: ThrowsExpectation<ErrorType>,
): Promise<ThrownError<ErrorType>>;
}
class CallableEnvBuilder<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
>
extends BaseEnvBuilder<Args, R, Fs>
implements AssertableTarget<R>
{
private args: Args;
constructor(
fn: (state: ActionState<Fs>, ...args: Args) => R,
args: Args,
cloneFrom?: BaseEnvBuilder<Args, R, Fs>,
) {
super(fn, cloneFrom);
this.args = args;
}
protected clone(): this {
return new CallableEnvBuilder(this.fn, this.args, this) as this;
}
public getArgs(): Args {
return this.args;
}
call(): R {
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<PassedAssertion<R, AResult>> {
// this.call() may or may not return a promise,
// `Promise.resolve` turns the result into one if it isn't already,
// and we then await it. That ensures that `result` is an `Awaited<R>`.
const result = await Promise.resolve(this.call());
// Run the main assertion on the `result`.
const assertionResult = await assertion(result, ...assertionArgs);
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
}
// Return the results of the function call and the main assertion.
return { result, assertionResult };
}
/**
* Asserts that calling the underlying function should throw an exception.
*
* @param t The execution context for the assertion.
* @param expectations Expectations for the error.
* @returns The error that was thrown.
*/
public async throws<ErrorType extends ErrorConstructor | Error>(
t: ExecutionContext<unknown>,
expectations?: ThrowsExpectation<ErrorType>,
): Promise<ThrownError<ErrorType>> {
// Run the main assertion.
const error = await t.throwsAsync(
async () => Promise.resolve(this.call()),
expectations,
);
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
}
// Return the error.
return error;
}
}
/** 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 +501,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 +513,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 +531,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 +676,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`.
*/
+10 -22
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<["Base", "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);
}
+4 -23
View File
@@ -13,11 +13,14 @@ import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL, VersionInfo } from "./codeql";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import { 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;