Compare commits

...

262 Commits
v3.37.8 ... v3

Author SHA1 Message Date
Michael B. Gale
1190a975f9 Merge pull request #4171 from github/backport-v3.38.2-2892aa5e1
Merge releases/v4 into releases/v3
2026-09-24 12:00:34 +01:00
github-actions[bot]
a09a142526 Rebuild 2026-09-24 10:29:19 +00:00
github-actions[bot]
aa62ed54ed Update version and changelog for v3.38.2 2026-09-24 10:29:11 +00:00
github-actions[bot]
aca72b1c31 Merge remote-tracking branch 'origin/releases/v4' into backport-v3.38.2-2892aa5e1 2026-09-24 10:29:11 +00:00
github-actions[bot]
259e857114 Revert "Rebuild"
This reverts commit 39e5c2d42c.
2026-09-24 10:29:11 +00:00
github-actions[bot]
37fc051b5d Revert "Update version and changelog for v3.38.1"
This reverts commit d7512d4cfe.
2026-09-24 10:29:11 +00:00
Michael B. Gale
2892aa5e19 Merge pull request #4168 from github/update-v4.38.2-a6ef2c96f
Merge main into releases/v4
2026-09-24 11:26:28 +01:00
Michael B. Gale
8ad03a333e Trigger workflows 2026-09-24 11:15:21 +01:00
github-actions[bot]
98af865db5 Update changelog for v4.38.2 2026-09-24 10:01:43 +00:00
Michael B. Gale
a6ef2c96fc Merge pull request #4156 from github/mario-campos/fix-validate-cmd
Fix path-resolution bug in `changenotes.mts validate` by using `getChangenotes`
2026-09-24 08:16:09 +00:00
Michael B. Gale
1ef28a1b76 Merge pull request #4166 from github/dependabot/github_actions/dot-github/workflows/actions-minor-4c9e3e8b22
Bump ruby/setup-ruby from 1.321.0 to 1.323.0 in /.github/workflows in the actions-minor group across 1 directory
2026-09-24 08:02:30 +00:00
Michael B. Gale
26cb08bab0 Merge pull request #4163 from github/mbg/fix-getCommitOid-stubs
Fix `getCommitOid` stubs
2026-09-24 07:07:29 +00:00
Michael B. Gale
f035ce3a98 Merge pull request #4165 from github/dependabot/npm_and_yarn/npm-minor-8eaed9c323
Bump the npm-minor group across 1 directory with 3 updates
2026-09-23 18:11:21 +00:00
github-actions[bot]
5e4e2550b4 Rebuild 2026-09-23 17:57:57 +00:00
dependabot[bot]
b13f5f47d5 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.321.0 to 1.323.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](95ef2b042f...984c0c8908)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 17:56:05 +00:00
github-actions[bot]
c87fe5756c Rebuild 2026-09-23 17:55:50 +00:00
dependabot[bot]
becb485c9f Bump the npm-minor group across 1 directory with 3 updates
Bumps the npm-minor group with 3 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml), [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and [yaml](https://github.com/eemeli/yaml).


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

Updates `eslint-plugin-jsdoc` from 64.3.8 to 64.5.2
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v64.3.8...v64.5.2)

Updates `yaml` from 2.9.0 to 2.9.1
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.9.0...v2.9.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 64.5.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: yaml
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-23 17:54:02 +00:00
Michael B. Gale
42277414c7 Stub getCommitOid correctly and check calls 2026-09-23 13:38:53 +01:00
Michael B. Gale
9c9e4b034d Fix getCommitOid stubs 2026-09-23 13:31:48 +01:00
Michael B. Gale
f0064b219d Merge pull request #4162 from github/mbg/cli/disable-perfdata
Disable `UsePerfData` for `resolveExtractor`
2026-09-23 12:17:25 +00:00
Michael B. Gale
9691115b1c Disable UsePerfData for resolveExtractor 2026-09-23 11:29:17 +01:00
Michael B. Gale
41a4b9a4cd Merge pull request #4160 from github/update-bundle/codeql-bundle-v2.27.1
Update default bundle to 2.27.1
2026-09-22 17:53:51 +00:00
Henry Mercer
33141e5cce Merge pull request #4159 from github/henrymercer/pr-4146-follow-ups
Address per-language bundle review follow-ups
2026-09-22 17:17:15 +00:00
github-actions[bot]
81fb67799a Add changelog note 2026-09-22 17:12:17 +00:00
github-actions[bot]
53162242d5 Update default bundle to codeql-bundle-v2.27.1 2026-09-22 17:12:09 +00:00
Henry Mercer
3bacfe2c5b Remove trailing whitespace from nightly comment
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:56:15 +01:00
Henry Mercer
e574ef13a6 Merge main into per-language bundle follow-ups
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:54:25 +01:00
Henry Mercer
48321b2d48 Update src/per-language-bundles.ts
Co-authored-by: Michael B. Gale <mbg@github.com>
2026-09-22 16:49:21 +01:00
Henry Mercer
f2ec2f6267 Tweak comment for latest nightly version check
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-22 16:23:55 +01:00
Henry Mercer
a9a8cd1aec Explain the nightly bundle version-check exception
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:07:10 +01:00
Henry Mercer
dba87a18dc Clarify elapsed-time helper documentation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:07:10 +01:00
Henry Mercer
06344e2ba1 Stub nightly release listing directly
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:07:09 +01:00
Henry Mercer
07dc94940e Use default state in per-language bundle tests
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-22 16:07:07 +01:00
Michael B. Gale
416ff0dea1 Merge pull request #4157 from github/mbg/refactor/checkout-path
Refactor how the `checkout_path` input is obtained
2026-09-22 14:04:08 +00:00
Mario Campos
738bd62186 Refactor changenotes validate to use helper getChangenotes
This reduces duplicate code between `assemble` and `validate`. It also has the benefit of fixing a bug in the current implementation of `validate`, where `isValidChangenoteFile` receives a relative file name where it should receive an absolute one.
2026-09-22 09:01:46 -05:00
Mario Campos
6dd161bc76 Merge pull request #4155 from github/mario-campos/merge-changenotes
Add `changenotes.mts assemble` command to move changenotes to CHANGELOG.md
2026-09-22 12:52:41 +00:00
Michael B. Gale
8a88af6849 Refactor cleanupAndUploadDatabases querying checkout_path itself 2026-09-22 11:17:02 +01:00
Michael B. Gale
ada4e83349 Refactor cleanupAndUploadOverlayBaseDatabaseToCache querying checkout_path itself 2026-09-22 10:54:07 +01:00
Michael B. Gale
3a30b151d6 Refactor setupDiffInformedQueryRun querying checkout_path itself 2026-09-22 10:53:49 +01:00
Michael B. Gale
669351e880 Replace getRequiredEnvParam calls in init and setup-codeql action 2026-09-22 10:53:27 +01:00
Mario Campos
b1668d6234 Flesh out a comment 2026-09-21 16:40:26 -05:00
Mario Campos
c0bd54fdf4 Replace JSDoc text with @throws 2026-09-21 16:36:10 -05:00
Mario Campos
0263122209 Format code with npm run lint-fix 2026-09-21 16:32:44 -05:00
Mario Campos
c496c6cceb Rename name to absolutePath for clarity 2026-09-21 16:31:55 -05:00
Mario Campos
598cda36cf Apply suggestion from @mbg
Co-authored-by: Michael B. Gale <mbg@github.com>
2026-09-21 16:28:52 -05:00
Mario Campos
771560691a Update JSDoc comment with parameter lines
Co-authored-by: Michael B. Gale <mbg@github.com>
2026-09-21 16:28:36 -05:00
Mario Campos
a92f7fb688 Refactor changenote file listing into function to D.R.Y. 2026-09-18 15:02:17 -05:00
Mario Campos
7f54212a01 Rename flush command to assemble 2026-09-18 13:49:18 -05:00
Mario Campos
977b29b897 Replace splice with push and pop 2026-09-18 13:47:06 -05:00
Mario Campos
bc0efd6d91 Simplify addBodyLinesToUnreleasedSection 2026-09-18 12:35:41 -05:00
Mario Campos
128614ad8b Simplify getHeader to operate on ChangelogSections 2026-09-18 12:24:19 -05:00
Mario Campos
d63b2a40db Assume '[UNRELEASED]' section is first section 2026-09-18 11:42:46 -05:00
Mario Campos
b246e56069 Use ExitCode instead of 0/1 2026-09-18 11:28:17 -05:00
Mario Campos
bffae1c4b8 Use withChangelog I/O helper 2026-09-18 11:24:26 -05:00
Mario Campos
1ee3265262 Add changenotes.mts flush command
This command will "flush" or move the changenotes in the `unreleased-change-notes` directory to the `[UNRELEASED]` section of the CHANGELOG.md file.
2026-09-18 11:14:49 -05:00
Mario Campos
bb1dc5460b Add CHANGELOG function addBodyLinesToUnreleasedSection
This will be used by the `pr-checks/changenotes.mts` script to "compile" the latest release entry of CHANGELOG.md.
2026-09-18 11:14:48 -05:00
Mario Campos
47d607e2c1 Add changelog parsing helper getHeader 2026-09-18 11:14:48 -05:00
Mario Campos
cb31eabcd8 Do not include trailing newlines in NO_CHANGES_STR
Changing `NO_CHANGES_STR` to just be the text will make it easier to insert/use. To not break anything, I added the deleted newlines to the locations where `NO_CHANGES_STR` was used.
2026-09-18 09:35:44 -05:00
Michael B. Gale
3ea06614da Merge pull request #4154 from github/backport-v3.38.1-1c5b67565
Merge releases/v4 into releases/v3
2026-09-18 15:06:05 +01:00
Michael B. Gale
a7afe0a2d7 Merge pull request #4153 from github/mergeback/v4.38.1-to-main-1c5b6756
Mergeback v4.38.1 refs/heads/releases/v4 into main
2026-09-18 13:40:20 +00:00
github-actions[bot]
39e5c2d42c Rebuild 2026-09-18 13:12:36 +00:00
github-actions[bot]
d7512d4cfe Update version and changelog for v3.38.1 2026-09-18 13:12:27 +00:00
github-actions[bot]
41500c6922 Merge remote-tracking branch 'origin/releases/v4' into backport-v3.38.1-1c5b67565 2026-09-18 13:12:27 +00:00
github-actions[bot]
693d869c25 Revert "Rebuild"
This reverts commit 3490969653.
2026-09-18 13:12:27 +00:00
github-actions[bot]
dc38ccb025 Revert "Update version and changelog for v3.38.0"
This reverts commit ce74b7377b.
2026-09-18 13:12:27 +00:00
github-actions[bot]
f8b1c08e6d Rebuild 2026-09-18 13:11:23 +00:00
github-actions[bot]
ba80880079 Update changelog and version after v4.38.1 2026-09-18 13:11:14 +00:00
Michael B. Gale
1c5b675653 Merge pull request #4152 from github/update-v4.38.1-a65b83a73
Merge main into releases/v4
2026-09-18 14:09:51 +01:00
Michael B. Gale
a97cdcae05 Add changelog entry for #4146 2026-09-18 13:58:09 +01:00
github-actions[bot]
cc6c6911c3 Update changelog for v4.38.1 2026-09-18 12:46:37 +00:00
Michael B. Gale
a65b83a73d Merge pull request #4146 from github/henrymercer/per-language-bundles-pr
Use per-language CodeQL bundles
2026-09-18 12:30:36 +00:00
Henry Mercer
07fa87d333 Clarify the latest-nightly eligibility exception
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:08:05 +01:00
Henry Mercer
f18f3536f1 Describe the bundle URL resolver
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:07:06 +01:00
Henry Mercer
ecec9b5a37 Share per-language telemetry fields without renaming
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:06:17 +01:00
Henry Mercer
79fe3a1270 Move download telemetry into the status-report directory
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:04:38 +01:00
Henry Mercer
ead1f7d93f Rename the platform module
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:03:27 +01:00
Henry Mercer
549d498da3 Simplify per-language platform eligibility checks
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:02:12 +01:00
Henry Mercer
ed3a24ccbc Group bundle eligibility test state overrides
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:01:31 +01:00
Henry Mercer
28b8f598f5 Use shared test state for bundle eligibility
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 19:00:38 +01:00
Henry Mercer
a0cc01096a Merge pull request #4145 from github/henrymercer/bundle-resolution-pr
Separate CodeQL bundle resolution from installation
2026-09-17 14:31:25 +00:00
Henry Mercer
41c04be873 Use initAllState in bundle cleanup tests
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 14:35:02 +01:00
Henry Mercer
06f4ce74f9 Isolate bundle cleanup test environments
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-17 14:34:28 +01:00
Henry Mercer
5a2f29e0a1 Merge published per-language bundle history
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 20:27:35 +01:00
Henry Mercer
cf9bbfe9a1 Merge pull request #4150 from github/dependabot/npm_and_yarn/npm-minor-b347c7d8de
Bump the npm-minor group across 1 directory with 3 updates
2026-09-16 19:14:07 +00:00
Henry Mercer
69f47159b1 Use Result.orElse for bundle extraction paths
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 19:55:13 +01:00
Henry Mercer
f4fa111630 Share elapsed-time rounding for bundle downloads
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 19:51:23 +01:00
Henry Mercer
89606bbad1 Return toolcache rejection reasons with Result
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 19:43:05 +01:00
Henry Mercer
bd2ddba96c Extract explicit CodeQL bundle URL classification
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 19:36:07 +01:00
dependabot[bot]
ac47650173 Bump the npm-minor group across 1 directory with 3 updates
Bumps the npm-minor group with 3 updates in the / directory: [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat), [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@eslint/compat` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/compat-v2.1.1/packages/compat)

Updates `eslint-plugin-jsdoc` from 64.3.4 to 64.3.8
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v64.3.4...v64.3.8)

Updates `typescript-eslint` from 8.69.0 to 8.70.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.70.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/compat"
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 64.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-16 17:54:39 +00:00
Henry Mercer
2f552a99f3 Clarify bundle resolution and latest-nightly selection
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 17:36:46 +01:00
Henry Mercer
f536ef48b7 Centralize CodeQL download telemetry fields
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 17:30:50 +01:00
Henry Mercer
dfb9bf52c9 Share CodeQL bundle platform definitions
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 17:28:00 +01:00
Henry Mercer
2d47caf123 Isolate per-language bundle eligibility state
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 17:25:24 +01:00
Henry Mercer
59ce3a25ba Include failed bundle attempts in fallback timing
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 16:56:49 +01:00
Henry Mercer
289376d7dd Use per-language CodeQL bundles
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 16:56:33 +01:00
Mario Campos
9961729e06 Merge pull request #4116 from github/mario-campos/validate-cn-workflow
Validate change-notes as a PR check
2026-09-16 15:12:11 +00:00
Michael B. Gale
7dbd038544 Merge pull request #4142 from github/mbg/copilot/update-instructions
Improve Copilot instructions
2026-09-16 13:03:53 +00:00
Henry Mercer
f3e0c870be Clarify resolved bundle handling
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-16 12:16:13 +01:00
Michael B. Gale
53e5014d5d Address review comments 2026-09-16 11:29:45 +01:00
Michael B. Gale
f959bfdc31 Merge remote-tracking branch 'origin/main' into mbg/copilot/update-instructions 2026-09-16 11:14:23 +01:00
Henry Mercer
eb76062ef2 Include failed bundle attempts in fallback timing
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-15 19:57:17 +01:00
Henry Mercer
ec5eecb246 Use per-language CodeQL bundles
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-15 18:17:16 +01:00
Henry Mercer
1970c450ec Separate CodeQL bundle resolution from installation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-15 15:25:18 +01:00
Mario Campos
e26a5c4139 Be consistent with language and conventions 2026-09-15 09:18:42 -05:00
Henry Mercer
f19a847dca Merge pull request #4135 from github/dependabot/npm_and_yarn/octokit/types-18.0.0
Bump @octokit/types from 17.0.0 to 18.0.0
2026-09-15 13:44:13 +00:00
Henry Mercer
d65664dcae Merge pull request #4138 from github/henrymercer/bundle-download-errors
Preserve HTTP errors from streaming bundle downloads
2026-09-15 12:36:14 +00:00
Henry Mercer
0224ca9728 Use a plain error when the HTTP status is unknown
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-15 12:55:26 +01:00
Mario Campos
6041eff66b Update validate.test.mts test names to reflect new isValidAllChangenoteFiles
The `isValidAllChangenoteFiles` now accepts a list of file paths rather than a file path string.
2026-09-14 13:40:10 -05:00
Mario Campos
f45ef9d1c5 Log something on successful changenote validation
Also, slightly improve the log statement of the validation error case.
2026-09-14 12:17:28 -05:00
Henry Mercer
13354125bc Omit unknown HTTP status codes from download errors
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-14 11:41:48 +01:00
Mario Campos
f60f4d9500 Validate change-notes in PRs
This commit also introduces the 'empty' home directory for future change-notes.
2026-09-11 16:04:21 -05:00
Mario Campos
a691c16115 Extend changetool to support directories 2026-09-11 16:04:21 -05:00
Mario Campos
b1c1bbb1e0 Extend ESLint rules to *.mts files 2026-09-11 14:52:02 -05:00
Mario Campos
1cf8f518e1 Merge pull request #4140 from github/mario-campos/move-withTmpFile
Move `withTmpFile` next to `withTmpDir`
2026-09-11 18:54:51 +00:00
Mario Campos
faf4f3af21 Add JSDoc to withTmpFile 2026-09-11 11:12:03 -05:00
Michael B. Gale
e72db597b8 Merge pull request #4143 from github/mbg/rename-script-to-changenote
Rename `validate-changelog` to `changenotes`
2026-09-11 15:09:33 +00:00
Mario Campos
15b91dbb92 Delete unnecessary .js file extension from import statement
Co-authored-by: Michael B. Gale <mbg@github.com>
2026-09-11 08:57:52 -05:00
Michael B. Gale
27a821e701 Rename validate-changelog to changenotes 2026-09-11 14:46:35 +01:00
Michael B. Gale
cc78526536 Improve Copilot instructions 2026-09-11 14:19:19 +01:00
Michael B. Gale
032138d5fb Merge pull request #4141 from github/mario-campos/finally-clean-withTmpDir
Always clean-up temp dirs in `withTmpDir`
2026-09-11 13:12:42 +00:00
Michael B. Gale
7be8083b7e Merge pull request #4139 from github/mbg/10sep26-deps
Update dependencies
2026-09-11 11:33:48 +00:00
Mario Campos
80d5f4e36a Always clean-up temp dirs in withTmpDir 2026-09-10 18:48:14 -05:00
Mario Campos
297617deb9 Update npm commands in sync.sh to include workspace root dependencies 2026-09-10 17:22:50 -05:00
Mario Campos
f6491d646e Reduce duplication by using withTmpDir in withTmpFile 2026-09-10 15:37:39 -05:00
Mario Campos
4375107b7e Move withTmpFile to util.ts and update tests to use it 2026-09-10 15:36:26 -05:00
Michael B. Gale
a50b659501 Update dependencies 2026-09-10 20:32:49 +01:00
Henry Mercer
5bdaf5d4af Preserve HTTP errors from streaming bundle downloads
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-10 19:05:41 +01:00
Michael B. Gale
14c568a926 Merge pull request #4128 from github/mbg/move-changetool
Migrate `changetool` to `pr-checks`
2026-09-10 16:42:38 +00:00
Michael B. Gale
0484c3ad1d Merge remote-tracking branch 'origin/main' into mbg/move-changetool 2026-09-10 17:09:46 +01:00
Michael B. Gale
aaa014366e Remove unneeded Promise.resolve 2026-09-10 16:59:12 +01:00
dependabot[bot]
5bd5bf93f2 Bump @octokit/types from 17.0.0 to 18.0.0
Bumps [@octokit/types](https://github.com/octokit/types.ts) from 17.0.0 to 18.0.0.
- [Release notes](https://github.com/octokit/types.ts/releases)
- [Commits](https://github.com/octokit/types.ts/compare/v17.0.0...v18.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-10 11:00:56 +00:00
Michael B. Gale
5584cec589 Merge pull request #4134 from github/dependabot/npm_and_yarn/npm-minor-a30b170aad
Bump the npm-minor group across 1 directory with 6 updates
2026-09-10 10:47:33 +00:00
Michael B. Gale
d9826583a3 Merge pull request #4136 from github/dependabot/github_actions/dot-github/workflows/actions-minor-4cf73e6ad1
Bump actions/setup-java from 6.0.0 to 6.0.1 in /.github/workflows in the actions-minor group across 1 directory
2026-09-10 10:34:38 +00:00
github-actions[bot]
9f28912d94 Rebuild 2026-09-09 17:58:17 +00:00
github-actions[bot]
d9ef050686 Rebuild 2026-09-09 17:56:37 +00:00
dependabot[bot]
a72798355c Bump actions/setup-java
Bumps the actions-minor group with 1 update in the /.github/workflows directory: [actions/setup-java](https://github.com/actions/setup-java).


Updates `actions/setup-java` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](dd06d9cba3...de7274f081)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-09 17:56:16 +00:00
dependabot[bot]
d40707c73d 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 |
| --- | --- | --- |
| [@octokit/core](https://github.com/octokit/core.js) | `7.0.7` | `7.0.8` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `5.4.0` | `5.4.1` |
| [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) | `64.2.1` | `64.3.4` |
| [globals](https://github.com/sindresorhus/globals) | `17.11.0` | `17.12.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.68.0` | `8.69.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.23.12` | `4.23.13` |



Updates `@octokit/core` from 7.0.7 to 7.0.8
- [Release notes](https://github.com/octokit/core.js/releases)
- [Commits](https://github.com/octokit/core.js/compare/v7.0.7...v7.0.8)

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

Updates `eslint-plugin-jsdoc` from 64.2.1 to 64.3.4
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v64.2.1...v64.3.4)

Updates `globals` from 17.11.0 to 17.12.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.11.0...v17.12.0)

Updates `typescript-eslint` from 8.68.0 to 8.69.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.69.0/packages/typescript-eslint)

Updates `tsx` from 4.23.12 to 4.23.13
- [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.23.12...v4.23.13)

---
updated-dependencies:
- dependency-name: "@octokit/core"
  dependency-version: 7.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: js-yaml
  dependency-version: 5.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 64.3.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: globals
  dependency-version: 17.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: tsx
  dependency-version: 4.23.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-09 17:54:35 +00:00
Henry Mercer
e855781840 Merge pull request #4132 from github/mergeback/v4.38.0-to-main-b96794f0
Mergeback v4.38.0 refs/heads/releases/v4 into main
2026-09-09 15:16:08 +00:00
Henry Mercer
faaca9a8f6 Merge pull request #4133 from github/backport-v3.38.0-b96794f01
Merge releases/v4 into releases/v3
2026-09-09 15:51:52 +01:00
github-actions[bot]
3490969653 Rebuild 2026-09-09 14:37:28 +00:00
github-actions[bot]
ce74b7377b Update version and changelog for v3.38.0 2026-09-09 14:37:15 +00:00
github-actions[bot]
739ee7214d Merge remote-tracking branch 'origin/releases/v4' into backport-v3.38.0-b96794f01 2026-09-09 14:37:15 +00:00
github-actions[bot]
a3c61c3ab4 Revert "Rebuild"
This reverts commit 2907452324.
2026-09-09 14:37:15 +00:00
github-actions[bot]
16e21b98bd Revert "Update version and changelog for v3.37.9"
This reverts commit a4f1d2dfeb.
2026-09-09 14:37:14 +00:00
github-actions[bot]
23a5edca55 Rebuild 2026-09-09 14:04:19 +00:00
github-actions[bot]
6f5a84b5b5 Update changelog and version after v4.38.0 2026-09-09 14:04:04 +00:00
Henry Mercer
b96794f015 Merge pull request #4131 from github/update-v4.38.0-7e08580a9
Merge main into releases/v4
2026-09-09 15:02:30 +01:00
github-actions[bot]
02d5093871 Update changelog for v4.38.0 2026-09-09 12:33:07 +00:00
Henry Mercer
7e08580a93 Merge pull request #4130 from github/henrymercer/workflow-runner-sizing
Run slow macOS checks on larger runners
2026-09-09 12:16:50 +00:00
Henry Mercer
bfcc52b4f5 Run slow macOS checks on larger runners
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-09 12:57:40 +01:00
Henry Mercer
8c251e757c Merge pull request #4129 from github/update-bundle/codeql-bundle-v2.27.0
Update default bundle to 2.27.0
2026-09-09 11:48:16 +00:00
github-actions[bot]
0b7ca400df Add changelog note 2026-09-09 11:33:54 +00:00
github-actions[bot]
40484b3395 Update default bundle to codeql-bundle-v2.27.0 2026-09-09 11:33:48 +00:00
Michael B. Gale
d23593114c Address linter errors in validate.test.mts 2026-09-09 11:44:24 +01:00
Michael B. Gale
b1363526cf Fix some formatting in pr-checks.yml 2026-09-09 11:44:23 +01:00
Michael B. Gale
83d1431d4e Migrate changetool to pr-checks 2026-09-09 11:44:23 +01:00
Henry Mercer
977e6ceaea Merge pull request #4124 from github/henrymercer/toolcache-bundle-cleanup
Delete unused CodeQL bundles from the toolcache before downloading
2026-09-08 17:04:25 +00:00
Henry Mercer
40a6b38247 Address toolcache cleanup review feedback
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-08 15:55:41 +01:00
Henry Mercer
deece8f852 Apply suggestion from @henrymercer 2026-09-07 14:08:36 +01:00
copilot-swe-agent[bot]
034db721dd Merge remote-tracking branch 'origin/main' into henrymercer/toolcache-bundle-cleanup
Co-authored-by: henrymercer <14129055+henrymercer@users.noreply.github.com>
2026-09-07 12:57:51 +00:00
Henry Mercer
313a0b9922 Merge pull request #4122 from github/henrymercer/friendly-potato
Don't record an overlay status when the job was cancelled
2026-09-04 16:55:52 +00:00
Henry Mercer
31da345c07 Address review comments
Delete each version directory individually so that a symlinked one is skipped rather than removed, take an `ActionState` so the environment is read through `ReadOnlyEnv` rather than the deprecated `getOptionalEnvVar`, let `deleteToolcacheBundles` report its own failure to locate the toolcache instead of having the caller catch it, quote paths in log messages, and rename `HAS_OBTAINED_CODEQL_TOOLS` to `HAS_SET_UP_CODEQL`, which is also set when we find the tools in the toolcache rather than downloading them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 17:43:40 +01:00
Henry Mercer
46dfb14111 Merge pull request #4123 from github/redsun82-address-pr-4072-comments
Analyze all supported languages in the Linux Arm64 check
2026-09-04 16:39:50 +00:00
Henry Mercer
e06b60f75f Merge pull request #4125 from github/henrymercer/merge-queue-sarif-upload-flake
Don't upload the ESLint SARIF from merge queue refs
2026-09-04 16:37:26 +00:00
Henry Mercer
a48f2d3077 Record an overlay status only for conclusive job statuses
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 17:33:02 +01:00
Henry Mercer
657964c39f Read the job status from a ReadOnlyEnv in recordOverlayStatus
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 17:31:24 +01:00
Henry Mercer
d2fe508a6d Don't upload the ESLint SARIF from merge queue refs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 17:22:52 +01:00
Paolo Tranquilli
16da74769e Log found databases in Linux Arm64 check
Log a success line for each language database found in the Linux Arm64 PR check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 12:27:23 +02:00
Henry Mercer
e13c3dc834 Only clean up a toolcache on the workspace filesystem
Some runner images keep the toolcache on a different volume to the workspace, so deleting the tools there frees up disk space that the analysis cannot use, and costs a later step that wanted them in the toolcache a download. Windows runners are laid out this way, with the toolcache on `C:` and the workspace on `D:`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 11:00:56 +01:00
Henry Mercer
1331773b9a Address Copilot review feedback
Run the cleanup even when the download will not be cached in the toolcache, since the toolcache shares a filesystem with the directory we extract to, so freeing it helps either way, and report an error other than the toolcache being absent as a failure rather than as an empty toolcache.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:26:09 +01:00
Henry Mercer
2681b03bd6 Add a changelog note
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:18:43 +01:00
Henry Mercer
a7a90f3ffb Enable toolcache cleanup in the multi-language PR check
The check installs the tools from a URL, so most versions in its matrix are downloaded rather than found in the toolcache, which is when the cleanup runs, and it then builds and analyses seven languages, so a bundle we damaged on the way in would show up.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:12:11 +01:00
Henry Mercer
4a0b22ec25 Delete unused CodeQL bundles from the toolcache before downloading
When we download a bundle the toolcache often already holds a different one that the job will not use, and on GitHub-hosted runners it shares a filesystem with the workspace, so it takes space away from the analysis. Empty the toolcache before downloading, which also frees space for the archive during extraction, and which is safe because getting as far as a download means the tools were not resolved from the toolcache. Skip this once a step has obtained the tools, since a later step may run a path it was given, and gate it on the runner being GitHub-hosted and on a feature flag that is off by default.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:12:10 +01:00
Henry Mercer
762a5ed7f7 Add a helper to delete the CodeQL tools from the toolcache
`deleteToolcacheBundles` removes `$RUNNER_TOOL_CACHE/CodeQL` and reports which versions were there. It refuses to follow a symlinked CodeQL directory so that it can only ever delete paths that are really inside the toolcache, and reports failures rather than throwing. Not called yet.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:12:10 +01:00
Henry Mercer
ceb85f25b5 Distinguish GitHub-hosted runners from ones that look hosted
`isHostedRunner` infers hostedness from the runner name and the toolcache path, so it also matches self-hosted runners that are configured to resemble hosted ones. Rename it to `looksLikeHostedRunner` so callers can see they are getting a heuristic, and add `isGitHubHostedRunner`, which reads the `RUNNER_ENVIRONMENT` value the Actions service reports. The existing callers keep the heuristic, so there is no behaviour change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 10:12:09 +01:00
Henry Mercer
9fddc16f0d Merge pull request #4088 from github/dependabot/npm_and_yarn/octokit/plugin-rest-endpoint-methods-18.0.0
Bump @octokit/plugin-rest-endpoint-methods from 17.0.0 to 18.0.0
2026-09-04 07:23:47 +00:00
Paolo Tranquilli
36cbf13b5e Address review comments
- Analyze all languages CodeQL supports on Linux Arm64 (except Swift,
  which is macOS only) in the Linux Arm64 check, building the compiled
  ones via the shared multi-language-repo build.sh
- De-duplicate the check's language list via a shared LANGUAGES env var
  used by both the init input and the assert loop
- Fix CHANGELOG tense: "download" -> "downloads"

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-04 08:30:10 +02:00
Henry Mercer
38dd4a088a Don't record an overlay status when the job was cancelled
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-03 18:21:46 +01:00
Henry Mercer
2f3c1c964a Add an internal job-status input to the init Action
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-03 18:21:14 +01:00
Michael B. Gale
5914b031b1 Merge pull request #4111 from github/dependabot/github_actions/dot-github/workflows/actions/setup-java-6.0.0
Bump actions/setup-java from 5.7.0 to 6.0.0 in /.github/workflows
2026-09-03 16:50:30 +00:00
Michael B. Gale
102499482f Merge pull request #4119 from github/dependabot/npm_and_yarn/npm-minor-21ac56865f
Bump the npm-minor group across 1 directory with 2 updates
2026-09-03 16:22:14 +00:00
Michael B. Gale
9401a335a3 Merge pull request #4118 from github/dependabot/npm_and_yarn/browserslist-4.28.8
Bump browserslist from 4.24.2 to 4.28.8
2026-09-03 16:21:07 +00:00
Michael B. Gale
3ee8a9398b Merge branch 'main' into dependabot/npm_and_yarn/browserslist-4.28.8 2026-09-03 17:03:16 +01:00
Michael B. Gale
25da1b1495 Merge branch 'main' into dependabot/npm_and_yarn/npm-minor-21ac56865f 2026-09-03 17:00:00 +01:00
Michael B. Gale
9509fd0822 Merge pull request #4120 from github/dependabot/npm_and_yarn/eslint-plugin-jsdoc-64.2.1
Bump eslint-plugin-jsdoc from 62.9.0 to 64.2.1
2026-09-03 15:58:16 +00:00
Michael B. Gale
20ec60c2d9 Merge branch 'main' into dependabot/npm_and_yarn/eslint-plugin-jsdoc-64.2.1 2026-09-03 15:12:29 +01:00
Paolo Tranquilli
4cccb3aa86 Merge pull request #4072 from github/redsun82-linux-arm64-support
Add support for Linux Arm64 runners
2026-09-03 13:58:15 +00:00
github-actions[bot]
b724a86493 Rebuild 2026-09-02 17:56:48 +00:00
dependabot[bot]
2a03009e3f Bump eslint-plugin-jsdoc from 62.9.0 to 64.2.1
Bumps [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) from 62.9.0 to 64.2.1.
- [Release notes](https://github.com/gajus/eslint-plugin-jsdoc/releases)
- [Commits](https://github.com/gajus/eslint-plugin-jsdoc/compare/v62.9.0...v64.2.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsdoc
  dependency-version: 64.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 17:55:01 +00:00
dependabot[bot]
da21771e2e Bump the npm-minor group across 1 directory with 2 updates
Bumps the npm-minor group with 2 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


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

Updates `typescript-eslint` from 8.67.0 to 8.68.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.68.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 17:54:50 +00:00
dependabot[bot]
bb36f8049c Bump browserslist from 4.24.2 to 4.28.8
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.24.2 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.24.2...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 13:55:34 +00:00
Michael B. Gale
276e2ce25d Merge pull request #4117 from github/mbg/deps/1sep
Update dependencies
2026-09-02 13:39:31 +00:00
Michael B. Gale
ccd5275aa4 Merge branch 'main' into dependabot/github_actions/dot-github/workflows/actions/setup-java-6.0.0 2026-09-02 14:36:24 +01:00
Mario Campos
a0c73122a6 Merge pull request #4096 from github/mario-campos/use-json-module
Use `json` module for JSON validation in `output-cache`
2026-09-01 15:17:48 +00:00
Michael B. Gale
50e244824e Update dependencies 2026-09-01 12:16:07 +01:00
Paolo Tranquilli
025009006c Address review comments
- Stub process.platform/arch in the supported-platform test and loop
  over all supported pairs (including linux/arm64) so it no longer
  depends on the host
- Run the default queries in the Linux Arm64 PR check so the databases
  are finalized end-to-end

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: fb7e2d12-6620-4a67-9c1c-4e82f4a5e8d9
2026-08-28 17:28:29 +02:00
Mario Campos
6f530319d8 Merge pull request #4113 from github/mario-campos/add-changenote-script
Create `changetool` script for validating change-notes
2026-08-27 18:09:46 +00:00
Henry Mercer
1bcdd0c019 Derive the API client type from @actions/github
`ApiClient` was hand-composed from `Octokit`, `Api` and `PaginateInterface`, imported directly from separately versioned Octokit packages. That asserted a shape matching what `@actions/github` actually returns, which held only while the versions happened to agree.

`@octokit/plugin-rest-endpoint-methods` v18 adds twelve Actions cache-limit methods. `@actions/github` still depends on `^17.0.0`, so the client it constructs no longer satisfies the v18-derived alias and the build fails.

Deriving the type from the constructor removes the assumption, so the alias tracks whatever `@actions/github` returns.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-27 18:56:47 +01:00
Mario Campos
f049ecb9ce Delete unused Node.js flag --experimental-test-module-mocks
Co-authored-by: Henry Mercer <henrymercer@github.com>
2026-08-27 11:22:56 -05:00
dependabot[bot]
25694c08ea Bump @octokit/plugin-rest-endpoint-methods from 17.0.0 to 18.0.0
Bumps [@octokit/plugin-rest-endpoint-methods](https://github.com/octokit/plugin-rest-endpoint-methods.js) from 17.0.0 to 18.0.0.
- [Release notes](https://github.com/octokit/plugin-rest-endpoint-methods.js/releases)
- [Commits](https://github.com/octokit/plugin-rest-endpoint-methods.js/compare/v17.0.0...v18.0.0)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-rest-endpoint-methods"
  dependency-version: 18.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-27 11:01:07 +00:00
Henry Mercer
0963041ab4 Merge pull request #4090 from github/dependabot/npm_and_yarn/octokit/plugin-paginate-rest-15.0.0
Bump @octokit/plugin-paginate-rest from 14.0.0 to 15.0.0
2026-08-27 10:45:55 +00:00
github-actions[bot]
1a8ddd7325 Rebuild 2026-08-27 10:32:49 +00:00
Henry Mercer
abc579a511 Merge pull request #4112 from github/dependabot/npm_and_yarn/npm-minor-62a3d68ea9
Bump the npm-minor group across 1 directory with 2 updates
2026-08-27 10:28:51 +00:00
dependabot[bot]
4d1d53ec73 Bump @octokit/plugin-paginate-rest from 14.0.0 to 15.0.0
Bumps [@octokit/plugin-paginate-rest](https://github.com/octokit/plugin-paginate-rest.js) from 14.0.0 to 15.0.0.
- [Release notes](https://github.com/octokit/plugin-paginate-rest.js/releases)
- [Commits](https://github.com/octokit/plugin-paginate-rest.js/compare/v14.0.0...v15.0.0)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-paginate-rest"
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-27 10:08:06 +00:00
Henry Mercer
6d4d5dfcd6 Merge pull request #4089 from github/dependabot/npm_and_yarn/octokit/types-17.0.0
Bump @octokit/types from 16.0.0 to 17.0.0
2026-08-27 09:37:27 +00:00
Paolo Tranquilli
7acc225a21 Merge pull request #4110 from github/redsun82-release-merge-instructions
Document merge-commit requirement for release PRs
2026-08-27 08:26:42 +00:00
Paolo Tranquilli
76ba03be44 Merge branch 'main' into redsun82-release-merge-instructions 2026-08-27 10:10:14 +02:00
Paolo Tranquilli
6441b26a49 Merge remote-tracking branch 'origin/main' into redsun82-linux-arm64-support 2026-08-27 10:09:22 +02:00
Mario Campos
cacb7b37ed Delete unused script/changetool NPM scripts 2026-08-26 23:39:11 -05:00
Mario Campos
e2cd31e32e Refactor tsconfig.json to extend base configuration and simplify options 2026-08-26 23:32:24 -05:00
Mario Campos
be555cf0a3 Extend scripts/changetool as a NPM workspace 2026-08-26 23:25:06 -05:00
Mario Campos
0447ab23b5 Enhance changenote validation to support nested bullet lists 2026-08-26 23:06:47 -05:00
Mario Campos
50ec5a5c18 Use Object.hasOwn instead of in operator
Apparently, the `in` operator will check inherited properties too.
2026-08-26 22:23:00 -05:00
Mario Campos
5aea817801 Replace Array.prototype.every() with for loop
`.every()` short-circuits on the first `false`
2026-08-26 22:20:25 -05:00
Mario Campos
580da893f6 Handle file errors as false 2026-08-26 22:15:34 -05:00
Mario Campos
426c10420b Create changetool script for validating change-notes 2026-08-26 17:13:40 -05:00
github-actions[bot]
dc5bc53050 Rebuild 2026-08-26 18:07:30 +00:00
github-actions[bot]
d75af13705 Rebuild 2026-08-26 18:03:17 +00:00
dependabot[bot]
0006c77502 Bump the npm-minor group across 1 directory with 2 updates
Bumps the npm-minor group with 2 updates in the / directory: [js-yaml](https://github.com/nodeca/js-yaml) and [uuid](https://github.com/uuidjs/uuid).


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

Updates `uuid` from 14.0.1 to 14.0.2
- [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.1...v14.0.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: uuid
  dependency-version: 14.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 18:01:20 +00:00
dependabot[bot]
ec1bd1999f Bump actions/setup-java from 5.7.0 to 6.0.0 in /.github/workflows
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5.7.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](b6effb05e4...dd06d9cba3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-26 17:59:42 +00:00
Henry Mercer
4d40c93072 Merge pull request #4108 from github/mergeback/v4.37.9-to-main-cdf488f5
Mergeback v4.37.9 refs/heads/releases/v4 into main
2026-08-26 17:19:52 +00:00
Henry Mercer
6f5948dfac Merge pull request #4109 from github/backport-v3.37.9-cdf488f59
Merge releases/v4 into releases/v3
2026-08-26 18:02:02 +01:00
Henry Mercer
3e93618b99 Merge pull request #4105 from github/henrymercer/tools-download-telemetry
Improve CodeQL tools download time telemetry
2026-08-26 17:01:23 +00:00
Paolo Tranquilli
42c2ea9ef7 Document merge-commit requirement for release PRs
Release, mergeback, and backport PRs must be merged with a merge commit
so the branch linkage the release automation depends on is preserved
(the mergeback tags the release using the merge commit of the
"Merge main into releases/vN" PR, so squashing or rebasing breaks
tagging).

Add a path-scoped Copilot instructions file scoped to the files that
reliably change in these PRs (CHANGELOG.md, src/defaults.json,
lib/defaults.json, src/api-compatibility.json), and add an explicit note
to the Releasing runbook in CONTRIBUTING.md so the rule is enforced
regardless of who merges.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dae6e42-7fc6-4fb1-b1ea-15dc17e402a2
2026-08-26 17:13:12 +02:00
github-actions[bot]
2907452324 Rebuild 2026-08-26 14:42:27 +00:00
github-actions[bot]
a4f1d2dfeb Update version and changelog for v3.37.9 2026-08-26 14:42:12 +00:00
github-actions[bot]
a87239a309 Merge remote-tracking branch 'origin/releases/v4' into backport-v3.37.9-cdf488f59 2026-08-26 14:42:12 +00:00
github-actions[bot]
c68e6e8e04 Revert "Rebuild"
This reverts commit 771aacb2ad.
2026-08-26 14:42:12 +00:00
github-actions[bot]
65e1329896 Revert "Update version and changelog for v3.37.8"
This reverts commit 0692998c5c.
2026-08-26 14:42:12 +00:00
github-actions[bot]
f37565646f Rebuild 2026-08-26 14:41:38 +00:00
github-actions[bot]
0f2e2bde5c Update changelog and version after v4.37.9 2026-08-26 14:41:24 +00:00
Paolo Tranquilli
cdf488f595 Merge pull request #4107 from github/update-v4.37.9-920ba7cd1
Merge main into releases/v4
2026-08-26 16:39:15 +02:00
github-actions[bot]
7243f38558 Update changelog for v4.37.9 2026-08-26 12:56:36 +00:00
Paolo Tranquilli
920ba7cd15 Merge pull request #4106 from github/update-bundle/codeql-bundle-v2.26.4
Update default bundle to 2.26.4
2026-08-26 08:49:41 +00:00
github-actions[bot]
ecfa6e1681 Add changelog note 2026-08-26 08:23:26 +00:00
github-actions[bot]
adcdf4a70d Update default bundle to codeql-bundle-v2.26.4 2026-08-26 08:23:20 +00:00
Paolo Tranquilli
b5383aad77 Merge remote-tracking branch 'origin/redsun82-linux-arm64-support' into redsun82-linux-arm64-support 2026-08-25 16:08:34 +02:00
Henry Mercer
9d89e2d1d6 Rebuild
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 628ce334-991a-4578-9c1b-93d2e96bbddb
2026-08-24 18:32:11 +01:00
Henry Mercer
bee82de8ba Add tools download durations to the init status report
Surface `tools_extraction_duration_ms` and `tools_total_duration_ms` from
both the `init` and `setup-codeql` actions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 628ce334-991a-4578-9c1b-93d2e96bbddb
2026-08-24 18:32:08 +01:00
Henry Mercer
c05e445696 Report tools download durations on both download paths
The streaming path reported no timings at all, so we have no data for the
path that most runs take. It now reports a total duration, which is also
populated on the download-then-extract path.

That path additionally reports the extraction duration, which was
previously computed but only logged.

`downloadDurationMs` keeps its existing meaning of time spent downloading
alone, so existing telemetry stays comparable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 628ce334-991a-4578-9c1b-93d2e96bbddb
2026-08-24 18:32:04 +01:00
Paolo Tranquilli
a952c0706c Merge branch 'main' into redsun82-linux-arm64-support 2026-08-24 10:55:44 +02:00
Paolo Tranquilli
97b7459e09 Merge remote-tracking branch 'origin/main' into redsun82-linux-arm64-support 2026-08-21 17:43:09 +02:00
Henry Mercer
486fec2a3e Merge pull request #4099 from github/update-supported-enterprise-server-versions
Update supported GitHub Enterprise Server versions
2026-08-21 12:54:32 +00:00
Henry Mercer
134624c67b Merge pull request #4101 from github/dependabot/npm_and_yarn/npm-minor-457d827a35
Bump the npm-minor group across 1 directory with 4 updates
2026-08-21 12:53:13 +00:00
Henry Mercer
ff43db8f98 Merge pull request #4103 from github/mergeback/v4.37.8-to-main-db488dde
Mergeback v4.37.8 refs/heads/releases/v4 into main
2026-08-21 12:51:15 +00:00
github-actions[bot]
4605e03a74 Rebuild 2026-08-21 12:26:00 +00:00
github-actions[bot]
099c869cad Update changelog and version after v4.37.8 2026-08-21 12:25:47 +00:00
dependabot[bot]
79a73408b4 Bump the npm-minor group across 1 directory with 4 updates
Bumps the npm-minor group with 4 updates in the / directory: [esbuild](https://github.com/evanw/esbuild), [globals](https://github.com/sindresorhus/globals), [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) and [tsx](https://github.com/privatenumber/tsx).


Updates `esbuild` from 0.28.1 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.1...v0.28.2)

Updates `globals` from 17.9.0 to 17.11.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.9.0...v17.11.0)

Updates `typescript-eslint` from 8.66.0 to 8.67.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.67.0/packages/typescript-eslint)

Updates `tsx` from 4.23.8 to 4.23.12
- [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.23.8...v4.23.12)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: globals
  dependency-version: 17.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.67.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-19 17:55:06 +00:00
github-actions[bot]
f9d9f07d37 Update supported GitHub Enterprise Server versions 2026-08-19 00:11:43 +00:00
Mario Campos
fcd8d74cda Refactor output-caching functions to accept file path dependency
This makes it easier to test. Credit to @mbg.
2026-08-14 14:41:04 -05:00
Mario Campos
b5d34388b7 Refactor tests in output-cache.test.ts to use named imports from output-cache 2026-08-14 14:28:17 -05:00
Mario Campos
1f46830a1f Fix VersionInfo derived type to pass linter 2026-08-14 14:24:37 -05:00
Mario Campos
b60777a215 Delete unnecessary statement from JSDoc comments 2026-08-14 14:05:42 -05:00
Mario Campos
f3deecb42d Refactor OutputCache with json module 2026-08-14 11:38:38 -05:00
Mario Campos
c8ba2d2a05 Refactor isVersionInfo with json module 2026-08-14 11:35:14 -05:00
Mario Campos
d7d901a925 Refactor tests to use getCommandCacheFilePath
As well as fixup a few other problems.
2026-08-14 10:56:07 -05:00
Mario Campos
cb9d39fcc1 Export getCommandCacheFilePath function for use in tests 2026-08-14 10:55:33 -05:00
Mario Campos
93d793c5fe Use json module for JSON validation in output-cache 2026-08-13 23:19:37 -05:00
dependabot[bot]
9875827ada Bump @octokit/types from 16.0.0 to 17.0.0
Bumps [@octokit/types](https://github.com/octokit/types.ts) from 16.0.0 to 17.0.0.
- [Release notes](https://github.com/octokit/types.ts/releases)
- [Commits](https://github.com/octokit/types.ts/compare/v16.0.0...v17.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 10:32:23 +00:00
Paolo Tranquilli
b4c390c362 Merge remote-tracking branch 'origin/main' into redsun82-linux-arm64-support 2026-08-10 09:51:23 +02:00
Paolo Tranquilli
43ae5e70b0 Address review comments
- Add darwin/x64 case to linked bundle tests and disambiguate the
  parametrized test title by platform/arch
- Reword CHANGELOG entry to clarify the CLI supports Linux Arm64
- Add a dedicated Linux Arm64 end-to-end PR check on ubuntu-24.04-arm

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 308c78bb-53a3-4877-a373-70fab87543f1
2026-08-05 15:51:01 +02:00
Paolo Tranquilli
4c96123771 Add support for linux/arm64
Recognize the linux/arm64 platform so the CodeQL CLI is accepted on
Arm64 Linux runners and the native per-platform bundle
codeql-bundle-linux-arm64.tar.zst is resolved, rather than falling
through to the combined all-platforms gzip fallback (which deliberately
omits arm64).

Add ["linux", "arm64"] to the supported platforms and make the linux
bundle name arch-aware. Keep the download mock faithful and cover the
new arch branch with tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-04 15:32:12 +02:00
83 changed files with 7520 additions and 2185 deletions

View File

@@ -1,14 +1,33 @@
# CodeQL Action - Copilot Instructions
The CodeQL Action is used in GitHub Actions workflows to run CodeQL scans using the CodeQL CLI.
## Overview
- The repository contains two TypeScript projects.
- The main TypeScript codebase is in the `src` directory, with accompanying unit tests in `.test.ts` files in the same directory.
- The main codebase is compiled to bundled JavaScript code, which is also contained in the repository in the `lib` directory.
- A secondary TypeScript codebase with scripts that are only used for development purposes or by CI is in the `pr-checks` directory. This codebase is not compiled to bundled JavaScript. It is executed directly with `tsx`, which handles compilation internally.
## Review instructions
- When wording review comments, be helpful and friendly. Assume that the PR author has written the code with the best of intentions. Word your comments constructively as suggestions for improvements. Do not word suggestions as commands.
- If you want to comment on a change that you believe will fail a CI check, do not present the CI failure you expect as a fact. Instead, write that you think a change "may" lead to a failure in CI. Suggest that, if such a failure manifests, the changes you are commenting on may be the place responsible for the failure and are worth looking at.
- If a suggestion you make is suitable for a follow-up, such as a refactoring that doesn't change the behaviour or fixing a typo in a comment, mention that it can be addressed in a later PR rather than blocking this one.
- If a change is a net improvement, for example because it improves on an existing limitation of existing code, do not complain about pre-existing problems that remain. You may comment on them, but you should make it clear that the thing you are commenting on is not new by writing e.g. "Not new in this PR, but [..]" followed by your description of the issue and a suggestion that it could be improved at the same time with e.g. "Consider whether this is worth addressing as part of this PR as well."
## Generated code
The CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from.
The main codebase of the CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from. The secondary TypeScript codebase has sources in the `pr-checks` directory, which are executed directly with `tsx` and not compiled to JavaScript in the `lib` folder.
GitHub Actions workflows in the `.github/workflows` directory whose filenames start with two underscores (e.g. `__all-platform-bundle.yml`) are automatically generated using the `pr-checks/sync.sh` script from template files in the `pr-checks/checks` directory. Therefore, you do not need to review files in the `.github/workflows` directory that start with two underscores. However, you should review changes to the `pr-checks` directory as well as workflows in the `.github/workflows` directory that do not start with underscores.
## Release process
Changes that are merged to the `main` branch are not used in production straight away. Instead, changes from `main` must first be released using the release process, which has to be manually triggered by a staff member. Therefore, it is possible for further changes to be made to the code after some changes have been merged to `main` but before they are released. Once the release process automation is started, a series of PRs are automatically generated:
- A release-PR is a PR which matches all of the following patterns:
- PR title starts with the exact prefix: `Merge main into releases/v`
- Equivalent regex: `^Merge main into releases\/v`
- AND head (source) branch matches the update-branch pattern:
@@ -17,5 +36,5 @@ GitHub Actions workflows in the `.github/workflows` directory whose filenames st
- Regex: `^releases\/v` (e.g. `releases/v3`)
- Consider all files in release-PRs to have already been reviewed and do not review them, except for the following two files:
- `CHANGELOG.md`
- `package.json`
- `CHANGELOG.md`
- `package.json`

View File

@@ -0,0 +1,16 @@
---
applyTo: "CHANGELOG.md,src/defaults.json,lib/defaults.json,src/api-compatibility.json"
---
# Merging release, mergeback, and backport PRs
The release process creates a cascade of PRs (`main` → `releases/vN`, then
`releases/vN` → `main` mergeback, then `releases/vN` → `releases/v(N-1)`
backport). These PRs reliably touch `CHANGELOG.md`, `src/defaults.json` /
`lib/defaults.json` (bundle/CLI version bump), and `src/api-compatibility.json`.
Such PRs **must be merged with a merge commit**. Never squash or rebase, as
that breaks the branch linkage the release automation relies on.
When arming auto-merge on these PRs, use `--merge` (e.g. `gh pr merge --merge`),
not `--squash` or `--rebase`.

View File

@@ -56,7 +56,7 @@ jobs:
include:
- os: ubuntu-latest
version: nightly-latest
- os: macos-latest
- os: macos-latest-xlarge
version: nightly-latest
- os: windows-latest
version: nightly-latest

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin

View File

@@ -80,7 +80,8 @@ jobs:
- id: init
uses: ./../action/init
with:
languages: javascript
# Request multiple languages so this check uses the combined bundle.
languages: javascript,python
tools: ${{ steps.prepare-test.outputs.tools-url }}
- uses: ./../action/analyze
with:

106
.github/workflows/__linux-arm64.yml generated vendored Normal file
View File

@@ -0,0 +1,106 @@
# Warning: This file is generated automatically, and should not be modified.
# Instead, please modify the template in the pr-checks directory and run:
# pr-checks/sync.sh
# to regenerate this file.
name: PR Check - Linux Arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GO111MODULE: auto
on:
push:
branches:
- main
- releases/v*
pull_request: {}
merge_group:
types:
- checks_requested
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
inputs:
dotnet-version:
type: string
description: The version of .NET to install
required: false
default: 9.x
go-version:
type: string
description: The version of Go to install
required: false
default: '>=1.21.0'
workflow_call:
inputs:
dotnet-version:
type: string
description: The version of .NET to install
required: false
default: 9.x
go-version:
type: string
description: The version of Go to install
required: false
default: '>=1.21.0'
defaults:
run:
shell: bash
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
jobs:
linux-arm64:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04-arm
version: nightly-latest
name: Linux Arm64
if: github.triggering_actor != 'dependabot[bot]'
permissions:
contents: read
security-events: read
timeout-minutes: 45
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
- name: Install Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: ${{ inputs.go-version || '>=1.21.0' }}
cache: false
- name: Prepare test
id: prepare-test
uses: ./.github/actions/prepare-test
with:
version: ${{ matrix.version }}
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- uses: ./../action/init
with:
languages: ${{ env.LANGUAGES }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Build code
run: ./build.sh
- uses: ./../action/analyze
with:
upload-database: false
- name: Assert databases exist
run: |
cd "$RUNNER_TEMP/codeql_databases"
for lang in ${LANGUAGES//,/ }; do
if [[ ! -d "$lang" ]]; then
echo "Did not find a database for $lang"
exit 1
fi
echo "Found database for $lang"
done
env:
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
CODEQL_ACTION_TEST_MODE: true

View File

@@ -191,5 +191,6 @@ jobs:
exit 1
fi
env:
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
CODEQL_ACTION_TEST_MODE: true

164
.github/workflows/__per-language-bundle-validation.yml generated vendored Normal file
View File

@@ -0,0 +1,164 @@
# Warning: This file is generated automatically, and should not be modified.
# Instead, please modify the template in the pr-checks directory and run:
# pr-checks/sync.sh
# to regenerate this file.
name: PR Check - Per-language bundles
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GO111MODULE: auto
on:
push:
branches:
- main
- releases/v*
pull_request: {}
merge_group:
types:
- checks_requested
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
inputs: {}
workflow_call:
inputs: {}
defaults:
run:
shell: bash
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: per-language-bundle-validation-${{github.ref}}
jobs:
per-language-bundle-validation:
strategy:
fail-fast: false
matrix:
include:
- language: actions
os: ubuntu-latest
version: nightly-latest
expected-extractors: actions javascript
- language: cpp
os: ubuntu-latest
version: nightly-latest
build-mode: manual
build-command: gcc -o main main.c
- language: csharp
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: go
os: ubuntu-latest
version: nightly-latest
build-mode: autobuild
- language: java
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: javascript
os: ubuntu-latest
version: nightly-latest
- language: python
os: ubuntu-latest
version: nightly-latest
- language: ruby
os: ubuntu-latest
version: nightly-latest
- language: rust
os: ubuntu-latest
version: nightly-latest
- language: swift
os: macos-latest-xlarge
version: nightly-latest
build-mode: autobuild
name: Per-language bundles
if: github.triggering_actor != 'dependabot[bot]'
permissions:
contents: read
security-events: read
timeout-minutes: 45
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Prepare test
id: prepare-test
uses: ./.github/actions/prepare-test
with:
version: ${{ matrix.version }}
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- uses: ./../action/init
id: init
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix['build-mode'] }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains only the expected extractors
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
LANGUAGE: ${{ matrix.language }}
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
run: |
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
echo "Extractors in the bundle:"
echo "$extractors"
echo "Expected: $EXPECTED_EXTRACTORS"
for expected in $EXPECTED_EXTRACTORS; do
if ! echo "$extractors" | grep -qx "$expected"; then
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
exit 1
fi
done
# If the bundle contained extractors beyond those the language needs, then it would not
# have been trimmed, and this job would be silently validating the combined bundle.
for other in actions cpp csharp go java javascript python ruby rust swift; do
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
continue
fi
if echo "$extractors" | grep -qx "$other"; then
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
exit 1
fi
done
- name: Check that the bundle was not added to the toolcache
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# A bundle that is missing most of its extractors must never be left in the toolcache,
# where a later job analyzing a different language could pick it up. The runner image
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
# rather than whether the toolcache contains CodeQL at all.
echo "CodeQL is at $CODEQL_PATH"
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
exit 1
fi
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
exit 1
fi
- name: Build code
if: matrix['build-command']
run: ${{ matrix['build-command'] }}
- uses: ./../action/analyze
id: analysis
with:
upload-database: false
- name: Check that a database was created for the language
env:
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
LANGUAGE: ${{ matrix.language }}
run: |
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
if [ -z "$database" ] || [ ! -d "$database" ]; then
echo "::error::No CodeQL database was created for ${LANGUAGE}."
echo "Databases: $DB_LOCATIONS"
exit 1
fi
echo "Created a ${LANGUAGE} database at ${database}."
env:
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
CODEQL_ACTION_TEST_MODE: true

View File

@@ -54,7 +54,7 @@ jobs:
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Set up Ruby
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -54,11 +54,11 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-latest
- os: macos-latest-xlarge
version: linked
- os: macos-latest
- os: macos-latest-xlarge
version: default
- os: macos-latest
- os: macos-latest-xlarge
version: nightly-latest
name: Swift analysis using a custom build command
if: github.triggering_actor != 'dependabot[bot]'

View File

@@ -75,7 +75,8 @@ jobs:
uses: ./../action/.github/actions/check-codescanning-config
with:
expected-config-file-contents: "{}"
languages: javascript
# Request multiple languages so later checks can reuse the combined bundle.
languages: javascript,python
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Packs from input

View File

@@ -45,7 +45,7 @@ jobs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache: "npm"
- name: Install dependencies
run: |
@@ -67,7 +67,12 @@ jobs:
- name: Upload sarif
uses: ./upload-sarif
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
# The merge queue deletes its `gh-readonly-queue` ref as soon as the queue entry resolves,
# so uploading against it races with that deletion. Both the `merge_group` run and the
# paired `push` run that the queue branch creates use that ref, so gate on the ref itself
# rather than the event. The same results are uploaded by the `pull_request` run and again
# by the `push` run on `main`.
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/')
with:
sarif_file: eslint.sarif
category: eslint
@@ -94,7 +99,7 @@ jobs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: 'npm'
cache: "npm"
- name: Install dependencies
id: install-deps
@@ -109,6 +114,10 @@ jobs:
working-directory: pr-checks
run: npx tsx --test
- name: Run `pr-checks/changenotes.mts` to ensure that all unreleased change notes are valid
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
run: npx tsx pr-checks/changenotes.mts validate
- name: Verify all Actions use the same Node version
id: head-version
run: |
@@ -158,14 +167,14 @@ jobs:
path: ${{ runner.temp }}/repo-size/
if-no-files-found: error
- name: 'Backport: Check out base ref'
- name: "Backport: Check out base ref"
id: checkout-base
if: ${{ startsWith(github.head_ref, 'backport-') }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.base_ref }}
- name: 'Backport: Verify Node versions unchanged'
- name: "Backport: Verify Node versions unchanged"
if: steps.checkout-base.outcome == 'success'
env:
HEAD_VERSION: ${{ steps.head-version.outputs.node_version }}

View File

@@ -2,6 +2,24 @@
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
## 3.38.2 - 24 Sept 2026
- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)
## 3.38.1 - 18 Sept 2026
- The CodeQL Action now has experimental support for CodeQL releases for which per-language bundles are available. Per-language bundles support analysis for a single language and are therefore smaller than the combined bundles that allow analysis for all supported languages. As a result, per-language bundles take up less space on disk and are faster to download. We expect to roll this change out to everyone in the coming weeks. [#4146](https://github.com/github/codeql-action/pull/4146)
## 3.38.0 - 09 Sept 2026
- On GitHub-hosted runners, the CodeQL Action now deletes unused CodeQL bundles from the toolcache before downloading a different bundle, which frees up disk space for the analysis. We expect to roll this change out to everyone in September. [#4124](https://github.com/github/codeql-action/pull/4124)
- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072)
- Update default CodeQL bundle version to [2.27.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.0). [#4129](https://github.com/github/codeql-action/pull/4129)
## 3.37.9 - 26 Aug 2026
- Update default CodeQL bundle version to [2.26.4](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.4). [#4106](https://github.com/github/codeql-action/pull/4106)
## 3.37.8 - 21 Aug 2026
No user facing changes.

View File

@@ -60,10 +60,13 @@ Here are a few things you can do that will increase the likelihood of your pull
This workflow goes through the pull requests that have been merged to `main` since the last release, creates a changelog, then opens a pull request to merge the changes since the last release into the `releases/v3` release branch.
You can start a release by triggering this workflow via [workflow dispatch](https://github.com/github/codeql-action/actions/workflows/update-release-branch.yml).
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it.
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it **with a merge commit** (`gh pr merge --merge`).
1. When the "Merge main into releases/v3" pull request is merged into the `releases/v3` branch, a mergeback pull request to `main` will be automatically created. This mergeback pull request incorporates the changelog updates into `main`, tags the release using the merge commit of the "Merge main into releases/v3" pull request, and bumps the patch version of the CodeQL Action.
1. If a backport to an older major version is required, a pull request targeting that version's branch will also be automatically created.
1. Approve the mergeback and backport pull request (if applicable) and automerge them.
1. Approve the mergeback and backport pull request (if applicable) and automerge them **with a merge commit** (`gh pr merge --merge`).
> [!NOTE]
> The release, mergeback, and backport pull requests must always be merged with a merge commit — **never squash or rebase**. The mergeback tags the release using the merge commit of the "Merge main into releases/v3" pull request, so squashing or rebasing breaks tagging and the branch linkage the release automation relies on.
Once the mergeback and backport pull request have been merged, the release is complete.

View File

@@ -158,7 +158,7 @@ export default [
},
},
{
files: ["**/*.ts", "**/*.js"],
files: ["**/*.ts", "**/*.js", "**/*.mts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
@@ -180,7 +180,7 @@ export default [
},
},
{
files: ["pr-checks/**/*.ts"],
files: ["pr-checks/**/*.ts", "pr-checks/**/*.mts"],
languageOptions: {
parserOptions: {

View File

@@ -164,6 +164,13 @@ inputs:
[Internal] The ID of the check run, as provided by the Actions runtime environment. Do not set this value manually.
default: ${{ job.check_run_id }}
required: false
job-status:
description: >-
[Internal] The status of the job, as provided by the Actions runtime environment. This is how the
post step learns whether the job as a whole succeeded, failed, or was cancelled. Do not set this
value manually.
default: ${{ job.status }}
required: false
outputs:
codeql-path:
description: The path of the CodeQL binary used for analysis

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.26.3",
"cliVersion": "2.26.3",
"priorBundleVersion": "codeql-bundle-v2.26.2",
"priorCliVersion": "2.26.2"
"bundleVersion": "codeql-bundle-v2.27.1",
"cliVersion": "2.27.1",
"priorBundleVersion": "codeql-bundle-v2.27.0",
"priorCliVersion": "2.27.0"
}

3058
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

1505
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "3.37.8",
"version": "3.38.2",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -30,28 +30,28 @@
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@octokit/core": "^7.0.7",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/core": "^7.0.8",
"@octokit/plugin-paginate-rest": "^15.0.0",
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"@octokit/plugin-retry": "^8.1.1",
"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.2.3",
"js-yaml": "^5.4.2",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
"semver": "^7.8.5",
"uuid": "^14.0.1",
"uuid": "^14.0.2",
"undici": "^6.28.0"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
"@eslint/compat": "^2.1.0",
"@eslint/compat": "^2.1.1",
"@microsoft/eslint-formatter-sarif": "^3.1.0",
"@octokit/types": "^16.0.0",
"@octokit/types": "^18.0.0",
"@types/archiver": "^8.0.0",
"@types/follow-redirects": "^1.14.4",
"@types/js-yaml": "^4.0.9",
@@ -61,19 +61,19 @@
"@types/semver": "^7.8.0",
"@types/sinon": "^22.0.0",
"ava": "^6.4.1",
"esbuild": "^0.28.1",
"esbuild": "^0.28.2",
"eslint": "^9.39.5",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-github": "^6.1.2",
"eslint-plugin-import-x": "^4.17.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-jsdoc": "^64.5.2",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.9.0",
"globals": "^17.12.0",
"nock": "^14.0.17",
"sinon": "^22.1.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.66.0"
"typescript-eslint": "^8.70.0"
},
"overrides": {
"@actions/tool-cache": {

View File

@@ -1,10 +1,7 @@
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";
/** The type of the Octokit client. */
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
/** Constructs an `ApiClient` using `token` for authentication. */
export function getApiClient(token: string): ApiClient {

View File

@@ -112,7 +112,7 @@ ${NO_CHANGES_STR}`;
describe("updateChangelog", async () => {
await it("removes `NO_CHANGES_STR` if present in [UNRELEASED] section", async () => {
const result = updateChangelog(EMPTY_CHANGELOG, "");
assert.ok(!result.includes(NO_CHANGES_STR.trim()));
assert.ok(!result.includes(NO_CHANGES_STR));
});
await it("doesn't remove `NO_CHANGES_STR` if present in versioned section", async () => {
@@ -120,7 +120,7 @@ describe("updateChangelog", async () => {
EMPTY_CHANGELOG.replace(UNRELEASED_PLACEHOLDER, "1.2.3"),
"",
);
assert.ok(result.includes(NO_CHANGES_STR.trim()));
assert.ok(result.includes(NO_CHANGES_STR));
});
await it("throws if there are no sections", async () => {

View File

@@ -9,17 +9,46 @@ import * as fs from "node:fs";
import { describe, it } from "node:test";
import {
addBodyLinesToUnreleasedSection,
ChangelogSection,
EMPTY_CHANGELOG,
getHeader,
getReleaseDateString,
NO_CHANGES_STR,
parseChangelog,
processChangelogForBackports,
renderChangelog,
setVersionAndDate,
UNRELEASED_PLACEHOLDER,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";
const testDate = new Date(2026, 7, 14);
describe("getHeader", async () => {
function Section(headerLine: string): ChangelogSection {
return {
headerLine,
bodyLines: [],
};
}
await it("returns non-headers unchanged", () => {
assert.equal("foo", getHeader(Section("foo")));
assert.equal("- bar", getHeader(Section("- bar")));
});
await it("strips octothorpes", async () => {
assert.equal("foo", getHeader(Section("# foo")));
assert.equal("foo", getHeader(Section("## foo")));
assert.equal("foo", getHeader(Section("### foo")));
assert.equal("foo", getHeader(Section("#### foo")));
assert.equal("foo", getHeader(Section("##### foo")));
assert.equal("foo", getHeader(Section("###### foo")));
});
await it("strips whitespace", async () => {
assert.equal("foo", getHeader(Section("# foo ")));
});
});
describe("getReleaseDateString", async () => {
await it("formats dates as expected", async () => {
assert.equal(getReleaseDateString(testDate), "14 Aug 2026");
@@ -70,3 +99,73 @@ describe("processChangelogForBackports", async () => {
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
});
});
describe("addBodyLinesToUnreleasedSection", async () => {
function newChangelogWithSections(sections: ChangelogSection[]) {
return {
preamble: [],
sections,
};
}
await it("throws error if '[UNRELEASED]' section is not first", async () => {
const invalidChangelog = newChangelogWithSections([
{
headerLine: "## Release 1.0.0",
bodyLines: [],
},
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: [],
},
]);
assert.throws(() =>
addBodyLinesToUnreleasedSection(invalidChangelog, ["foo"]),
);
});
await it("overwrites 'No user facing changes.'", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
addBodyLinesToUnreleasedSection(changelog, ["- foo"]);
assert.equal(changelog.sections[0].bodyLines.length, 3);
assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]);
});
await it("does nothing if lines is empty", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
const changelogClone = structuredClone(changelog);
addBodyLinesToUnreleasedSection(changelog, []);
assert.deepEqual(changelog, changelogClone);
});
await it("inserts a line", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", "- Added a new dependency.", ""],
},
]);
const lineToInsert = "- foo";
addBodyLinesToUnreleasedSection(changelog, [lineToInsert]);
assert.equal(changelog.sections[0].bodyLines.length, 4);
assert.ok(
changelog.sections[0].bodyLines.some((line) => line === lineToInsert),
);
});
});

View File

@@ -6,14 +6,16 @@ import { CHANGELOG_FILE, DryRunOption } from "./config";
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
/** The default contents for a section in the changelog. */
export const NO_CHANGES_STR = "No user facing changes.\n\n";
export const NO_CHANGES_STR = "No user facing changes.";
/** Placeholder changelog content for a new release. */
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
## ${UNRELEASED_PLACEHOLDER}
${NO_CHANGES_STR}`;
${NO_CHANGES_STR}
`;
/**
* Represents sections in a changelog.
@@ -31,6 +33,13 @@ export interface Changelog {
sections: ChangelogSection[];
}
/**
* Returns the text of the header (without the '## ' prefix) of the given section.
* */
export function getHeader(section: ChangelogSection): string {
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
}
/** Returns `date` formatted as `DD Mon YYYY`. */
export function getReleaseDateString(today: Date = new Date()): string {
return today.toLocaleDateString("en-GB", {
@@ -125,6 +134,42 @@ export function parseChangelog(content: string): Changelog {
return { preamble, sections };
}
/**
* Inserts the changenotes `lines` in the `[UNRELEASED]` section of `changelog`.
* If the section contains the stock message {@link NO_CHANGES_STR}, then
* `lines` will be inserted in place and the stock message will be deleted.
*
* @throws Error -- if the [UNRELEASED] section does not exist.
*
* @param changelog The CHANGELOG object to modify.
* @param lines The changenotes to insert.
*/
export function addBodyLinesToUnreleasedSection(
changelog: Changelog,
lines: string[],
) {
// Do nothing if there is nothing to insert.
if (lines.length === 0) return;
const unreleasedSection = changelog.sections[0];
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
throw Error(
`'${UNRELEASED_PLACEHOLDER}' is not the first section of 'CHANGELOG.md'`,
);
}
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
unreleasedSection.bodyLines = ["", ...lines, ""];
return;
}
// The last body line should be a blank line (for spacing).
// Remove it so that we can add `lines` and then add the blank line back.
unreleasedSection.bodyLines.pop();
unreleasedSection.bodyLines.push(...lines);
unreleasedSection.bodyLines.push("");
}
/**
* Combines an array of lines into a single string by adding line breaks.
*/
@@ -204,7 +249,7 @@ export function processChangelogForBackports(
// Add an entry if we didn't keep any.
if (!foundContent) {
section.bodyLines.push(NO_CHANGES_STR.trim());
section.bodyLines.push(NO_CHANGES_STR);
}
}

View File

@@ -0,0 +1,121 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { matter } from "lite-matter";
import type { List, ListItem } from "mdast";
import { fromMarkdown } from "mdast-util-from-markdown";
// Regex for filename: YYYY-MM-DD-id.md
const VALID_CHANGE_NOTE_FILENAME_PATTERN =
/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/;
export const VALID_CHANGE_NOTE_CATEGORIES = {
breaking: "Breaking Changes",
feature: "New Features",
improvement: "Improvements",
securityFix: "Security Fixes",
fix: "Bug Fixes",
unship: "Removed Features",
deprecation: "Deprecations",
knownIssue: "Known Issues",
misc: "Miscellaneous",
};
/**
* Validates that the given Markdown string meets the criteria for a change-note, which is:
* - A single unordered list
* - Each list item must start with a hyphen (-)
* - No other Markdown elements are allowed
* @param content The Markdown string to validate
* @returns True if the string is a valid change-note, false otherwise
*/
export function isValidChangenoteContent(content: string): boolean {
const ast = fromMarkdown(content);
const lines = content.split("\n");
function listHasHyphenBullets(node: List | ListItem): boolean {
if (node.type === "list") {
return node.children.every(listHasHyphenBullets);
}
const line = lines[node.position!.start.line - 1].trim();
return (
line.startsWith("-") &&
node.children.every(
(child) => child.type !== "list" || listHasHyphenBullets(child),
)
);
}
return (
ast.children.length === 1 &&
ast.children[0].type === "list" &&
ast.children[0].ordered === false &&
listHasHyphenBullets(ast.children[0])
);
}
/**
* Validates that the given filename meets the criteria for a change-note filename.
* @param filename The name of the change-note file to validate.
* @returns True if the filename is valid, false otherwise.
*/
export function isValidChangenoteFilename(filename: string): boolean {
return filename.match(VALID_CHANGE_NOTE_FILENAME_PATTERN) !== null;
}
/**
* Validates that the given frontmatter has a valid change-note category.
* @param frontmatter The frontmatter object to validate.
* @returns True if the frontmatter has a valid category, false otherwise.
*/
export function hasValidChangenoteCategory(
frontmatter: Record<string, unknown>,
): boolean {
const category = frontmatter["category"];
return (
typeof category === "string" &&
Object.hasOwn(VALID_CHANGE_NOTE_CATEGORIES, category)
);
}
/**
* Validates that the given change-note file meets all of the criteria for a change-note.
* @param filename The name of the change-note file to validate.
* @returns True if the file is a valid change-note, false otherwise.
*/
export function isValidChangenoteFile(filename: string): boolean {
let isValid: boolean = true;
let fileData: string | undefined;
try {
fileData = fs.readFileSync(filename, "utf8");
} catch (error) {
console.error(`${filename}: failed to read file`, error);
return false;
}
const { data: frontmatter, content } = matter(fileData);
if (!isValidChangenoteFilename(path.basename(filename))) {
isValid = false;
console.error(
`${filename}: invalid filename; must match pattern YYYY-MM-DD-id.md`,
);
}
if (!hasValidChangenoteCategory(frontmatter)) {
isValid = false;
const categories = Object.keys(VALID_CHANGE_NOTE_CATEGORIES).join(", ");
console.error(
`${filename}: invalid category; must be one of: ${categories}`,
);
}
if (!isValidChangenoteContent(content)) {
isValid = false;
console.error(
`${filename}: invalid Markdown; content must be a single unordered list with hyphen bullets and no other Markdown elements`,
);
}
return isValid;
}

View File

@@ -0,0 +1,186 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { withTmpFile } from "../../src/util";
import {
hasValidChangenoteCategory,
isValidChangenoteContent,
isValidChangenoteFile,
isValidChangenoteFilename,
VALID_CHANGE_NOTE_CATEGORIES,
} from "./validate.mjs";
await describe("isValidChangenoteContent", async () => {
await it("recognizes an unordered Markdown list", () => {
const inputs = [
"- One changenote entry",
"- First item\n- Second item",
"\n\n\n\n- Fixed a bug\n- Added a feature",
];
for (const input of inputs) {
assert.equal(isValidChangenoteContent(input), true);
}
});
await it("does not recognize non-Markdown text", () => {
const inputs = [
"This is not a list.",
'["this", "is", "JSON"]',
"---",
"***",
"___",
"paragraph",
];
for (const input of inputs) {
assert.equal(isValidChangenoteContent(input), false);
}
});
await it("does not recognize ordered Markdown lists", () => {
const inputs = [
"1. First item\n2. Second item",
"\n\n\n1. First item\n1. Second item",
];
for (const input of inputs) {
assert.equal(isValidChangenoteContent(input), false);
}
});
await it("requires all list items to use a hyphen bullet", () => {
const inputs = [
"* Fixed a bug\n* Added feature",
"+ Fixed a bug\n+ Added feature",
"- Fixed a bug\n* Added feature",
"- Fixed a bug\n+ Added feature",
"- Fixed a bug\n * Added feature\n + Updated docs",
"\n\n\n* Fixed a bug",
"\n\n\n+ Fixed a bug",
"---\n* Fixed a bug\n* Added feature",
] as const;
for (const input of inputs) {
assert.equal(isValidChangenoteContent(input), false);
}
});
await it("does not contain other Markdown elements", () => {
const inputs = [
"- Fixed a bug\n\nParagraph of text",
"- Fixed a bug\n\n* Added a feature",
"# Header\n- Fixed a bug",
"- Fixed a bug\n## Subheader",
];
for (const input of inputs) {
assert.equal(isValidChangenoteContent(input), false);
}
});
});
await describe("isValidChangenoteFilename", async () => {
await it("accepts valid filenames", () => {
const inputs = [
"2023-01-01-fix-bug.md",
"2023-12-31-add-feature.md",
"2023-06-15-update-docs.md",
];
for (const input of inputs) {
assert.equal(isValidChangenoteFilename(input), true);
}
});
await it("rejects invalid filenames", () => {
const inputs = [
"missing-date-from-filename.md",
"2021-01-01.md",
"2026-12-19-wrong-file-name-extension.txt",
];
for (const input of inputs) {
assert.equal(isValidChangenoteFilename(input), false);
}
});
});
await describe("hasValidChangenoteCategory", async () => {
await it("accepts valid categories", () => {
for (const category of Object.keys(VALID_CHANGE_NOTE_CATEGORIES)) {
const frontmatter = { category };
assert.equal(hasValidChangenoteCategory(frontmatter), true);
}
});
await it("rejects invalid categories", () => {
const inputs = [
"",
"invalid-category",
"bug-fix",
"new-feature",
"security-patch",
"miscellaneous",
"documentation",
];
for (const category of inputs) {
const frontmatter = { category };
assert.equal(hasValidChangenoteCategory(frontmatter), false);
}
});
await it("reject missing category", () => {
assert.equal(hasValidChangenoteCategory({}), false);
assert.equal(hasValidChangenoteCategory({ category: null }), false);
assert.equal(hasValidChangenoteCategory({ category: undefined }), false);
});
});
await describe("isValidChangenoteFile", async () => {
await it("accepts a valid change-note file", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",
"---\ncategory: fix\n---\n- Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), true);
},
);
});
await it("rejects a non-existent path", async () => {
assert.equal(isValidChangenoteFile("non-existent-file.md"), false);
});
await it("rejects invalid filename", async () => {
await withTmpFile(
"fix-bug.md",
"---\ncategory: fix\n---\n- Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
await it("rejects missing frontmatter", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",
"- Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
await it("rejects invalid Markdown", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",
"---\ncategory: fix\n---\n* Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
});

134
pr-checks/changenotes.mts Executable file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env npx tsx
import * as fs from "node:fs";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import path from "path";
import { ExitCode } from "@actions/core";
import { matter } from "lite-matter";
import {
addBodyLinesToUnreleasedSection,
parseChangelog,
renderChangelog,
withChangelog,
} from "./changelog";
import { isValidChangenoteFile } from "./changelog/validate.mjs";
import { CHANGENOTES_DIR } from "./config";
/**
* Describes a changenote file, including its file path, frontmatter, and content.
*/
interface ChangenoteFile {
absolutePath: string;
data: Record<string, any>;
content: string;
}
/**
* Returns the absolute file paths of all files in
* {@link CHANGENOTES_DIR} (except ".gitkeep").
* */
function listUnreleasedChangenoteDir(): string[] {
return fs
.readdirSync(CHANGENOTES_DIR)
.filter((name) => name !== ".gitkeep")
.map((name) => path.join(CHANGENOTES_DIR, name));
}
/**
* Scans the {@link CHANGENOTES_DIR} directory for changenote files
* and returns a parsed listing of those changenote files.
*/
function getChangenotes(): ChangenoteFile[] {
return listUnreleasedChangenoteDir().map((absolutePath) => {
return {
absolutePath,
...matter(fs.readFileSync(absolutePath, "utf-8")),
};
});
}
const entryPoint = process.argv[1];
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
try {
process.exit(main());
} catch (error) {
console.error(error);
process.exit(ExitCode.Failure);
}
}
function main(): ExitCode {
const { positionals } = parseArgs({
allowPositionals: true,
strict: true,
});
const [command] = positionals;
switch (command) {
case undefined:
case "help":
return usage();
case "assemble":
return assemble();
case "validate":
return validate();
default:
console.error(`Unknown command: ${command}`);
return ExitCode.Failure;
}
}
function usage(): ExitCode {
const message =
"Usage: changenotes.mts assemble\n" +
" changenotes.mts validate\n" +
" changenotes.mts help";
console.log(message);
return ExitCode.Success;
}
function assemble(): ExitCode {
try {
const changenotes = getChangenotes();
const changenoteBodies = changenotes.map((c) => c.content);
const changenotePaths = changenotes.map((c) => c.absolutePath);
withChangelog((contents) => {
const changelog = parseChangelog(contents);
addBodyLinesToUnreleasedSection(changelog, changenoteBodies);
return renderChangelog(changelog);
}, {});
// Delete changenotes only after successful processing.
for (const p of changenotePaths) {
fs.unlinkSync(p);
}
return ExitCode.Success;
} catch (e) {
console.error("Failed to assemble changenotes to 'CHANGELOG.md'", e);
}
return ExitCode.Failure;
}
function validate(): ExitCode {
try {
const allChangenotesValid = getChangenotes().reduce(
(r, changenote) => r && isValidChangenoteFile(changenote.absolutePath),
true,
);
if (allChangenotesValid) {
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
return ExitCode.Success;
}
} catch (error) {
console.error(
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
error,
);
}
return ExitCode.Failure;
}

View File

@@ -2,7 +2,8 @@ name: "All-platform bundle"
description: "Tests using an all-platform CodeQL Bundle"
operatingSystems:
- ubuntu
- macos
- os: macos
runner-image: macos-latest-xlarge
- windows
versions:
- nightly-latest

View File

@@ -30,7 +30,8 @@ steps:
- id: init
uses: ./../action/init
with:
languages: javascript
# Request multiple languages so this check uses the combined bundle.
languages: javascript,python
tools: ${{ steps.prepare-test.outputs.tools-url }}
- uses: ./../action/analyze
with:

View File

@@ -0,0 +1,35 @@
name: "Linux Arm64"
description: "An end-to-end integration test running on a Linux Arm64 runner, checking that the native linux-arm64 CodeQL bundle is downloaded and can analyze interpreted and compiled code"
operatingSystems:
- os: ubuntu
runner-image: ubuntu-24.04-arm
# The native linux-arm64 CodeQL bundle is only available in recent CLI releases, so we restrict this
# check to `nightly-latest`, which is guaranteed to ship it. Older stable versions do not have an
# arm64 asset, and `prepare-test` would resolve an x64 bundle URL for them on this runner.
versions:
- nightly-latest
installGo: true
installDotNet: true
# The set of languages CodeQL supports on this platform, excluding Swift (macOS only).
env:
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
steps:
- uses: ./../action/init
with:
languages: ${{ env.LANGUAGES }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Build code
run: ./build.sh
- uses: ./../action/analyze
with:
upload-database: false
- name: Assert databases exist
run: |
cd "$RUNNER_TEMP/codeql_databases"
for lang in ${LANGUAGES//,/ }; do
if [[ ! -d "$lang" ]]; then
echo "Did not find a database for $lang"
exit 1
fi
echo "Found database for $lang"
done

View File

@@ -15,6 +15,7 @@ operatingSystems:
- stable-v2.21.4
- stable-v2.22.4
env:
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
installGo: true
installDotNet: true

View File

@@ -0,0 +1,117 @@
name: Per-language bundles
description: Validates extraction and analysis using each per-language CodeQL bundle.
# TODO: Use a released bundle once releases include per-language bundles.
matrix:
include:
- language: actions
os: ubuntu-latest
version: nightly-latest
# Actions also needs the JavaScript extractor.
expected-extractors: actions javascript
- language: cpp
os: ubuntu-latest
version: nightly-latest
build-mode: manual
build-command: gcc -o main main.c
- language: csharp
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: go
os: ubuntu-latest
version: nightly-latest
build-mode: autobuild
- language: java
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: javascript
os: ubuntu-latest
version: nightly-latest
- language: python
os: ubuntu-latest
version: nightly-latest
- language: ruby
os: ubuntu-latest
version: nightly-latest
- language: rust
os: ubuntu-latest
version: nightly-latest
- language: swift
os: macos-latest-xlarge
version: nightly-latest
build-mode: autobuild
env:
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
steps:
- uses: ./../action/init
id: init
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix['build-mode'] }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains only the expected extractors
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
LANGUAGE: ${{ matrix.language }}
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
run: |
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
echo "Extractors in the bundle:"
echo "$extractors"
echo "Expected: $EXPECTED_EXTRACTORS"
for expected in $EXPECTED_EXTRACTORS; do
if ! echo "$extractors" | grep -qx "$expected"; then
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
exit 1
fi
done
# If the bundle contained extractors beyond those the language needs, then it would not
# have been trimmed, and this job would be silently validating the combined bundle.
for other in actions cpp csharp go java javascript python ruby rust swift; do
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
continue
fi
if echo "$extractors" | grep -qx "$other"; then
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
exit 1
fi
done
- name: Check that the bundle was not added to the toolcache
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# A bundle that is missing most of its extractors must never be left in the toolcache,
# where a later job analyzing a different language could pick it up. The runner image
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
# rather than whether the toolcache contains CodeQL at all.
echo "CodeQL is at $CODEQL_PATH"
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
exit 1
fi
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
exit 1
fi
- name: Build code
if: matrix['build-command']
run: ${{ matrix['build-command'] }}
- uses: ./../action/analyze
id: analysis
with:
upload-database: false
- name: Check that a database was created for the language
env:
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
LANGUAGE: ${{ matrix.language }}
run: |
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
if [ -z "$database" ] || [ ! -d "$database" ]; then
echo "::error::No CodeQL database was created for ${LANGUAGE}."
echo "Databases: $DB_LOCATIONS"
exit 1
fi
echo "Created a ${LANGUAGE} database at ${database}."

View File

@@ -5,7 +5,7 @@ versions:
- default
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -5,7 +5,8 @@ versions:
- default
- nightly-latest
operatingSystems:
- macos
- os: macos
runner-image: macos-latest-xlarge
installGo: true
installDotNet: true
env:

View File

@@ -18,6 +18,9 @@ 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 unreleased change-notes directory. */
export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
/** The path to the esbuild metadata file. */
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");

View File

@@ -1,17 +1,19 @@
{
"private": true,
"description": "Dependencies for the sync.ts",
"description": "Dependencies for codeql-action scripts",
"dependencies": {
"@actions/core": "^2.0.3",
"@actions/github": "^8.0.1",
"@octokit/core": "^7.0.7",
"@octokit/plugin-paginate-rest": ">=9.2.2",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@octokit/core": "^7.0.8",
"@octokit/plugin-paginate-rest": ">=15.0.0",
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"lite-matter": "^0.1.2",
"mdast-util-from-markdown": "^2.0.3",
"semver": "^7.8.5",
"yaml": "^2.9.0"
"yaml": "^2.9.1"
},
"devDependencies": {
"@types/node": "^20.19.43",
"tsx": "^4.23.8"
"tsx": "^4.23.13"
}
}

View File

@@ -4,12 +4,19 @@ set -e
cd "$(dirname "$0")"
# Run `npm ci` in CI or `npm install` otherwise.
#
# `pr-checks` is an npm workspace of the repository root and the two share a single hoisted
# `node_modules` directory. Running npm from this directory puts it in workspace mode, where it
# ignores the root project's own dependencies by default. `npm ci` would then rebuild the shared
# `node_modules` with only this workspace's dependencies, removing the root's ones, which breaks
# anything that imports from `src` (such as `sync.ts` itself). `--include-workspace-root` keeps the
# root project's dependencies in the installed tree.
if [ "$GITHUB_ACTIONS" = "true" ]; then
echo "In Actions, running 'npm ci' for 'sync.ts'..."
npm ci
npm ci --include-workspace-root
else
echo "Running 'npm install' for 'sync.ts'..."
npm install --no-audit --no-fund
npm install --no-audit --no-fund --include-workspace-root
fi
npx tsx sync.ts

View File

@@ -79,6 +79,8 @@ interface Specification extends JobSpecification {
useAllPlatformBundle?: string;
/** Values for the `analysis-kinds` matrix dimension. */
analysisKinds?: string[];
/** Overrides the generated job matrix using GitHub Actions matrix syntax. */
matrix?: Record<string, unknown>;
/** Container image configuration for the job. */
container?: any;
@@ -253,8 +255,8 @@ const languageSetups: LanguageSetups = {
name: "Install Java",
uses: pinnedUses(
"actions/setup-java",
"b6effb05e454b25005698d916606bdc6ffcbf961",
"v5.7.0",
"de7274f081f381c8f8158605e0321c36c376e2e6",
"v6.0.1",
),
with: {
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
@@ -512,9 +514,6 @@ function generateJob(
specDocument: yaml.Document,
checkSpecification: Specification,
) {
const matrix: Array<Record<string, any>> =
generateJobMatrix(checkSpecification);
const useAllPlatformBundle = checkSpecification.useAllPlatformBundle
? checkSpecification.useAllPlatformBundle
: "false";
@@ -567,8 +566,8 @@ function generateJob(
const checkJob: Record<string, any> = {
strategy: {
"fail-fast": false,
matrix: {
include: matrix,
matrix: checkSpecification.matrix ?? {
include: generateJobMatrix(checkSpecification),
},
},
name: checkSpecification.name,

View File

@@ -6,8 +6,8 @@
"module": "preserve",
"rootDir": "..",
"sourceMap": false,
"noEmit": true,
"noEmit": true
},
"include": ["./*.ts", "../src/**/*.ts"],
"include": ["./**/*.ts", "./**/*.mts", "../src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -7,7 +7,7 @@ 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 { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Logger } from "./logging";
import {
doesDirectoryExist,
@@ -283,6 +283,19 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
}
/**
* Whether the job is running on a runner that GitHub hosts, and whose toolcache is therefore thrown
* away once the job has finished.
*
* Unlike `looksLikeHostedRunner`, this is based on what the service reports for the job rather than
* on how the runner's filesystem happens to be laid out, so it does not match self-hosted runners
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
* `/opt/hostedtoolcache`.
*/
export function isGitHubHostedRunner(env: ReadOnlyEnv = getEnv()) {
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
}
/** Determines whether the workflow trigger is `dynamic`. */
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
return getWorkflowEventName(env) === "dynamic";

View File

@@ -212,7 +212,11 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
await runAutobuild(config, BuiltInLanguage.go, logger);
}
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
async function run({
startedAt,
logger,
actions,
}: ActionState<["Base", "Logger", "Actions"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -307,8 +311,13 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
logger,
);
const checkoutPath = actions.getRequiredInput("checkout_path");
// Setup diff informed analysis if needed (based on whether init created the file)
const diffRangePackDir = await setupDiffInformedQueryRun(logger);
const diffRangePackDir = await setupDiffInformedQueryRun(
logger,
checkoutPath,
);
await warnIfGoInstalledAfterInit(config, logger);
await runAutobuildIfLegacyGoWorkflow(config, logger);
@@ -354,7 +363,6 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
actionsUtil.getOptionalInput("upload"),
);
if (runStats) {
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
const category = actionsUtil.getOptionalInput("category");
uploadResults = await postProcessAndUploadSarif(
@@ -388,18 +396,23 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
// Possibly upload the overlay-base database to actions cache.
// Note: Take care with the ordering of this call since databases may be cleaned up
// at the `overlay` level.
await cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger);
await cleanupAndUploadOverlayBaseDatabaseToCache(
codeql,
config,
logger,
checkoutPath,
);
// Possibly upload the database bundles for remote queries.
// Note: Take care with the ordering of this call since databases may be cleaned up
// at the `overlay` or `clear` level.
databaseUploadResults = await cleanupAndUploadDatabases(
{ logger, features },
repositoryNwo,
codeql,
config,
apiDetails,
features,
logger,
checkoutPath,
);
// Possibly upload the TRAP caches for later re-use

View File

@@ -5,7 +5,7 @@ import { performance } from "perf_hooks";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import { getTemporaryDirectory, getRequiredInput } from "./actions-util";
import { getTemporaryDirectory } from "./actions-util";
import * as analyses from "./analyses";
import { setupCppAutobuild } from "./autobuild";
import { type CodeQL } from "./codeql";
@@ -233,6 +233,7 @@ async function finalizeDatabaseCreation(
*/
export async function setupDiffInformedQueryRun(
logger: Logger,
checkoutPath: string,
): Promise<string | undefined> {
return await withGroupAsync(
"Generating diff range extension pack",
@@ -245,7 +246,6 @@ export async function setupDiffInformedQueryRun(
return undefined;
}
const checkoutPath = getRequiredInput("checkout_path");
const packDir = writeDiffRangeDataExtensionPack(
logger,
diffRanges,

View File

@@ -1,8 +1,5 @@
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 {
@@ -128,7 +125,7 @@ export function makeProxyRequestOptions(
}
/** The type of GitHub API client we use. */
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
/** Options for `createApiClientWithDetails`. */
interface CreateApiClientOptions {

View File

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

View File

@@ -5,7 +5,7 @@ import * as core from "@actions/core";
import { getOptionalInput, isDefaultSetup } from "./actions-util";
import { EnvVar } from "./environment";
import { Logger } from "./logging";
import { isHostedRunner, tryGetFolderBytes } from "./util";
import { looksLikeHostedRunner, tryGetFolderBytes } from "./util";
/**
* Returns the total size of all the specified paths.
@@ -109,7 +109,7 @@ export function getDependencyCachingEnabled(): CachingKind {
if (dependencyCaching !== undefined) return getCachingKind(dependencyCaching);
// On self-hosted runners which may have dependencies installed centrally, disable caching by default
if (!isHostedRunner()) return CachingKind.None;
if (!looksLikeHostedRunner()) return CachingKind.None;
// Disable in advanced workflows by default.
if (!isDefaultSetup()) return CachingKind.None;

View File

@@ -128,7 +128,6 @@ test("CliError constructor with empty stderr", (t) => {
for (const [platform, arch] of [
["weird_plat", "x64"],
["linux", "arm64"],
["win32", "arm64"],
]) {
test.serial(
@@ -157,20 +156,34 @@ for (const [platform, arch] of [
);
}
test("wrapCliConfigurationError - supported platform", (t) => {
const commandError = new CommandInvocationError(
"codeql",
["version"],
1,
"Some error",
for (const [platform, arch] of [
["linux", "x64"],
["linux", "arm64"],
["win32", "x64"],
["darwin", "x64"],
["darwin", "arm64"],
]) {
test.serial(
`wrapCliConfigurationError - ${platform}/${arch} supported`,
(t) => {
sinon.stub(process, "platform").value(platform);
sinon.stub(process, "arch").value(arch);
const commandError = new CommandInvocationError(
"codeql",
["version"],
1,
"Some error",
);
const cliError = new CliError(commandError);
const wrappedError = wrapCliConfigurationError(cliError);
// Should return the original error since the platform is supported, rather
// than replacing it with the unsupported-platform ConfigurationError.
t.is(wrappedError, cliError);
},
);
const cliError = new CliError(commandError);
const wrappedError = wrapCliConfigurationError(cliError);
// Should return the original error since platform is supported
t.is(wrappedError, cliError);
});
}
test("wrapCliConfigurationError - autobuild error", (t) => {
const commandError = new CommandInvocationError(

View File

@@ -8,6 +8,7 @@ import { ConfigurationError } from "./util";
const SUPPORTED_PLATFORMS = [
["linux", "x64"],
["linux", "arm64"],
["win32", "x64"],
["darwin", "x64"],
["darwin", "arm64"],

View File

@@ -3,12 +3,11 @@ import path from "path";
import test from "ava";
import { EnvVar } from "../environment";
import { getRunnerLogger } from "../logging";
import { getTestEnv, setupTests } from "../testing-utils";
import { setupTests } from "../testing-utils";
import * as util from "../util";
import * as outputCache from "./output-cache";
import { getCachedCodeQlVersion } from "./output-cache";
setupTests(test);
@@ -18,18 +17,18 @@ test.serial(
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "codeql-action-command-cache.json");
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(
cacheFile,
cacheFilePath,
JSON.stringify({
cmd: "/path/to/codeql",
entries: { version: { version: "2.20.0" } },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.deepEqual(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
{
version: "2.20.0",
},
@@ -42,18 +41,17 @@ test.serial(
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(
cacheFile,
cacheFilePath,
JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
entries: { version: { version: "2.20.0" } },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});
@@ -64,11 +62,10 @@ test.serial(
"getCachedCodeQlVersion ignores a malformed persisted value",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(cacheFile, "not valid json", "utf8");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(cacheFilePath, "not valid json", "utf8");
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});
@@ -79,9 +76,7 @@ test.serial(
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const cacheFilePath = path.join(tmpDir, "cache.json");
const testValues = [
{ cmd: "/path/to/codeql" },
{ entries: { version: { version: "2.20.0" } } },
@@ -104,9 +99,9 @@ test.serial(
].map((v) => JSON.stringify(v));
for (const value of testValues) {
fs.writeFileSync(cacheFile, value, "utf8");
fs.writeFileSync(cacheFilePath, value, "utf8");
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
value,
);
@@ -117,10 +112,10 @@ test.serial(
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const cacheFilePath = path.join(tmpDir, "cache.json");
t.notThrows(() => {
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});

View File

@@ -3,9 +3,10 @@ import path from "path";
import { getTemporaryDirectory } from "../actions-util";
import { Env } from "../environment";
import * as json from "../json";
import { Logger } from "../logging";
import type { VersionInfo } from "./types";
import { VersionInfo, versionInfoBaseSchema } from "./types";
/**
* The keys of the command cache. Each key corresponds to a command whose output we cache.
@@ -13,12 +14,19 @@ import type { VersionInfo } from "./types";
export type CommandCacheKey = string;
/**
* The type of the command cache that is persisted to disk.
* The JSON schema of the command cache that is persisted to disk.
*/
export interface OutputCache {
cmd: string;
entries: Record<CommandCacheKey, unknown>;
}
const outputCacheSchema = {
cmd: json.string,
entries: json.object({}),
} as const satisfies json.Schema;
/**
* The type that describes the command cache that is persisted to disk.
*/
export type OutputCache = json.FromSchema<typeof outputCacheSchema> & {
entries: { version: VersionInfo };
};
/**
* The name of the temporary file that backs the on-disk cache of
@@ -43,18 +51,18 @@ export function resetCachedCodeQlVersion(): void {
* Returns the path to the temporary file that backs the
* on-disk cache of CLI responses between workflow steps.
*/
function getCommandCacheFilePath(env: Env): string {
export function getCommandCacheFilePath(env: Env): string {
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
}
/**
* Caches the CodeQL CLI version both in-memory and on disk.
* @param env The environment variables to use.
* @param cacheFilePath The path to the cache file.
* @param cmd The path to the CodeQL CLI.
* @param version The version information to cache.
*/
export function cacheCodeQlVersion(
env: Env,
cacheFilePath: string,
cmd: string,
version: VersionInfo,
): void {
@@ -70,22 +78,18 @@ export function cacheCodeQlVersion(
// processes, can reuse it rather than invoking `codeql version` again. We
// record the CLI path so that a different step using a different CodeQL bundle
// doesn't pick up a stale version.
fs.writeFileSync(
getCommandCacheFilePath(env),
JSON.stringify(outputCache),
"utf8",
);
fs.writeFileSync(cacheFilePath, JSON.stringify(outputCache), "utf8");
}
/**
* Returns the cached CodeQL CLI version, if any.
* @param logger The logger to use for logging messages.
* @param env The environment variables to use.
* @param cacheFilePath The path to the cache file.
* @param cmd The path to the CodeQL CLI.
*/
export function getCachedCodeQlVersion(
logger: Logger,
env: Env,
cacheFilePath: string,
cmd?: string,
): undefined | VersionInfo {
if (cachedCodeQlVersion !== undefined) {
@@ -96,11 +100,9 @@ export function getCachedCodeQlVersion(
// invokes `codeql version` instead.
let serialized: string;
try {
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
serialized = fs.readFileSync(cacheFilePath, "utf8");
} catch (e) {
logger.debug(
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`,
);
logger.debug(`Cannot read CLI-cache file ${cacheFilePath}: ${e}`);
return undefined;
}
let persisted: unknown;
@@ -127,17 +129,7 @@ export function getCachedCodeQlVersion(
* @param x The value to test
*/
function isVersionInfo(x: unknown): x is VersionInfo {
const candidate = x as Partial<VersionInfo> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.version === "string" &&
(candidate.features === undefined ||
(typeof candidate.features === "object" &&
candidate.features !== null)) &&
(candidate.overlayVersion === undefined ||
typeof candidate.overlayVersion === "number")
);
return json.isObject(x) && json.validateSchema(versionInfoBaseSchema, x);
}
/**
@@ -145,12 +137,10 @@ function isVersionInfo(x: unknown): x is VersionInfo {
* @param x The value to test
*/
function isOutputCache(x: unknown): x is OutputCache {
const candidate = x as Partial<OutputCache> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.cmd === "string" &&
candidate.entries !== undefined &&
isVersionInfo(candidate.entries.version)
json.isObject(x) &&
json.validateSchema(outputCacheSchema, x) &&
json.isObject<{ version: unknown }>(x.entries) &&
isVersionInfo(x.entries.version)
);
}

View File

@@ -1,6 +1,11 @@
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
import * as json from "../json";
/**
* The JSON schema of the expected output of the `codeql version` command.
*/
export const versionInfoBaseSchema = {
version: json.string,
features: json.optional(json.object({})),
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
@@ -9,5 +14,17 @@ export interface VersionInfo {
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
overlayVersion: json.optional(json.number),
} as const satisfies json.Schema;
/**
* The base type that describes the expected output of the `codeql version` command.
*/
export type VersionInfoBase = json.FromSchema<typeof versionInfoBaseSchema>;
/**
* The full type that describes the expected output of the `codeql version` command.
*/
export type VersionInfo = Omit<VersionInfoBase, "features"> & {
features?: { [name: string]: boolean };
};

56
src/codeql-bundle.test.ts Normal file
View File

@@ -0,0 +1,56 @@
import test from "ava";
import { getCodeQLBundleFromUrl } from "./codeql-bundle";
import { BuiltInLanguage } from "./languages";
for (const [assetName, language] of [
["codeql-bundle-java-linux64.tar.zst", BuiltInLanguage.java],
["codeql-bundle-swift-osx64.tar.zst", BuiltInLanguage.swift],
// Recognize unpublished language/platform combinations to keep them out of the toolcache.
["codeql-bundle-csharp-win64.tar.gz", BuiltInLanguage.csharp],
["codeql-bundle-java-kotlin-linux64.tar.zst", BuiltInLanguage.java],
["codeql-bundle-%70ython-linux64.tar.zst", BuiltInLanguage.python],
] as const) {
test(`getCodeQLBundleFromUrl identifies ${assetName} without adding a fallback`, (t) => {
const url = `https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${assetName}`;
t.deepEqual(getCodeQLBundleFromUrl(url), {
kind: "per-language",
url,
language,
});
});
}
test("getCodeQLBundleFromUrl preserves encoding, query parameters and fragments", (t) => {
const url =
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/codeql-bundle-%70ython-linux64.tar.zst?download=1#asset";
t.deepEqual(getCodeQLBundleFromUrl(url), {
kind: "per-language",
url,
language: BuiltInLanguage.python,
});
});
test("getCodeQLBundleFromUrl treats unrecognized assets as combined bundles", (t) => {
for (const name of [
"codeql-bundle-linux64.tar.zst",
"codeql-bundle-osx64.tar.gz",
"codeql-bundle-win64.tar.zst",
// The all-platform bundle.
"codeql-bundle.tar.gz",
// A platform we do not publish per-language bundles for, whose name also contains a hyphen.
"codeql-bundle-linux-arm64.tar.zst",
// Not a language we know about.
"codeql-bundle-cobol-linux64.tar.zst",
// A name we cannot decode must not be mistaken for a language either.
"codeql-bundle-%zz-linux64.tar.zst",
]) {
const url = `https://github.com/github/codeql-action/releases/download/codeql-bundle-v1.2.3/${name}`;
t.deepEqual(getCodeQLBundleFromUrl(url), { kind: "combined", url });
}
});
test("getCodeQLBundleFromUrl preserves URLs it cannot parse", (t) => {
const url = "not a url";
t.deepEqual(getCodeQLBundleFromUrl(url), { kind: "combined", url });
});

33
src/codeql-bundle.ts Normal file
View File

@@ -0,0 +1,33 @@
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
/** Describes the contents and location of a downloadable CodeQL bundle. */
export type CodeQLBundle =
| { kind: "combined"; url: string }
| {
kind: "per-language";
url: string;
language: BuiltInLanguage;
/** Only set when the Action selected the bundle, allowing a same-version fallback. */
combinedBundleURL?: string;
};
const PER_LANGUAGE_BUNDLE_NAME =
/^codeql-bundle-(.+)-(?:linux64|osx64|win64)\.tar\.(?:gz|zst)$/;
/** Classifies an explicit tools URL without changing it or adding a fallback. */
export function getCodeQLBundleFromUrl(url: string): CodeQLBundle {
let assetName: string;
try {
const pathname = new URL(url).pathname;
// URL-encoded names must not bypass the toolcache safeguard.
assetName = decodeURIComponent(pathname.split("/").pop() ?? "");
} catch {
return { kind: "combined", url };
}
const match = assetName.match(PER_LANGUAGE_BUNDLE_NAME);
const language = match ? parseBuiltInLanguage(match[1]) : undefined;
return language === undefined
? { kind: "combined", url }
: { kind: "per-language", url, language };
}

View File

@@ -90,7 +90,7 @@ async function installIntoToolcache({
tmpDir: string;
}) {
const url = mockBundleDownloadApi({ apiDetails, isPinned, tagName });
await codeql.setupCodeQL(
return await codeql.setupCodeQL(
cliVersion !== undefined ? undefined : url,
apiDetails,
tmpDir,
@@ -259,6 +259,65 @@ test.serial(
},
);
for (const { cliVersion, tagName, expectedToolcacheVersion } of [
{
cliVersion: "2.21.0",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "2.21.0",
},
{
cliVersion: "2.21.0-rc.1",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "0.0.0-20240101",
},
{
cliVersion: "2.21.0+20240101",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "0.0.0-20240101",
},
{
cliVersion: "2.21.0",
tagName: "custom-release",
expectedToolcacheVersion: undefined,
},
]) {
test.serial(
`preserves CLI version ${cliVersion} when installing ${tagName}`,
async (t) => {
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = await installIntoToolcache({
cliVersion,
isPinned: false,
tagName,
tmpDir,
});
t.is(result.toolsVersion, cliVersion);
t.is(result.toolsSource, ToolsSource.Download);
t.true(
Number.isInteger(result.toolsDownloadStatusReport?.totalDurationMs),
);
t.deepEqual(
toolcache.findAllVersions("CodeQL"),
expectedToolcacheVersion === undefined
? []
: [expectedToolcacheVersion],
);
if (expectedToolcacheVersion !== undefined) {
const cachedFolder = toolcache.find(
"CodeQL",
expectedToolcacheVersion,
);
t.truthy(cachedFolder);
t.true(fs.existsSync(`${cachedFolder}.complete`));
}
});
},
);
}
const EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES = [
{
tagName: "codeql-bundle-2.17.6",

View File

@@ -510,7 +510,12 @@ async function getCodeQLForCmd(
return cmd;
},
async getVersion() {
let result = outputCache.getCachedCodeQlVersion(logger, getEnv(), cmd);
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
let result = outputCache.getCachedCodeQlVersion(
logger,
cacheFilePath,
cmd,
);
if (result === undefined) {
result = await runCliJson<VersionInfo>(
cmd,
@@ -519,7 +524,7 @@ async function getCodeQLForCmd(
noStreamStdout: true,
},
);
outputCache.cacheCodeQlVersion(getEnv(), cmd, result);
outputCache.cacheCodeQlVersion(cacheFilePath, cmd, result);
}
return result;
},
@@ -932,6 +937,7 @@ async function getCodeQLForCmd(
"--format=json",
`--language=${language}`,
"--extractor-include-aliases",
"-J-XX:-UsePerfData",
...getExtraOptionsFromEnv(["resolve", "extractor"]),
],
{

View File

@@ -90,9 +90,8 @@ import {
Result,
Success,
Failure,
isHostedRunner,
looksLikeHostedRunner,
} from "./util";
export { type Config } from "./config/action-config";
/**
@@ -938,7 +937,7 @@ export async function isTrapCachingEnabled(
if (trapCaching !== undefined) return trapCaching === "true";
// On self-hosted runners which may have slow network access, disable TRAP caching by default.
if (!isHostedRunner()) return false;
if (!looksLikeHostedRunner()) return false;
// If overlay analysis is enabled, then disable TRAP caching since overlay analysis supersedes it.
// This change is gated behind a feature flag.

View File

@@ -20,8 +20,9 @@ import {
checkExpectedLogMessages,
createFeatures,
createTestConfig,
getRecordingLogger,
LoggedMessage,
getTestEnv,
initAllState,
RecordingLogger,
setupActionsVars,
setupTests,
} from "./testing-utils";
@@ -90,23 +91,24 @@ test.serial(
"Abort database upload if 'upload-database' input set to false",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("false");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Database upload disabled in workflow. Skipping upload.",
]);
});
@@ -117,7 +119,8 @@ test.serial(
"Abort database upload if 'analysis-kinds: code-scanning' is not enabled",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -126,8 +129,9 @@ test.serial(
await mockHttpRequests(201);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
{
@@ -135,10 +139,9 @@ test.serial(
analysisKinds: [AnalysisKind.CodeQuality],
},
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
]);
});
@@ -147,7 +150,8 @@ test.serial(
test.serial("Abort database upload if running against GHES", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -157,16 +161,16 @@ test.serial("Abort database upload if running against GHES", async (t) => {
const config = getTestConfig(tmpDir);
config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" };
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
config,
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Not running against github.com or GHEC-DR. Skipping upload.",
]);
});
@@ -176,23 +180,24 @@ test.serial(
"Abort database upload if not analyzing default branch",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Not analyzing default branch. Skipping upload.",
]);
});
@@ -203,7 +208,8 @@ test.serial(
"Don't crash if uploading a database fails with a non-retryable error",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -212,17 +218,17 @@ test.serial(
const databaseUploadSpy = await mockHttpRequests(422);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Failed to upload database for javascript: some error message",
]);
@@ -236,7 +242,8 @@ test.serial(
"Don't crash if uploading a database fails with a retryable error",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -251,17 +258,17 @@ test.serial(
.stub(global, "setTimeout")
.callsFake((fn: () => void) => originalSetTimeout(fn, 0));
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Failed to upload database for javascript: some error message",
]);
@@ -279,7 +286,8 @@ test.serial(
test.serial("Successfully uploading a database to github.com", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -288,16 +296,16 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
await mockHttpRequests(201);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
testApiDetails,
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Successfully uploaded database for javascript",
]);
});
@@ -305,7 +313,8 @@ test.serial("Successfully uploading a database to github.com", async (t) => {
test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -314,8 +323,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
const databaseUploadSpy = await mockHttpRequests(201);
const loggedMessages: LoggedMessage[] = [];
const logger = new RecordingLogger();
await cleanupAndUploadDatabases(
initAllState({ env, logger }),
testRepoName,
getCodeQL(),
getTestConfig(tmpDir),
@@ -324,10 +334,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => {
url: "https://tenant.ghe.com",
apiURL: undefined,
},
createFeatures([]),
getRecordingLogger(loggedMessages),
"",
);
checkExpectedLogMessages(t, loggedMessages, [
checkExpectedLogMessages(t, logger.messages, [
"Successfully uploaded database for javascript",
]);
t.assert(
@@ -343,7 +352,8 @@ test.serial(
"Records overlay and clear cleanup sizes when uploading an overlay-base database",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -375,14 +385,16 @@ test.serial(
const config = getTestConfig(tmpDir);
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
const loggedMessages: LoggedMessage[] = [];
const results = await cleanupAndUploadDatabases(
initAllState({
env,
features: createFeatures([Feature.UploadOverlayDbToApi]),
}),
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger(loggedMessages),
"",
);
// The database should be cleaned up at the `overlay` level for the upload
@@ -402,7 +414,8 @@ test.serial(
"Does not measure clear cleanup size for a regular (non-overlay-base) upload",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -422,12 +435,15 @@ test.serial(
});
const results = await cleanupAndUploadDatabases(
initAllState({
env,
features: createFeatures([Feature.UploadOverlayDbToApi]),
}),
testRepoName,
codeql,
getTestConfig(tmpDir),
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
"",
);
// A regular upload is cleaned only once, at the `clear` level.
@@ -441,7 +457,8 @@ test.serial(
test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -465,12 +482,15 @@ test.serial("Does not measure clear cleanup size in debug mode", async (t) => {
config.debugMode = true;
const results = await cleanupAndUploadDatabases(
initAllState({
env,
features: createFeatures([Feature.UploadOverlayDbToApi]),
}),
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
"",
);
// In debug mode we clean up at the `overlay` level for the upload but skip
@@ -486,7 +506,8 @@ test.serial(
"Does not record a clear cleanup duration when the clear cleanup fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const env = getTestEnv();
setupActionsVars(tmpDir, tmpDir, {}, env);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
@@ -510,12 +531,15 @@ test.serial(
config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
const results = await cleanupAndUploadDatabases(
initAllState({
env,
features: createFeatures([Feature.UploadOverlayDbToApi]),
}),
testRepoName,
codeql,
config,
testApiDetails,
createFeatures([Feature.UploadOverlayDbToApi]),
getRecordingLogger([]),
"",
);
// When the `clear` cleanup fails, no size is measured, so we should not

View File

@@ -1,5 +1,6 @@
import * as fs from "fs";
import { ActionState } from "./action-common";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import {
@@ -9,7 +10,7 @@ import {
} from "./api-client";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { Logger, withGroupAsync } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
@@ -45,13 +46,15 @@ export interface DatabaseUploadResult {
}
export async function cleanupAndUploadDatabases(
action: ActionState<["Logger", "FeatureFlags"]>,
repositoryNwo: RepositoryNwo,
codeql: CodeQL,
config: Config,
apiDetails: GitHubApiDetails,
features: FeatureEnablement,
logger: Logger,
checkoutPath: string,
): Promise<DatabaseUploadResult[]> {
const logger = action.logger;
if (actionsUtil.getRequiredInput("upload-database") !== "true") {
logger.debug("Database upload disabled in workflow. Skipping upload.");
return [];
@@ -87,7 +90,7 @@ export async function cleanupAndUploadDatabases(
// If config.overlayDatabaseMode is OverlayBase, then we have overlay base databases for all languages.
const shouldUploadOverlayBase =
config.overlayDatabaseMode === OverlayDatabaseMode.OverlayBase &&
(await features.getValue(Feature.UploadOverlayDbToApi, codeql));
(await action.features.getValue(Feature.UploadOverlayDbToApi, codeql));
const cleanupLevel = shouldUploadOverlayBase
? CleanupLevel.Overlay
: CleanupLevel.Clear;
@@ -110,9 +113,7 @@ export async function cleanupAndUploadDatabases(
includeDiagnostics: false,
});
bundledDbSize = fs.statSync(bundledDb).size;
const commitOid = await gitUtils.getCommitOid(
actionsUtil.getRequiredInput("checkout_path"),
);
const commitOid = await gitUtils.getCommitOid(checkoutPath);
// Upload with manual retry logic. We disable Octokit's built-in retries
// because the request body is a ReadStream, which can only be consumed
// once.

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.26.3",
"cliVersion": "2.26.3",
"priorBundleVersion": "codeql-bundle-v2.26.2",
"priorCliVersion": "2.26.2"
"bundleVersion": "codeql-bundle-v2.27.1",
"cliVersion": "2.27.1",
"priorBundleVersion": "codeql-bundle-v2.27.0",
"priorCliVersion": "2.27.0"
}

View File

@@ -63,6 +63,12 @@ export enum EnvVar {
/** Whether the CodeQL Action has already warned the user about low disk space. */
HAS_WARNED_ABOUT_DISK_SPACE = "CODEQL_ACTION_HAS_WARNED_ABOUT_DISK_SPACE",
/**
* Whether a step in this job has already set up CodeQL. Steps that run afterwards may be holding
* a path into the toolcache, so we must not delete anything from it.
*/
HAS_SET_UP_CODEQL = "CODEQL_ACTION_HAS_SET_UP_CODEQL",
/** Whether the `setup-codeql` action has been run. */
SETUP_CODEQL_ACTION_HAS_RUN = "CODEQL_ACTION_SETUP_CODEQL_HAS_RUN",

View File

@@ -74,6 +74,11 @@ export enum Feature {
AllowMergeConfigFiles = "allow_merge_config_files",
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
/**
* Controls whether we delete CodeQL bundles that we are not going to use from the toolcache
* before downloading a different bundle, in order to reclaim disk space.
*/
CleanupToolcacheBundles = "cleanup_toolcache_bundles",
CleanupTrapCaches = "cleanup_trap_caches",
/** Whether to allow the `config-file` input to be specified via a repository property. */
ConfigFileRepositoryProperty = "config_file_repository_property",
@@ -159,6 +164,11 @@ export enum Feature {
OverlayAnalysisStatusCheck = "overlay_analysis_status_check",
/** Controls whether overlay build failures on the default branch are stored in the Actions cache. */
OverlayAnalysisStatusSave = "overlay_analysis_status_save",
/**
* Controls whether we may download a bundle containing only the single language being analysed,
* rather than the combined bundle that contains every language.
*/
PerLanguageBundles = "per_language_bundles",
QaTelemetryEnabled = "qa_telemetry_enabled",
/** Routes (some) API requests through the registry proxy. */
ProxyApiRequests = "proxy_api_requests",
@@ -211,6 +221,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
minimumVersion: undefined,
},
[Feature.CleanupToolcacheBundles]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES",
minimumVersion: undefined,
},
[Feature.CleanupTrapCaches]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
@@ -424,6 +439,11 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SKIP_RESOURCE_CHECKS",
minimumVersion: undefined,
},
[Feature.PerLanguageBundles]: {
defaultValue: false,
envVar: "CODEQL_ACTION_PER_LANGUAGE_BUNDLES",
minimumVersion: undefined,
},
[Feature.QaTelemetryEnabled]: {
defaultValue: false,
envVar: "CODEQL_ACTION_QA_TELEMETRY",

View File

@@ -29,10 +29,13 @@ test.serial(
process.env["GITHUB_SHA"] = currentSha;
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs("HEAD").resolves(currentSha);
callback.withArgs(sinon.match.string, "HEAD").resolves(currentSha);
const actualRef = await gitUtils.getRef();
t.deepEqual(actualRef, expectedRef);
t.is(callback.callCount, 1);
t.true(callback.calledOnceWith(tmpDir, "HEAD"));
});
},
);
@@ -48,11 +51,17 @@ test.serial(
const sha = "a".repeat(40);
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs("refs/remotes/pull/1/merge").resolves(sha);
callback.withArgs("HEAD").resolves(sha);
callback
.withArgs(sinon.match.string, "refs/remotes/pull/1/merge")
.resolves(sha);
callback.withArgs(sinon.match.any, "HEAD").resolves(sha);
const actualRef = await gitUtils.getRef();
t.deepEqual(actualRef, expectedRef);
t.is(callback.callCount, 2);
t.true(callback.calledWith(tmpDir, "HEAD"));
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
});
},
);
@@ -66,11 +75,18 @@ test.serial(
process.env["GITHUB_SHA"] = "a".repeat(40);
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("a".repeat(40));
callback
.withArgs(tmpDir, "refs/remotes/pull/1/merge")
.resolves("a".repeat(40));
callback.withArgs(tmpDir, "HEAD").resolves("b".repeat(40));
callback.throws(new Error("Unexpected getCommitOid call in test."));
const actualRef = await gitUtils.getRef();
t.deepEqual(actualRef, "refs/pull/1/head");
t.is(callback.callCount, 2);
t.true(callback.calledWith(tmpDir, "refs/remotes/pull/1/merge"));
t.true(callback.calledWith(tmpDir, "HEAD"));
});
},
);
@@ -92,11 +108,14 @@ test.serial(
process.env["GITHUB_SHA"] = "a".repeat(40);
const callback = sinon.stub(gitUtils, "getCommitOid");
callback.withArgs("refs/pull/1/merge").resolves("b".repeat(40));
callback.withArgs("HEAD").resolves("b".repeat(40));
callback.withArgs(tmpDir, "refs/pull/1/merge").resolves("b".repeat(40));
callback.withArgs(sinon.match.any, "HEAD").resolves("b".repeat(40));
const actualRef = await gitUtils.getRef();
t.deepEqual(actualRef, "refs/pull/2/merge");
// getCommitOid shouldn't be called, because the ref should be taken from the input
t.is(callback.callCount, 0);
});
},
);

View File

@@ -15,10 +15,12 @@ import { getRunnerLogger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import * as overlayStatus from "./overlay/status";
import { parseRepositoryNwo } from "./repository";
import { JobStatus } from "./status-report";
import {
createFeatures,
createTestConfig,
DEFAULT_ACTIONS_VARS,
getTestEnv,
makeMacro,
makeVersionInfo,
RecordingLogger,
@@ -58,6 +60,8 @@ test.serial("init-post action with debug mode off", async (t) => {
createTestConfig({ debugMode: false }),
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
);
@@ -80,6 +84,8 @@ test.serial("init-post action with debug mode on", async (t) => {
createTestConfig({ debugMode: true }),
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
);
@@ -375,6 +381,8 @@ test.serial(
}),
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.OverlayAnalysisStatusSave]),
"success",
getTestEnv(),
getRunnerLogger(true),
);
@@ -443,6 +451,8 @@ test.serial(
}),
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
);
@@ -457,8 +467,13 @@ test.serial(
test.serial("does not save overlay status when build successful", async (t) => {
return await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
// Mark analyze as having completed successfully.
// Mark analyze as having completed successfully. `tryUploadSarifIfRunFailed` reads this from
// the process environment, while `recordOverlayStatus` reads it from the environment it is
// given.
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] = "true";
const env = getTestEnv({
[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]: "true",
});
sinon.stub(util, "checkDiskUsage").resolves({
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
@@ -480,6 +495,8 @@ test.serial("does not save overlay status when build successful", async (t) => {
}),
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.OverlayAnalysisStatusSave]),
"success",
env,
getRunnerLogger(true),
);
@@ -517,6 +534,8 @@ test.serial(
}),
parseRepositoryNwo("github/codeql-action"),
createFeatures([]),
"success",
getTestEnv(),
getRunnerLogger(true),
);
@@ -528,6 +547,137 @@ test.serial(
},
);
/**
* Runs `uploadFailureInfo` for an overlay-base job that did not complete successfully, with the
* given job status from the Actions runtime environment.
*/
async function runOverlayPostStep({
jobStatus,
codeQlReportedError = false,
}: {
jobStatus: string | undefined;
codeQlReportedError?: boolean;
}) {
return await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY];
const env = getTestEnv(
codeQlReportedError
? { [EnvVar.JOB_STATUS]: JobStatus.FailureStatus }
: {},
);
sinon.stub(util, "checkDiskUsage").resolves({
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
numTotalBytes: 200 * NUM_BYTES_PER_GIB,
});
const saveOverlayStatusStub = sinon
.stub(overlayStatus, "saveOverlayStatus")
.resolves(true);
await initActionPostHelper.uploadFailureInfo(
sinon.spy(),
sinon.spy(),
codeql.createStubCodeQL({}),
createTestConfig({
debugMode: false,
languages: ["javascript"],
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
}),
parseRepositoryNwo("github/codeql-action"),
createFeatures([Feature.OverlayAnalysisStatusSave]),
jobStatus,
env,
getRunnerLogger(true),
);
return { saveOverlayStatusStub };
});
}
test.serial(
"does not save overlay status when the job was cancelled",
async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: "cancelled",
});
t.true(
saveOverlayStatusStub.notCalled,
"a cancellation tells us nothing about whether the analysis would have succeeded",
);
},
);
test.serial(
"does not save overlay status when the job status is not recognised",
async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: "some-new-status",
});
t.true(
saveOverlayStatusStub.notCalled,
"a status we do not recognise tells us nothing about whether the analysis would have succeeded",
);
},
);
test.serial(
"does not save overlay status when the job status is unavailable",
async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: undefined,
});
t.true(
saveOverlayStatusStub.notCalled,
"without a job status we cannot tell whether the analysis would have succeeded",
);
},
);
test.serial(
"saves overlay status when the job failed rather than being cancelled",
async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: "failure",
});
t.true(
saveOverlayStatusStub.calledOnce,
"a failed job indicates that the analysis itself failed",
);
},
);
test.serial("saves overlay status when the job succeeded", async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: "success",
});
t.true(
saveOverlayStatusStub.calledOnce,
"the analysis did not complete successfully even though the job as a whole succeeded",
);
});
test.serial(
"saves overlay status when a CodeQL Action reported an error before the run was cancelled",
async (t) => {
const { saveOverlayStatusStub } = await runOverlayPostStep({
jobStatus: "cancelled",
codeQlReportedError: true,
});
t.true(
saveOverlayStatusStub.calledOnce,
"the analysis genuinely failed, even though the run was later cancelled",
);
},
);
function createTestWorkflow(
steps: workflow.WorkflowJobStep[],
): workflow.Workflow {

View File

@@ -18,7 +18,7 @@ import {
sanitizeArtifactName,
} from "./debug-artifacts";
import * as dependencyCaching from "./dependency-caching";
import { EnvVar } from "./environment";
import { EnvVar, ReadOnlyEnv } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
@@ -316,6 +316,8 @@ export async function tryUploadSarifIfRunFailed(
* @param config The CodeQL Action configuration.
* @param repositoryNwo The name and owner of the repository.
* @param features Information about enabled features.
* @param jobStatus The status of the job, as reported by the Actions runtime environment.
* @param env The environment to read variables from.
* @param logger The logger to use.
* @returns The results of uploading the SARIF file for the failure.
*/
@@ -331,9 +333,11 @@ export async function uploadFailureInfo(
config: Config,
repositoryNwo: RepositoryNwo,
features: FeatureEnablement,
jobStatus: string | undefined,
env: ReadOnlyEnv,
logger: Logger,
): Promise<UploadFailedSarifResult> {
await recordOverlayStatus(codeql, config, features, logger);
await recordOverlayStatus(codeql, config, features, jobStatus, env, logger);
const uploadFailedSarifResult = await tryUploadSarifIfRunFailed(
config,
@@ -412,6 +416,37 @@ export async function uploadFailureInfo(
return uploadFailedSarifResult;
}
/**
* Whether one of the CodeQL Actions reported an error for this job, which means the analysis
* genuinely failed.
*
* Note that the converse does not hold: an Action that is terminated abruptly, or that fails before
* it can gather telemetry, does not get to report anything.
*/
function didCodeQlReportError(env: ReadOnlyEnv): boolean {
const jobStatus = env.getOptional(EnvVar.JOB_STATUS);
return (
jobStatus === JobStatus.FailureStatus ||
jobStatus === JobStatus.ConfigErrorStatus
);
}
/**
* Whether the job status tells us anything about whether the analysis itself would have succeeded.
*
* We check for the statuses we know to be meaningful rather than excluding the ones that are not,
* so that a status we do not recognise is treated as inconclusive.
*/
function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
switch (jobStatus?.trim().toLowerCase()) {
case "failure":
case "success":
return true;
default:
return false;
}
}
/**
* If overlay base database creation was attempted but the analysis did not complete
* successfully, save the failure status to the Actions cache so that subsequent runs
@@ -421,16 +456,30 @@ async function recordOverlayStatus(
codeql: CodeQL,
config: Config,
features: FeatureEnablement,
jobStatus: string | undefined,
env: ReadOnlyEnv,
logger: Logger,
) {
if (
config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase ||
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true" ||
env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" ||
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
) {
return;
}
// Only record a failure when the job outcome tells us something about the analysis. A cancelled
// job, or a status we do not recognise, says nothing about whether the analysis would have
// succeeded, so recording a failure would disable overlay analysis needlessly. We still record
// one if a CodeQL Action reported an error before the job ended.
if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) {
logger.info(
"Not recording an improved incremental analysis failure for this job because the job " +
`status (${jobStatus ?? "unset"}) does not tell us whether the analysis itself failed.`,
);
return;
}
const checkRunIdInput = actionsUtil.getOptionalInput("check-run-id");
const checkRunId =
checkRunIdInput !== undefined ? parseInt(checkRunIdInput, 10) : undefined;

View File

@@ -8,6 +8,7 @@ import * as core from "@actions/core";
import {
restoreInputs,
getOptionalInput,
getTemporaryDirectory,
printDebugLogs,
} from "./actions-util";
@@ -20,7 +21,7 @@ import {
DependencyCachingUsageReport,
getDependencyCacheUsage,
} from "./dependency-caching";
import { EnvVar } from "./environment";
import { EnvVar, getEnv } from "./environment";
import { initFeatures } from "./feature-flags";
import * as gitUtils from "./git-utils";
import * as initActionPostHelper from "./init-action-post-helper";
@@ -55,6 +56,11 @@ async function run(startedAt: Date) {
| undefined;
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
try {
// Read the job status before restoring inputs, since it is provided by the Actions runtime
// environment for this step and would otherwise be overwritten by the value that the `init`
// Action saw, which is always a success.
const jobStatus = getOptionalInput("job-status");
// Restore inputs from `init` Action.
restoreInputs();
@@ -84,6 +90,8 @@ async function run(startedAt: Date) {
config,
repositoryNwo,
features,
jobStatus,
getEnv(),
logger,
);

View File

@@ -38,7 +38,7 @@ import {
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import { loadRepositoryProperties } from "./feature-flags/properties";
import {
@@ -63,13 +63,13 @@ import { ToolsSource } from "./setup-codeql";
import {
ActionName,
InitStatusReport,
InitToolsDownloadFields,
InitWithConfigStatusReport,
createInitWithConfigStatusReport,
createStatusReportBase,
getActionsStatus,
sendStatusReport,
} from "./status-report";
import { createInitToolsDownloadFields } from "./status-report/tools-download";
import { ToolsDownloadStatusReport } from "./tools-download";
import { ToolsFeature } from "./tools-features";
import { getCombinedTracerConfig } from "./tracer-config";
@@ -81,7 +81,6 @@ import {
DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME,
getCodeQLMemoryLimit,
getRequiredEnvParam,
getThreadsFlagValue,
initializeEnvironment,
ConfigurationError,
@@ -168,15 +167,10 @@ async function sendCompletedStatusReport(
initStatusReport.computed_inputs.tools = toolsInput;
}
const initToolsDownloadFields: InitToolsDownloadFields = {};
if (toolsDownloadStatusReport?.downloadDurationMs !== undefined) {
initToolsDownloadFields.tools_download_duration_ms =
toolsDownloadStatusReport.downloadDurationMs;
}
if (toolsFeatureFlagsValid !== undefined) {
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
}
const initToolsDownloadFields = createInitToolsDownloadFields(
toolsDownloadStatusReport,
toolsFeatureFlagsValid,
);
if (config !== undefined) {
// Append fields that are dependent on `config`
@@ -230,8 +224,8 @@ async function run(
apiDetails = {
auth: getRequiredInput("token"),
externalRepoAuth: getOptionalInput("external-repository-token"),
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL),
};
const gitHubVersion = await getGitHubVersion();
@@ -260,7 +254,7 @@ async function run(
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
// source-root is absolute, it is used as given.
sourceRoot = path.resolve(
getRequiredEnvParam("GITHUB_WORKSPACE"),
actionState.env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE),
getOptionalInput("source-root") || "",
);
@@ -388,7 +382,9 @@ async function run(
repository: repositoryNwo,
tempDir: getTemporaryDirectory(),
codeql,
workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"),
workspacePath: actionState.env.getRequired(
ActionsEnvVars.GITHUB_WORKSPACE,
),
sourceRoot,
githubVersion: gitHubVersion,
apiDetails,

View File

@@ -3,11 +3,7 @@ import * as fs from "fs";
import * as actionsCache from "@actions/cache";
import * as semver from "semver";
import {
getRequiredInput,
getWorkflowRunAttempt,
getWorkflowRunID,
} from "../actions-util";
import { getWorkflowRunAttempt, getWorkflowRunID } from "../actions-util";
import { getAutomationID, listActionsCaches } from "../api-client";
import { createCacheKeyHash } from "../caching-utils";
import { type CodeQL } from "../codeql";
@@ -107,12 +103,13 @@ async function checkOverlayBaseDatabase(
* Uploads the overlay-base database to the GitHub Actions cache. If conditions
* for uploading are not met, the function does nothing and returns false.
*
* This function uses the `checkout_path` input to determine the repository path
* This function uses the `checkoutPath` to determine the repository path
* and works only when called from `analyze` or `upload-sarif`.
*
* @param codeql The CodeQL instance
* @param config The configuration object
* @param logger The logger instance
* @param checkoutPath The path at which the repository is checked out at.
* @returns A promise that resolves to true if the upload was performed and
* successfully completed, or false otherwise
*/
@@ -120,6 +117,7 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache(
codeql: CodeQL,
config: Config,
logger: Logger,
checkoutPath: string,
): Promise<boolean> {
const overlayDatabaseMode = config.overlayDatabaseMode;
if (overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase) {
@@ -180,7 +178,6 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache(
}
const codeQlVersion = (await codeql.getVersion()).version;
const checkoutPath = getRequiredInput("checkout_path");
const cacheSaveKey = await getCacheSaveKey(
config,
codeQlVersion,

View File

@@ -0,0 +1,175 @@
import test from "ava";
import { ActionsEnvVars } from "./environment";
import { Feature } from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import {
getPerLanguageBundleLanguage,
MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
PerLanguageBundleOptions,
} from "./per-language-bundles";
import { BundlePlatform } from "./platform";
import {
createFeatures,
getRecordingLogger,
getTestEnv,
initAllState,
LoggedMessage,
} from "./testing-utils";
import { GitHubVariant } from "./util";
/** Options for which we would use a per-language bundle. */
const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = {
rawLanguages: ["java"],
// Any version at least as new as the minimum will do.
cliVersion: MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION,
compressionMethod: "zstd",
platform: BundlePlatform.Linux64,
variant: GitHubVariant.DOTCOM,
};
async function checkEligibility(
overrides: Partial<PerLanguageBundleOptions>,
stateOverrides: Partial<ReturnType<typeof initAllState>> = {},
) {
return getPerLanguageBundleLanguage(
initAllState({
env: getTestEnv({
[ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted",
}),
features: createFeatures([Feature.PerLanguageBundles]),
...stateOverrides,
}),
{ ...ELIGIBLE_OPTIONS, ...overrides },
);
}
for (const platform of Object.values(BundlePlatform)) {
test(`getPerLanguageBundleLanguage selects only supported languages on ${platform}`, async (t) => {
for (const language of Object.values(BuiltInLanguage)) {
const supported =
language === BuiltInLanguage.swift
? platform === BundlePlatform.Osx64
: platform === BundlePlatform.Linux64;
t.is(
await checkEligibility({ rawLanguages: [language], platform }),
supported ? language : undefined,
language,
);
}
});
}
test("getPerLanguageBundleLanguage normalizes aliases before selecting a bundle", async (t) => {
t.is(
await checkEligibility({ rawLanguages: ["java-kotlin"] }),
BuiltInLanguage.java,
);
});
test("getPerLanguageBundleLanguage rejects unknown platforms", async (t) => {
t.is(await checkEligibility({ platform: undefined }), undefined);
});
test("getPerLanguageBundleLanguage requires exactly one language", async (t) => {
t.is(await checkEligibility({ rawLanguages: undefined }), undefined);
t.is(await checkEligibility({ rawLanguages: [] }), undefined);
t.is(await checkEligibility({ rawLanguages: ["java", "python"] }), undefined);
});
test("getPerLanguageBundleLanguage requires a known language", async (t) => {
t.is(await checkEligibility({ rawLanguages: ["cobol"] }), undefined);
});
test("getPerLanguageBundleLanguage requires a zstd bundle", async (t) => {
t.is(await checkEligibility({ compressionMethod: "gzip" }), undefined);
});
test("getPerLanguageBundleLanguage requires GitHub.com", async (t) => {
// Other products resolve the combined bundle against their own instance, so asking for a
// per-language bundle they do not mirror would move the download off that instance.
for (const variant of [GitHubVariant.GHES, GitHubVariant.GHEC_DR]) {
t.is(await checkEligibility({ variant }), undefined);
}
});
test("getPerLanguageBundleLanguage requires a GitHub-hosted runner", async (t) => {
// A self-hosted runner may have a toolcache that persists between jobs, which is worth more than
// a smaller download.
t.is(
await checkEligibility(
{},
{
env: getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
},
),
undefined,
);
// Self-hosted runners are routinely configured to look like hosted ones, for example by mounting
// a persistent volume at `/opt/hostedtoolcache`, so we require the service to tell us explicitly.
t.is(
await checkEligibility(
{},
{
env: getTestEnv({ RUNNER_TOOL_CACHE: "/opt/hostedtoolcache" }),
},
),
undefined,
);
});
test("getPerLanguageBundleLanguage requires a supported release version", async (t) => {
t.is(await checkEligibility({ cliVersion: undefined }), undefined);
t.is(await checkEligibility({ cliVersion: "2.27.0" }), undefined);
t.is(await checkEligibility({ cliVersion: "2.27.1" }), BuiltInLanguage.java);
});
test("getPerLanguageBundleLanguage requires the feature flag", async (t) => {
t.is(await checkEligibility({}, { features: createFeatures([]) }), undefined);
});
test("getPerLanguageBundleLanguage explains a disabled feature before checking eligibility", async (t) => {
const messages: LoggedMessage[] = [];
const language = await getPerLanguageBundleLanguage(
initAllState({
features: createFeatures([]),
logger: getRecordingLogger(messages, { logToConsole: false }),
}),
{ ...ELIGIBLE_OPTIONS, rawLanguages: undefined, cliVersion: undefined },
);
t.is(language, undefined);
t.deepEqual(
messages.map((message) => message.message),
[
"Not using a per-language CodeQL bundle since the per_language_bundles feature is disabled.",
],
);
});
test("getPerLanguageBundleLanguage skips only the release version check for the latest nightly", async (t) => {
const nightly = { isLatestNightly: true, cliVersion: undefined };
t.is(await checkEligibility(nightly), BuiltInLanguage.java);
for (const overrides of [
{ rawLanguages: undefined },
{ rawLanguages: ["java", "python"] },
{ compressionMethod: "gzip" as const },
{ platform: BundlePlatform.Osx64 },
{ variant: GitHubVariant.GHES },
{ variant: GitHubVariant.GHEC_DR },
]) {
t.is(await checkEligibility({ ...nightly, ...overrides }), undefined);
}
t.is(
await checkEligibility(nightly, { features: createFeatures([]) }),
undefined,
);
t.is(
await checkEligibility(nightly, {
env: getTestEnv({ [ActionsEnvVars.RUNNER_ENVIRONMENT]: "self-hosted" }),
}),
undefined,
);
});

133
src/per-language-bundles.ts Normal file
View File

@@ -0,0 +1,133 @@
import * as semver from "semver";
import { ActionState } from "./action-common";
import { isGitHubHostedRunner } from "./actions-util";
import { Feature } from "./feature-flags";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { BundlePlatform } from "./platform";
import * as tar from "./tar";
import { GitHubVariant } from "./util";
/** Minimum CLI version for selecting a per-language release bundle. */
export const MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION = "2.27.1";
/** Languages with per-language bundles published for each platform. */
const PER_LANGUAGE_BUNDLE_LANGUAGES: Readonly<
Record<BundlePlatform, ReadonlySet<BuiltInLanguage>>
> = {
[BundlePlatform.Linux64]: new Set([
BuiltInLanguage.actions,
BuiltInLanguage.cpp,
BuiltInLanguage.csharp,
BuiltInLanguage.go,
BuiltInLanguage.java,
BuiltInLanguage.javascript,
BuiltInLanguage.python,
BuiltInLanguage.ruby,
BuiltInLanguage.rust,
]),
[BundlePlatform.LinuxArm64]: new Set(),
[BundlePlatform.Osx64]: new Set([BuiltInLanguage.swift]),
[BundlePlatform.Win64]: new Set(),
};
/** Inputs that determine whether we may download a per-language bundle. */
export interface PerLanguageBundleOptions {
/** Explicit input only: autodetection needs a CLI instance. */
rawLanguages: string[] | undefined;
/** Requested CLI version, if known. Ignored when requesting the latest nightly. */
cliVersion: string | undefined;
compressionMethod: tar.CompressionMethod;
/** Platform for which the bundle is requested. */
platform: BundlePlatform | undefined;
variant: GitHubVariant;
/** Whether the Action is selecting the latest nightly rather than a release version. */
isLatestNightly?: boolean;
}
/** Returns the eligible bundle language, or undefined for the combined bundle. */
export async function getPerLanguageBundleLanguage(
{
env,
features,
logger,
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
options: PerLanguageBundleOptions,
): Promise<BuiltInLanguage | undefined> {
const {
rawLanguages,
cliVersion,
compressionMethod,
platform,
variant,
isLatestNightly,
} = options;
const explain = (reason: string) => {
logger.debug(`Not using a per-language CodeQL bundle since ${reason}.`);
return undefined;
};
if (!(await features.getValue(Feature.PerLanguageBundles))) {
return explain(`the ${Feature.PerLanguageBundles} feature is disabled`);
}
if (rawLanguages?.length !== 1) {
return explain(
`exactly one language must be requested via the 'languages' input, but ${
rawLanguages?.length ?? 0
} were`,
);
}
const language = parseBuiltInLanguage(rawLanguages[0]);
if (language === undefined) {
return explain(`'${rawLanguages[0]}' is not a known CodeQL language`);
}
if (compressionMethod !== "zstd") {
// Per-language bundles are only published as zstd archives.
return explain(`the bundle would be downloaded as '${compressionMethod}'`);
}
if (variant !== GitHubVariant.DOTCOM) {
// Tenant mirrors may lack these assets, and an unreachable github.com fails with a
// connection error rather than a recoverable 404.
return explain(`we are running against ${variant}`);
}
if (!isGitHubHostedRunner(env)) {
// Per-language installs stay out of the toolcache; self-hosted runners should retain
// the reusable combined bundle instead.
return explain("the job is not running on a GitHub-hosted runner");
}
// Nightly releases are identified by dates rather than versions. If
// `isLatestNightly` is `true`, the latest nightly is requested with
// `tools: nightly` and we don't yet have the corresponding tag at this point.
// Therefore, we skip the version check and don't have an equivalent.
// We can safely assume that the latest nightly will have per-language bundles.
if (!isLatestNightly) {
if (cliVersion === undefined) {
return explain("the requested CLI version is unknown");
}
if (!semver.gte(cliVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION)) {
return explain(
`the requested CodeQL version ${cliVersion} is older than ${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}, which is the ` +
"first version for which per-language bundles are published",
);
}
}
if (
platform === undefined ||
!PER_LANGUAGE_BUNDLE_LANGUAGES[platform].has(language)
) {
return explain(
`no per-language bundle is published for ${language} on ${platform ?? "an unknown platform"}`,
);
}
return language;
}

18
src/platform.test.ts Normal file
View File

@@ -0,0 +1,18 @@
import test from "ava";
import { BundlePlatform, getBundlePlatform } from "./platform";
for (const [platform, arch, expected] of [
["linux", "x64", BundlePlatform.Linux64],
["linux", "arm64", BundlePlatform.LinuxArm64],
["linux", "ia32", BundlePlatform.Linux64],
["darwin", "x64", BundlePlatform.Osx64],
["darwin", "arm64", BundlePlatform.Osx64],
["win32", "x64", BundlePlatform.Win64],
["win32", "arm64", BundlePlatform.Win64],
["freebsd", "x64", undefined],
] as const) {
test(`getBundlePlatform maps ${platform}/${arch} to ${expected ?? "an all-platform bundle"}`, (t) => {
t.is(getBundlePlatform(platform, arch), expected);
});
}

26
src/platform.ts Normal file
View File

@@ -0,0 +1,26 @@
/** Platform identifiers used in CodeQL bundle asset names. */
export enum BundlePlatform {
Linux64 = "linux64",
LinuxArm64 = "linux-arm64",
Osx64 = "osx64",
Win64 = "win64",
}
/** Returns the bundle platform, or undefined when an all-platform bundle is required. */
export function getBundlePlatform(
platform: NodeJS.Platform = process.platform,
arch: NodeJS.Architecture = process.arch,
): BundlePlatform | undefined {
switch (platform) {
case "win32":
return BundlePlatform.Win64;
case "linux":
return arch === "arm64"
? BundlePlatform.LinuxArm64
: BundlePlatform.Linux64;
case "darwin":
return BundlePlatform.Osx64;
default:
return undefined;
}
}

View File

@@ -12,7 +12,7 @@ import { getGitHubVersion } from "./api-client";
import { CodeQL } from "./codeql";
import { ComputedInput, getToolsInput } from "./config/inputs";
import { getRawLanguagesNoAutodetect } from "./config-utils";
import { EnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import { initFeatures } from "./feature-flags";
import { loadRepositoryProperties } from "./feature-flags/properties";
import { initCodeQL } from "./init";
@@ -22,17 +22,16 @@ import { ToolsSource } from "./setup-codeql";
import {
ActionName,
InitStatusReport,
InitToolsDownloadFields,
createStatusReportBase,
getActionsStatus,
sendStatusReport,
} from "./status-report";
import { createInitToolsDownloadFields } from "./status-report/tools-download";
import { ToolsDownloadStatusReport } from "./tools-download";
import {
checkDiskUsage,
checkForTimeout,
checkGitHubVersionInRange,
getRequiredEnvParam,
initializeEnvironment,
ConfigurationError,
wrapError,
@@ -79,15 +78,10 @@ async function sendCompletedStatusReport(
initStatusReport.computed_inputs.tools = toolsInput;
}
const initToolsDownloadFields: InitToolsDownloadFields = {};
if (toolsDownloadStatusReport?.downloadDurationMs !== undefined) {
initToolsDownloadFields.tools_download_duration_ms =
toolsDownloadStatusReport.downloadDurationMs;
}
if (toolsFeatureFlagsValid !== undefined) {
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
}
const initToolsDownloadFields = createInitToolsDownloadFields(
toolsDownloadStatusReport,
toolsFeatureFlagsValid,
);
await sendStatusReport({ ...initStatusReport, ...initToolsDownloadFields });
}
@@ -113,8 +107,8 @@ async function run(
const apiDetails = {
auth: getRequiredInput("token"),
externalRepoAuth: getOptionalInput("external-repository-token"),
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL),
apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL),
};
const gitHubVersion = await getGitHubVersion();

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,30 @@
import * as fs from "fs";
import { OutgoingHttpHeaders } from "http";
import * as path from "path";
import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as toolcache from "@actions/tool-cache";
import { default as deepEqual } from "fast-deep-equal";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
import { ActionState } from "./action-common";
import {
isAnalyzingPullRequest,
isDynamicWorkflow,
isGitHubHostedRunner,
isRunningLocalAction,
} from "./actions-util";
import * as api from "./api-client";
import { CodeQLBundle, getCodeQLBundleFromUrl } from "./codeql-bundle";
import * as defaults from "./defaults.json";
import {
addNoLanguageDiagnostic,
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar, getEnv } from "./environment";
import {
CODEQL_VERSION_ZSTD_BUNDLE,
CodeQLDefaultVersionInfo,
@@ -26,12 +32,17 @@ import {
Feature,
FeatureEnablement,
} from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import { Logger } from "./logging";
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
import { getPerLanguageBundleLanguage } from "./per-language-bundles";
import { getBundlePlatform } from "./platform";
import * as tar from "./tar";
import {
deleteToolcacheBundles,
downloadAndExtract,
getToolcacheDirectory,
isToolcacheOnWorkspaceFilesystem,
ToolsDownloadStatusReport,
writeToolcacheMarkerFile,
} from "./tools-download";
@@ -66,21 +77,25 @@ function getCodeQLBundleExtension(
}
}
/**
* Returns the name of the CodeQL bundle asset to download.
*
* @param compressionMethod The compression method of the bundle.
* @param language Optional language for a per-language bundle. If omitted, returns a combined bundle name.
*/
export function getCodeQLBundleName(
compressionMethod: tar.CompressionMethod,
language?: BuiltInLanguage,
): string {
const extension = getCodeQLBundleExtension(compressionMethod);
const platform = getBundlePlatform();
let platform: string;
if (process.platform === "win32") {
platform = "win64";
} else if (process.platform === "linux") {
platform = "linux64";
} else if (process.platform === "darwin") {
platform = "osx64";
} else {
if (platform === undefined) {
return `codeql-bundle${extension}`;
}
if (language !== undefined) {
return `codeql-bundle-${language}-${platform}${extension}`;
}
return `codeql-bundle-${platform}${extension}`;
}
@@ -101,7 +116,7 @@ export function getCodeQLActionRepository(logger: Logger): string {
async function getCodeQLBundleDownloadURL(
tagName: string,
apiDetails: api.GitHubApiDetails,
compressionMethod: tar.CompressionMethod,
codeQLBundleName: string,
logger: Logger,
): Promise<string> {
const codeQLActionRepository = getCodeQLActionRepository(logger);
@@ -120,7 +135,6 @@ async function getCodeQLBundleDownloadURL(
return !self.slice(0, index).some((other) => deepEqual(source, other));
},
);
const codeQLBundleName = getCodeQLBundleName(compressionMethod);
for (const downloadSource of uniqueDownloadSources) {
const [apiURL, repository] = downloadSource;
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
@@ -158,7 +172,7 @@ function tryGetBundleVersionFromTagName(
tagName: string,
logger: Logger,
): string | undefined {
const match = tagName.match(/^codeql-bundle-(.*)$/);
const match = tagName.match(/^codeql-bundle-(.+)$/);
if (match === null || match.length < 2) {
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
return undefined;
@@ -209,6 +223,22 @@ export function convertToSemVer(version: string, logger: Logger): string {
return s;
}
/** A resolved download, including its bundle identity and version. */
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */
compressionMethod: tar.CompressionMethod;
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** Requested CLI version, if known. */
cliVersion?: string;
/** Resolved version for telemetry, independent of whether the bundle can be cached. */
toolsVersion: string;
}
export type CodeQLToolsSource =
| {
codeqlTarPath: string;
@@ -223,17 +253,7 @@ export type CodeQLToolsSource =
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string;
}
| {
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** CLI version of the tools, if known. */
cliVersion?: string;
compressionMethod: tar.CompressionMethod;
codeqlURL: string;
sourceType: "download";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string;
};
| CodeQLDownloadSource;
/**
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
@@ -442,7 +462,7 @@ export async function getCodeQLSource(
};
}
/** CLI version number, for example 2.12.6. */
/** Requested CLI version number, for example 2.12.6. */
let cliVersion: string | undefined;
/** Tag name of the CodeQL bundle, for example `codeql-bundle-20230120`. */
let tagName: string | undefined;
@@ -452,6 +472,7 @@ export async function getCodeQLSource(
* This does not always include a tag name.
*/
let url: string | undefined;
let bundle: CodeQLBundle | undefined;
// We allow forcing the nightly CLI via the FF for `dynamic` events (or in test mode) where the
// `tools` input cannot be adjusted to explicitly request it.
@@ -460,7 +481,8 @@ export async function getCodeQLSource(
const forceNightly = forceNightlyValueFF && canForceNightlyWithFF;
// For advanced workflows, a value from `CODEQL_NIGHTLY_TOOLS_INPUTS` can be specified explicitly
// for the `tools` input in the workflow file.
// for the `tools` input. This is the computed input, so it may come from the repository property
// rather than the workflow file.
const nightlyRequestedByToolsInput =
toolsInput !== undefined &&
CODEQL_NIGHTLY_TOOLS_INPUTS.includes(toolsInput);
@@ -494,7 +516,12 @@ export async function getCodeQLSource(
`Using the latest CodeQL CLI nightly, as requested by 'tools: ${toolsInput}'.`,
);
}
toolsInput = await getNightlyToolsUrl(logger);
bundle = await getLatestNightlyBundle(
{ env: getEnv(), features, logger },
rawLanguages,
variant,
);
toolsInput = bundle.url;
}
/**
@@ -577,7 +604,7 @@ export async function getCodeQLSource(
if (tagName) {
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
// If the bundle version is a semantic version, it is a CLI version number.
if (bundleVersion && semver.valid(bundleVersion)) {
if (bundleVersion !== undefined && semver.valid(bundleVersion)) {
cliVersion = convertToSemVer(bundleVersion, logger);
}
}
@@ -594,13 +621,15 @@ export async function getCodeQLSource(
}
const bundleVersion =
tagName && tryGetBundleVersionFromTagName(tagName, logger);
const humanReadableVersion =
tagName !== undefined
? tryGetBundleVersionFromTagName(tagName, logger)
: undefined;
const resolvedVersion =
cliVersion ??
(bundleVersion && convertToSemVer(bundleVersion, logger)) ??
tagName ??
url ??
"unknown";
(bundleVersion !== undefined
? convertToSemVer(bundleVersion, logger)
: undefined);
const humanReadableVersion = resolvedVersion ?? tagName ?? url ?? "unknown";
logger.debug(
"Attempting to obtain CodeQL tools. " +
@@ -715,18 +744,55 @@ export async function getCodeQLSource(
let compressionMethod: tar.CompressionMethod;
if (!url) {
const bundleTagName = tagName;
if (bundleTagName === undefined) {
throw new Error(
"Could not determine a release tag for the requested CodeQL bundle.",
);
}
compressionMethod =
cliVersion !== undefined &&
(await useZstdBundle(cliVersion, tarSupportsZstd))
? "zstd"
: "gzip";
url = await getCodeQLBundleDownloadURL(
tagName!,
apiDetails,
compressionMethod,
logger,
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
{ env: getEnv(), features, logger },
{
rawLanguages,
cliVersion,
compressionMethod,
platform: getBundlePlatform(),
variant,
},
);
// Resolves the combined or per-language bundle URL for the requested release.
const resolveBundleURL = (language?: BuiltInLanguage) =>
getCodeQLBundleDownloadURL(
bundleTagName,
apiDetails,
getCodeQLBundleName(compressionMethod, language),
logger,
);
const combinedBundleURL = await resolveBundleURL();
if (perLanguageBundleLanguage !== undefined) {
logger.info(
`Selected the per-language CodeQL bundle for '${perLanguageBundleLanguage}'.`,
);
url = await resolveBundleURL(perLanguageBundleLanguage);
bundle = {
kind: "per-language",
url,
language: perLanguageBundleLanguage,
combinedBundleURL,
};
} else {
url = combinedBundleURL;
bundle = { kind: "combined", url };
}
} else {
const method = tar.inferCompressionMethod(url);
if (method === undefined) {
@@ -736,6 +802,13 @@ export async function getCodeQLSource(
);
}
compressionMethod = method;
bundle ??= getCodeQLBundleFromUrl(url);
if (bundle.kind === "per-language") {
logger.info(
`${url} appears to be a CodeQL bundle that contains only ${bundle.language}.`,
);
}
}
if (cliVersion) {
@@ -744,12 +817,12 @@ export async function getCodeQLSource(
logger.info(`Using CodeQL CLI sourced from ${url} .`);
}
return {
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
bundle,
bundleVersion,
cliVersion,
codeqlURL: url,
compressionMethod,
sourceType: "download",
toolsVersion: cliVersion ?? humanReadableVersion,
toolsVersion: resolvedVersion ?? "unknown",
};
}
@@ -763,7 +836,7 @@ async function tryGetFallbackToolcacheVersion(
logger: Logger,
): Promise<string | undefined> {
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
if (!bundleVersion) {
if (bundleVersion === undefined) {
return undefined;
}
const fallbackVersion = convertToSemVer(bundleVersion, logger);
@@ -777,10 +850,7 @@ async function tryGetFallbackToolcacheVersion(
// Exported using `export const` for testing purposes. Specifically, we want to
// be able to stub this function and have other functions in this file use that stub.
export const downloadCodeQL = async function (
codeqlURL: string,
compressionMethod: tar.CompressionMethod,
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
@@ -788,8 +858,9 @@ export const downloadCodeQL = async function (
): Promise<{
codeqlFolder: string;
statusReport: ToolsDownloadStatusReport;
toolsVersion: string;
}> {
const { bundle, compressionMethod } = source;
const codeqlURL = bundle.url;
const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers: OutgoingHttpHeaders = {
@@ -808,15 +879,11 @@ export const downloadCodeQL = async function (
);
}
const toolcacheInfo = getToolcacheDestinationInfo(
maybeBundleVersion,
maybeCliVersion,
logger,
const toolcacheDestination = getToolcacheDestination({ logger }, source);
const extractedBundlePath = toolcacheDestination.orElse(
getTempExtractionDir(tempDir),
);
const extractedBundlePath =
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
const statusReport = await downloadAndExtract(
codeqlURL,
compressionMethod,
@@ -827,46 +894,95 @@ export const downloadCodeQL = async function (
logger,
);
if (!toolcacheInfo) {
logger.debug(
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
`URL ${codeqlURL}.`,
);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? "unknown",
};
if (toolcacheDestination.isSuccess()) {
writeToolcacheMarkerFile(toolcacheDestination.value, logger);
} else {
logger.debug(toolcacheDestination.value);
}
writeToolcacheMarkerFile(toolcacheInfo.path, logger);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? toolcacheInfo.version,
statusReport:
bundle.kind === "per-language"
? {
...statusReport,
perLanguage: { tools_bundle_language: bundle.language },
}
: statusReport,
};
};
function getToolcacheDestinationInfo(
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
logger: Logger,
): { path: string; version: string } | undefined {
if (maybeBundleVersion) {
const version = getCanonicalToolcacheVersion(
maybeCliVersion,
maybeBundleVersion,
logger,
/**
* Returns the canonical toolcache directory, or the reason the bundle cannot be cached.
*/
function getToolcacheDestination(
{ logger }: ActionState<["Logger"]>,
source: CodeQLDownloadSource,
): util.Result<string, string> {
if (source.bundle.kind !== "combined") {
return new util.Failure(
"Not caching the CodeQL tools because they came from a bundle that contains only a " +
"single language.",
);
}
if (!source.bundleVersion) {
return new util.Failure(
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
`URL ${source.bundle.url}.`,
);
return {
path: getToolcacheDirectory(version),
version,
};
}
return undefined;
return new util.Success(
getToolcacheDirectory(
getCanonicalToolcacheVersion(
source.cliVersion,
source.bundleVersion,
logger,
),
),
);
}
/**
* Reclaims disk space by deleting the CodeQL tools from the toolcache, if enabled.
*
* On GitHub-hosted runners the toolcache shares a filesystem with the workspace, so tools left in
* the toolcache take up space that the analysis could use instead. This holds wherever we extract
* the tools we are obtaining, since the toolcache is on that filesystem either way.
*/
async function tryDeleteToolcacheBundles({
env,
features,
logger,
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>): Promise<void> {
// A step that has already set up CodeQL may hand out a path into the toolcache that a later step
// runs, so only the first step to set it up can know that nothing else relies on the toolcache.
if (env.getOptional(EnvVar.HAS_SET_UP_CODEQL) !== undefined) {
logger.debug(
"Not deleting the CodeQL tools from the toolcache since a previous step in this job has " +
"already set up CodeQL.",
);
return;
}
if (
!isGitHubHostedRunner() ||
!isToolcacheOnWorkspaceFilesystem(logger) ||
!(await features.getValue(Feature.CleanupToolcacheBundles))
) {
return;
}
const result = await deleteToolcacheBundles({ env, logger });
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/toolcache-bundle-cleanup",
"Toolcache CodeQL bundle cleanup",
{ ...result },
),
);
}
export function getCodeQLURLVersion(url: string): string {
@@ -949,7 +1065,6 @@ export async function setupCodeQLBundle(
);
let codeqlFolder: string;
let toolsVersion = source.toolsVersion;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsSource: ToolsSource;
switch (source.sourceType) {
@@ -970,17 +1085,13 @@ export async function setupCodeQLBundle(
toolsSource = ToolsSource.Toolcache;
break;
case "download": {
const result = await downloadCodeQL(
source.codeqlURL,
source.compressionMethod,
source.bundleVersion,
source.cliVersion,
const result = await downloadCodeQLBundle(
{ env: getEnv(), features, logger },
source,
apiDetails,
zstdAvailability.version,
tempDir,
logger,
);
toolsVersion = result.toolsVersion;
codeqlFolder = result.codeqlFolder;
toolsDownloadStatusReport = result.statusReport;
toolsSource = ToolsSource.Download;
@@ -989,14 +1100,86 @@ export async function setupCodeQLBundle(
default:
util.assertNever(source);
}
// Record that this job now has a copy of the CodeQL tools, so that a later step doesn't delete
// the toolcache out from under the path we are about to return.
core.exportVariable(EnvVar.HAS_SET_UP_CODEQL, "true");
return {
codeqlFolder,
toolsDownloadStatusReport,
toolsSource,
toolsVersion,
toolsVersion: source.toolsVersion,
};
}
/**
* Performs eligible toolcache cleanup once, then downloads and extracts the resolved bundle.
*
* If an automatically selected per-language bundle is missing, downloads the combined bundle
* from the same release instead. Explicit bundle URLs are not substituted.
*
* @returns The extraction directory and download timings.
*/
export async function downloadCodeQLBundle(
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
): Promise<{
codeqlFolder: string;
statusReport: ToolsDownloadStatusReport;
}> {
const { bundle } = source;
const { logger } = action;
await tryDeleteToolcacheBundles(action);
const startTime = performance.now();
try {
return await downloadCodeQL(
source,
apiDetails,
tarVersion,
tempDir,
logger,
);
} catch (e) {
if (
bundle.kind !== "per-language" ||
bundle.combinedBundleURL === undefined ||
util.asHTTPError(e)?.status !== 404
) {
throw e;
}
logger.warning(
`No per-language CodeQL bundle for '${bundle.language}' was found at ${bundle.url}, so ` +
"falling back to the bundle that contains all languages. This analysis will still " +
"produce correct results, but will take longer to set up.",
);
const result = await downloadCodeQL(
{
...source,
bundle: { kind: "combined", url: bundle.combinedBundleURL },
},
apiDetails,
tarVersion,
tempDir,
logger,
);
return {
...result,
statusReport: {
...result.statusReport,
totalDurationMs: util.durationMsSince(startTime),
perLanguage: { tools_per_language_bundle_fallback: true },
},
};
}
}
async function useZstdBundle(
cliVersion: string,
tarSupportsZstd: boolean,
@@ -1014,9 +1197,15 @@ function getTempExtractionDir(tempDir: string) {
}
/**
* Get the URL of the latest nightly CodeQL bundle.
* Selects a bundle from the latest nightly release, preferring a per-language bundle when eligible.
* Records the combined bundle URL from that release for use if the selected asset is missing.
*/
async function getNightlyToolsUrl(logger: Logger) {
async function getLatestNightlyBundle(
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
rawLanguages: string[] | undefined,
variant: util.GitHubVariant,
): Promise<CodeQLBundle> {
const { logger } = action;
const zstdAvailability = await tar.isZstdAvailable(logger);
// The nightly is guaranteed to have a zstd bundle
const compressionMethod = (await useZstdBundle(
@@ -1026,6 +1215,15 @@ async function getNightlyToolsUrl(logger: Logger) {
? "zstd"
: "gzip";
const language = await getPerLanguageBundleLanguage(action, {
rawLanguages,
cliVersion: undefined,
compressionMethod,
platform: getBundlePlatform(),
variant,
isLatestNightly: true,
});
try {
// Since nightlies are prereleases, we can't just download the latest release
// on the repository. So instead we need to find the latest pre-release
@@ -1041,7 +1239,17 @@ async function getNightlyToolsUrl(logger: Logger) {
if (!latestRelease) {
throw new Error("Could not find the latest nightly release.");
}
return `https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${getCodeQLBundleName(compressionMethod)}`;
const assetUrl = (name: string) =>
`https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${name}`;
const url = assetUrl(getCodeQLBundleName(compressionMethod, language));
return language === undefined
? { kind: "combined", url }
: {
kind: "per-language",
url,
language,
combinedBundleURL: assetUrl(getCodeQLBundleName(compressionMethod)),
};
} catch (e) {
throw new Error(
`Failed to retrieve the latest nightly release: ${util.wrapError(e)}`,

View File

@@ -14,7 +14,10 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { getCachedCodeQlVersion } from "./cli/output-cache";
import {
getCachedCodeQlVersion,
getCommandCacheFilePath,
} from "./cli/output-cache";
import type { Config } from "./config/action-config";
import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
@@ -376,7 +379,10 @@ export async function createStatusReportBase(
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion(logger, getEnv());
const codeQlCliVersion = getCachedCodeQlVersion(
logger,
getCommandCacheFilePath(getEnv()),
);
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
const testingEnvironment = getTestingEnvironment();
// re-export the testing environment variable so that it is available to subsequent steps,
@@ -618,16 +624,6 @@ export interface InitWithConfigStatusReport extends InitStatusReport {
config_file: string;
}
/** Fields of the init status report populated when the tools source is `download`. */
export interface InitToolsDownloadFields {
/** Time taken to download the bundle, in milliseconds. */
tools_download_duration_ms?: number;
/**
* Whether the relevant tools dotcom feature flags have been misconfigured.
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
tools_feature_flags_valid?: boolean;
}
/**
* Composes a `InitWithConfigStatusReport` from the given values.
*

View File

@@ -0,0 +1,80 @@
import test from "ava";
import { BuiltInLanguage } from "../languages";
import { createInitToolsDownloadFields } from "./tools-download";
test("createInitToolsDownloadFields omits absent download data", (t) => {
t.deepEqual(createInitToolsDownloadFields(undefined, undefined), {});
});
test("createInitToolsDownloadFields reports feature flags without a download", (t) => {
t.deepEqual(createInitToolsDownloadFields(undefined, false), {
tools_feature_flags_valid: false,
});
});
test("createInitToolsDownloadFields reports only the total for a streaming download", (t) => {
t.deepEqual(
createInitToolsDownloadFields({ totalDurationMs: 300 }, undefined),
{ tools_total_duration_ms: 300 },
);
});
test("createInitToolsDownloadFields preserves per-language metadata", (t) => {
t.deepEqual(
createInitToolsDownloadFields(
{
totalDurationMs: 300,
perLanguage: { tools_bundle_language: BuiltInLanguage.java },
},
true,
),
{
tools_total_duration_ms: 300,
tools_bundle_language: BuiltInLanguage.java,
tools_feature_flags_valid: true,
},
);
});
test("createInitToolsDownloadFields preserves fallback and per-attempt timings", (t) => {
t.deepEqual(
createInitToolsDownloadFields(
{
downloadDurationMs: 200,
extractionDurationMs: 100,
totalDurationMs: 1000,
perLanguage: { tools_per_language_bundle_fallback: true },
},
undefined,
),
{
tools_download_duration_ms: 200,
tools_extraction_duration_ms: 100,
tools_total_duration_ms: 1000,
tools_per_language_bundle_fallback: true,
},
);
});
test("createInitToolsDownloadFields preserves zero durations and false flags", (t) => {
t.deepEqual(
createInitToolsDownloadFields(
{
downloadDurationMs: 0,
extractionDurationMs: 0,
totalDurationMs: 0,
perLanguage: { tools_per_language_bundle_fallback: false },
},
false,
),
{
tools_download_duration_ms: 0,
tools_extraction_duration_ms: 0,
tools_total_duration_ms: 0,
tools_per_language_bundle_fallback: false,
tools_feature_flags_valid: false,
},
);
});

View File

@@ -0,0 +1,56 @@
import type { ToolsDownloadStatusReport } from "../tools-download";
/** Telemetry describing per-language bundle downloads. */
export interface PerLanguageToolsStatusReport {
/** The language of the single-language bundle that was downloaded, if any. */
tools_bundle_language?: string;
/**
* Whether we tried to download a single-language bundle, but it did not exist and we fell back to
* the combined bundle.
*/
tools_per_language_bundle_fallback?: boolean;
}
/** Fields of the init status report populated when the tools source is `download`. */
export interface InitToolsDownloadFields extends PerLanguageToolsStatusReport {
/**
* Time taken to download the bundle, in milliseconds. Not populated when the bundle is downloaded
* and extracted concurrently.
*/
tools_download_duration_ms?: number;
/**
* Time taken to extract the bundle, in milliseconds. Not populated when the bundle is downloaded
* and extracted concurrently.
*/
tools_extraction_duration_ms?: number;
/**
* Total time taken to make the bundle available on disk, including failed download attempts
* before a fallback, in milliseconds.
*/
tools_total_duration_ms?: number;
/**
* Whether the relevant tools dotcom feature flags have been misconfigured.
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
tools_feature_flags_valid?: boolean;
}
/** Converts download results to telemetry fields shared by the init and setup-codeql Actions. */
export function createInitToolsDownloadFields(
report: ToolsDownloadStatusReport | undefined,
toolsFeatureFlagsValid: boolean | undefined,
): InitToolsDownloadFields {
const fields: InitToolsDownloadFields = { ...report?.perLanguage };
if (report?.downloadDurationMs !== undefined) {
fields.tools_download_duration_ms = report.downloadDurationMs;
}
if (report?.extractionDurationMs !== undefined) {
fields.tools_extraction_duration_ms = report.extractionDurationMs;
}
if (report?.totalDurationMs !== undefined) {
fields.tools_total_duration_ms = report.totalDurationMs;
}
if (toolsFeatureFlagsValid !== undefined) {
fields.tools_feature_flags_valid = toolsFeatureFlagsValid;
}
return fields;
}

View File

@@ -32,6 +32,7 @@ import {
} from "./feature-flags";
import { Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import { getBundlePlatform } from "./platform";
import { ActionName } from "./status-report";
import {
DEFAULT_DEBUG_ARTIFACT_NAME,
@@ -933,19 +934,14 @@ export function mockBundleDownloadApi({
platformSpecific?: boolean;
tagName: string;
}): string {
const platform =
process.platform === "win32"
? "win64"
: process.platform === "linux"
? "linux64"
: "osx64";
const platform = platformSpecific ? getBundlePlatform() : undefined;
const baseUrl = apiDetails?.url ?? "https://example.com";
const bundleUrls = ["tar.gz", "tar.zst"].map((extension) => {
const relativeUrl = apiDetails
? `/${repo}/releases/download/${tagName}/codeql-bundle${
platformSpecific ? `-${platform}` : ""
platform !== undefined ? `-${platform}` : ""
}.${extension}`
: `/download/${tagName}/codeql-bundle.${extension}`;

View File

@@ -1,8 +1,12 @@
import { once } from "events";
import * as fs from "fs";
import { ClientRequest, IncomingMessage } from "http";
import * as path from "path";
import * as core from "@actions/core";
import * as toolcache from "@actions/tool-cache";
import test from "ava";
import { https } from "follow-redirects";
import nock from "nock";
import * as sinon from "sinon";
@@ -10,14 +14,14 @@ import { getRunnerLogger } from "./logging";
import * as tar from "./tar";
import { setupTests } from "./testing-utils";
import { downloadAndExtract } from "./tools-download";
import { withTmpDir } from "./util";
import * as util from "./util";
setupTests(test);
test.serial(
"downloadAndExtract reports the duration when downloading before extracting",
"downloadAndExtract reports the durations when downloading before extracting",
async (t) => {
await withTmpDir(async (tmpDir) => {
await util.withTmpDir(async (tmpDir) => {
const archivePath = path.join(tmpDir, "codeql-bundle.tar.gz");
const destination = path.join(tmpDir, "codeql");
sinon.stub(toolcache, "downloadTool").resolves(archivePath);
@@ -34,6 +38,8 @@ test.serial(
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
t.assert(Number.isInteger(statusReport.extractionDurationMs));
t.assert(Number.isInteger(statusReport.totalDurationMs));
});
},
);
@@ -41,13 +47,16 @@ test.serial(
test.serial(
"downloadAndExtract falls back to downloading before extracting if streaming fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.resolves(archivePath);
.callsFake(async () => {
t.false(fs.existsSync(destination));
return archivePath;
});
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
@@ -66,6 +75,83 @@ test.serial(
getRunnerLogger(true),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
t.assert(Number.isInteger(statusReport.totalDurationMs));
t.true(request.isDone());
t.false(extractTarZst.called);
t.true(downloadTool.calledOnce);
t.true(extract.calledOnce);
});
},
);
test.serial(
"downloadAndExtract rethrows a 404 rather than retrying the download",
async (t) => {
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon.stub(toolcache, "downloadTool");
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
.get("/codeql-bundle.tar.zst")
.reply(404, "Not found");
const error = await t.throwsAsync(
downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
),
{
instanceOf: util.HTTPError,
message:
"Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst. HTTP status code: 404.",
},
);
t.is(error?.status, 404);
t.true(request.isDone());
t.false(extractTarZst.called);
t.false(downloadTool.called);
t.false(fs.existsSync(destination));
});
},
);
test.serial(
"downloadAndExtract falls back to downloading before extracting on a server error",
async (t) => {
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.callsFake(async () => {
t.false(fs.existsSync(destination));
return archivePath;
});
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
.get("/codeql-bundle.tar.zst")
.reply(500);
const statusReport = await downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
t.true(request.isDone());
t.false(extractTarZst.called);
@@ -76,9 +162,57 @@ test.serial(
);
test.serial(
"downloadAndExtract omits the download duration when streaming extraction",
"downloadAndExtract handles an unknown status as a non-HTTP error",
async (t) => {
await withTmpDir(async (tmpDir) => {
const asHTTPError = sinon.spy(util, "asHTTPError");
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
const destination = path.join(tmpDir, "codeql");
const response = sinon.createStubInstance(IncomingMessage);
response.statusCode = undefined;
sinon
.stub(https, "get")
.callsArgWith(2, response)
.returns(sinon.createStubInstance(ClientRequest));
const warning = sinon.stub(core, "warning");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.resolves(archivePath);
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
await downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
);
t.is(
warning.firstCall.args[0],
"Failed to download and extract CodeQL bundle using streaming with error: Failed to download CodeQL bundle from https://example.com/codeql-bundle.tar.zst.",
);
t.true(response.resume.calledOnce);
t.false(extractTarZst.called);
t.true(downloadTool.calledOnce);
t.true(extract.calledOnce);
});
t.true(asHTTPError.calledOnce);
t.true(asHTTPError.firstCall.args[0] instanceof Error);
t.false(asHTTPError.firstCall.args[0] instanceof util.HTTPError);
t.is(asHTTPError.firstCall.returnValue, undefined);
},
);
test.serial(
"downloadAndExtract reports only the total duration when streaming extraction",
async (t) => {
await util.withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const downloadTool = sinon.stub(toolcache, "downloadTool");
const extractTarZst = sinon
@@ -106,7 +240,9 @@ test.serial(
getRunnerLogger(true),
);
t.deepEqual(statusReport, {});
t.assert(Number.isInteger(statusReport.totalDurationMs));
t.is(statusReport.downloadDurationMs, undefined);
t.is(statusReport.extractionDurationMs, undefined);
t.false(downloadTool.called);
t.true(extractTarZst.calledOnce);
t.true(request.isDone());

View File

@@ -10,9 +10,19 @@ import * as toolcache from "@actions/tool-cache";
import { https } from "follow-redirects";
import * as semver from "semver";
import { ActionState } from "./action-common";
import { ActionsEnvVars, getEnv, ReadOnlyEnv } from "./environment";
import { formatDuration, Logger } from "./logging";
import type { PerLanguageToolsStatusReport } from "./status-report/tools-download";
import * as tar from "./tar";
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
import {
asHTTPError,
cleanUpPath,
durationMsSince,
getErrorMessage,
getRequiredEnvParam,
HTTPError,
} from "./util";
/**
* High watermark to use when streaming the download and extraction of the CodeQL tools.
@@ -31,7 +41,22 @@ const STREAMING_STALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const TOOLCACHE_TOOL_NAME = "CodeQL";
export type ToolsDownloadStatusReport = {
/**
* Time spent downloading the bundle, in milliseconds. Not populated when the bundle is downloaded
* and extracted concurrently, since the two cannot be told apart.
*/
downloadDurationMs?: number;
/**
* Time spent extracting the bundle, in milliseconds. Not populated when the bundle is downloaded
* and extracted concurrently, since the two cannot be told apart.
*/
extractionDurationMs?: number;
/**
* Total time taken to make the bundle available on disk, including failed download attempts
* before a fallback, in milliseconds.
*/
totalDurationMs: number;
perLanguage?: PerLanguageToolsStatusReport;
};
export async function downloadAndExtract(
@@ -47,11 +72,12 @@ export async function downloadAndExtract(
`Downloading CodeQL tools from ${codeqlURL} . This may take a while.`,
);
const startTime = performance.now();
try {
if (compressionMethod === "zstd" && process.platform === "linux") {
logger.info(`Streaming the extraction of the CodeQL bundle.`);
const toolsInstallStart = performance.now();
await downloadAndExtractZstdWithStreaming(
codeqlURL,
dest,
@@ -61,26 +87,30 @@ export async function downloadAndExtract(
logger,
);
const combinedDurationMs = Math.round(
performance.now() - toolsInstallStart,
);
const totalDurationMs = durationMsSince(startTime);
logger.info(
`Finished downloading and extracting CodeQL bundle to ${dest} (${formatDuration(
combinedDurationMs,
totalDurationMs,
)}).`,
);
return {};
return { totalDurationMs };
}
} catch (e) {
// If we failed during processing, we want to clean up the destination directory
// before we either try again or give up.
await cleanUpPath(dest, "CodeQL bundle", logger);
// Retrying a 404 is pointless: the asset does not exist, so downloading it a different way
// will fail in the same way.
if (asHTTPError(e)?.status === 404) {
throw e;
}
core.warning(
`Failed to download and extract CodeQL bundle using streaming with error: ${getErrorMessage(e)}`,
);
core.warning(`Falling back to downloading the bundle before extracting.`);
// If we failed during processing, we want to clean up the destination directory
// before we try again.
await cleanUpPath(dest, "CodeQL bundle", logger);
}
const toolsDownloadStart = performance.now();
@@ -90,7 +120,7 @@ export async function downloadAndExtract(
authorization,
headers,
);
const downloadDurationMs = Math.round(performance.now() - toolsDownloadStart);
const downloadDurationMs = durationMsSince(toolsDownloadStart);
logger.info(
`Finished downloading CodeQL bundle to ${archivedBundlePath} (${formatDuration(
@@ -98,7 +128,7 @@ export async function downloadAndExtract(
)}).`,
);
let extractionDurationMs: number;
let extractionDurationMs: number | undefined;
try {
logger.info("Extracting CodeQL bundle.");
@@ -110,7 +140,7 @@ export async function downloadAndExtract(
tarVersion,
logger,
);
extractionDurationMs = Math.round(performance.now() - extractionStart);
extractionDurationMs = durationMsSince(extractionStart);
logger.info(
`Finished extracting CodeQL bundle to ${dest} (${formatDuration(
extractionDurationMs,
@@ -120,7 +150,11 @@ export async function downloadAndExtract(
await cleanUpPath(archivedBundlePath, "CodeQL bundle archive", logger);
}
return { downloadDurationMs };
return {
downloadDurationMs,
extractionDurationMs,
totalDurationMs: durationMsSince(startTime),
};
}
async function downloadAndExtractZstdWithStreaming(
@@ -172,24 +206,167 @@ async function downloadAndExtractZstdWithStreaming(
if (response.statusCode !== 200) {
// Discard the response body so that the connection can be released.
response.resume();
throw new Error(
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
);
const baseMessage = `Failed to download CodeQL bundle from ${codeqlURL}.`;
if (response.statusCode !== undefined) {
throw new HTTPError(
`${baseMessage} HTTP status code: ${response.statusCode}.`,
response.statusCode,
);
}
throw new Error(baseMessage);
}
await tar.extractTarZst(response, dest, tarVersion, logger);
}
/** Gets the path to the toolcache directory that holds all versions of the CodeQL tools. */
function getToolcacheToolDirectory(env: ReadOnlyEnv): string {
return path.join(
env.getRequired(ActionsEnvVars.RUNNER_TOOL_CACHE),
TOOLCACHE_TOOL_NAME,
);
}
/** Gets the name of the toolcache directory that holds the given version of the CodeQL tools. */
function getToolcacheVersionDirectoryName(version: string): string {
return semver.clean(version) || version;
}
/** Gets the path to the toolcache directory for the specified version of the CodeQL tools. */
export function getToolcacheDirectory(version: string): string {
return path.join(
getRequiredEnvParam("RUNNER_TOOL_CACHE"),
TOOLCACHE_TOOL_NAME,
semver.clean(version) || version,
getToolcacheToolDirectory(getEnv()),
getToolcacheVersionDirectoryName(version),
os.arch() || "",
);
}
/**
* Whether the toolcache is on the same filesystem as the workspace, and so whether deleting the
* tools frees up disk space that the analysis can use.
*
* These are separate volumes on some runner images. Windows runners, for example, keep the
* toolcache on `C:` while the workspace is on `D:`.
*/
export function isToolcacheOnWorkspaceFilesystem(logger: Logger): boolean {
try {
return (
fs.statSync(getRequiredEnvParam("RUNNER_TOOL_CACHE")).dev ===
fs.statSync(getRequiredEnvParam("GITHUB_WORKSPACE")).dev
);
} catch (e) {
logger.debug(
`Could not determine whether the toolcache is on the same filesystem as the workspace: ${getErrorMessage(e)}`,
);
return false;
}
}
/** The outcome of trying to reclaim disk space by deleting the CodeQL tools from the toolcache. */
export interface ToolcacheCleanupResult {
/** The versions of the CodeQL tools that were deleted. */
deletedVersions: string[];
/**
* Whether we hit an error while trying to delete the tools. Distinguishes a toolcache that had
* nothing to reclaim from one we failed to clean up.
*/
failed: boolean;
}
/**
* Deletes every version of the CodeQL tools from the toolcache.
*
* Only safe to call when we are about to download the tools, since that means we did not resolve
* them from the toolcache and so nothing in there is in use by this job.
*
* This only ever touches the CodeQL directory of the toolcache. Cleanup errors are logged and
* returned as `failed: true` rather than thrown.
*
* @returns the versions that were deleted, and whether we hit an error while trying.
*/
export async function deleteToolcacheBundles({
env,
logger,
}: ActionState<["Logger", "ReadOnlyEnv"]>): Promise<ToolcacheCleanupResult> {
let toolDirectory: string;
try {
toolDirectory = getToolcacheToolDirectory(env);
} catch (e) {
logger.info(
`Unable to determine toolcache directory: ${getErrorMessage(e)}`,
);
return { deletedVersions: [], failed: true };
}
try {
// Refuse to follow a symlinked CodeQL directory, so that we can only ever delete paths that are
// really inside the toolcache.
if ((await fs.promises.lstat(toolDirectory)).isSymbolicLink()) {
logger.info(
`Not deleting the CodeQL tools from the toolcache since '${toolDirectory}' is a symlink.`,
);
return { deletedVersions: [], failed: true };
}
} catch (e: any) {
if (e?.code === "ENOENT") {
logger.debug(
`There are no CodeQL tools at '${toolDirectory}' to delete from the toolcache.`,
);
return { deletedVersions: [], failed: false };
}
logger.info(
`Failed to inspect the CodeQL tools at '${toolDirectory}': ${getErrorMessage(e)}`,
);
return { deletedVersions: [], failed: true };
}
try {
const entries = await fs.promises.readdir(toolDirectory, {
withFileTypes: true,
});
const deletedVersions: string[] = [];
let failed = false;
for (const entry of entries) {
// `isDirectory` is false for a symlink, so we never delete a version directory that is
// really somewhere else.
if (!entry.isDirectory()) {
logger.debug(
`Not deleting '${entry.name}' from the CodeQL toolcache since it is not a directory.`,
);
continue;
}
const versionDirectory = path.join(toolDirectory, entry.name);
try {
await fs.promises.rm(versionDirectory, {
force: true,
recursive: true,
});
deletedVersions.push(entry.name);
logger.info(
`Deleted the CodeQL tools at '${versionDirectory}' from the toolcache to free up disk space.`,
);
} catch (e) {
failed = true;
logger.info(
`Failed to delete the CodeQL tools at '${versionDirectory}' from the toolcache: ${getErrorMessage(e)}`,
);
}
}
return { deletedVersions: deletedVersions.sort(), failed };
} catch (e) {
logger.info(
`Failed to clean up the CodeQL toolcache at '${toolDirectory}': ${getErrorMessage(e)}`,
);
return { deletedVersions: [], failed: true };
}
}
export function writeToolcacheMarkerFile(
extractedPath: string,
logger: Logger,

View File

@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as os from "os";
import path from "path";
import { performance } from "perf_hooks";
import * as core from "@actions/core";
import test from "ava";
@@ -508,6 +509,26 @@ test("joinAtMost - truncates list if array is > than limit", (t) => {
t.false(result.includes("test6"));
});
test.serial(
"durationMsSince rounds elapsed milliseconds rather than the timestamps",
(t) => {
const startTime = 1000.25;
const now = sinon.stub(performance, "now");
for (const [endTime, expected] of [
[1000.25, 0],
[1000.74, 0],
[1000.75, 1],
[1001.74, 1],
[1001.75, 2],
[2000.74, 1000],
[2000.75, 1001],
]) {
now.returns(endTime);
t.is(util.durationMsSince(startTime), expected);
}
},
);
test("Success creates a success result", (t) => {
const result = new util.Success("test value");
t.true(result.isSuccess());

View File

@@ -2,6 +2,7 @@ import * as fs from "fs";
import * as fsPromises from "fs/promises";
import * as os from "os";
import * as path from "path";
import { performance } from "perf_hooks";
import * as core from "@actions/core";
import * as io from "@actions/io";
@@ -84,9 +85,32 @@ export async function withTmpDir<T>(
body: (tmpDir: string) => Promise<T>,
): Promise<T> {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codeql-action-"));
const result = await body(tmpDir);
await fs.promises.rm(tmpDir, { force: true, recursive: true });
return result;
try {
return await body(tmpDir);
} finally {
await fs.promises.rm(tmpDir, { force: true, recursive: true });
}
}
/**
* Creates a temporary file with the given contents, runs the given body, and
* then deletes the file. Note that, to create a temporary file, we first create
* a temporary directory via {@link withTmpDir} and then create the file within
* that directory.
* @param baseFileName The name to assign the temporary file.
* @param contents The contents to write to the temporary file.
* @param body The function to execute with the temporary file.
*/
export async function withTmpFile<T>(
baseFileName: string,
contents: string,
body: (filePath: string) => Promise<T> | T,
): Promise<T> {
return withTmpDir(async (tmpDir) => {
const filePath = path.join(tmpDir, baseFileName);
fs.writeFileSync(filePath, contents);
return body(filePath);
});
}
/**
@@ -658,6 +682,11 @@ export async function bundleDb(
return databaseBundlePath;
}
/** Returns the elapsed milliseconds, rounded, since `startTime` was recorded with `performance.now()`. */
export function durationMsSince(startTime: number): number {
return Math.round(performance.now() - startTime);
}
/**
* @param milliseconds time to delay
* @param opts options
@@ -842,9 +871,13 @@ export async function checkForTimeout() {
* directory with the name hostedtoolcache which is present on
* GitHub-hosted runners.
*
* @returns true iff the runner is hosted by GitHub
* Since this is a heuristic over how the runner happens to be named and laid out, it also matches
* self-hosted runners that are configured to resemble hosted ones. Prefer
* `isGitHubHostedRunner` when you need the answer the Actions service reports.
*
* @returns true iff the runner looks like it is hosted by GitHub
*/
export function isHostedRunner() {
export function looksLikeHostedRunner() {
return (
// Name of the runner on hosted Windows runners
process.env["RUNNER_NAME"]?.includes("Hosted Agent") ||

View File