Compare commits

..

227 Commits

Author SHA1 Message Date
Michael B. Gale
1cf30a546e Add global.json 2025-11-13 19:18:54 +00:00
Michael B. Gale
ba454b8ab4 Merge pull request #3284 from github/mbg/ci/fix-enterprise-workflow
Fix `update-supported-enterprise-server-versions.yml` workflow
2025-11-12 15:35:56 +00:00
Michael B. Gale
7a7cd8565c Don't push for PR event 2025-11-12 15:09:25 +00:00
Michael B. Gale
fd830db27b Trigger on PR for relevant changes 2025-11-12 15:05:11 +00:00
Michael B. Gale
a7e52b690b Perform sparse checkout 2025-11-12 15:04:21 +00:00
Michael B. Gale
71c3720f43 Run npm ci in update-supported-enterprise-server-versions.yml 2025-11-12 14:57:05 +00:00
Michael B. Gale
534824ea1b Merge pull request #3117 from github/mbg/csharp/new-cache-key-calculation
Support non-lock files for C# cache key computation
2025-11-12 11:03:09 +00:00
Henry Mercer
1d9f357d01 Merge pull request #3281 from github/dependabot/npm_and_yarn/npm-minor-9dd9c1a8e4
Bump the npm-minor group with 4 updates
2025-11-11 10:21:24 +00:00
github-actions[bot]
3d7be7bf78 Rebuild 2025-11-10 17:49:58 +00:00
dependabot[bot]
63bb415fff Bump the npm-minor group with 4 updates
Bumps the npm-minor group with 4 updates: [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [esbuild](https://github.com/evanw/esbuild).


Updates `@eslint/js` from 9.39.0 to 9.39.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.1/packages/js)

Updates `@typescript-eslint/eslint-plugin` from 8.46.3 to 8.46.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.46.3 to 8.46.4
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.4/packages/parser)

Updates `esbuild` from 0.25.12 to 0.27.0
- [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.25.12...v0.27.0)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.46.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: esbuild
  dependency-version: 0.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 17:48:25 +00:00
Michael B. Gale
04bd5c6aab Merge pull request #3279 from github/mbg/lint/jsdoc-param-names
Linter: check JSDoc parameter names exist
2025-11-10 11:36:02 +00:00
Michael B. Gale
48a56f6b93 Add some tests for downloadDependencyCaches related to feature prefixes 2025-11-09 12:03:18 +00:00
Michael B. Gale
4885eb2ad9 Insert new featurePrefix after general cache key prefix 2025-11-09 11:17:02 +00:00
Michael B. Gale
a47d5507cf Restore earlier log messages for checkHashPatterns 2025-11-09 11:11:10 +00:00
Michael B. Gale
b0e9dfce55 Restore missing status.push resulting from a bad merge 2025-11-09 11:10:27 +00:00
Michael B. Gale
35c91ef0af Add tests for getCsharpHashPatterns
- Make the function more easily testable by allowing `makePatternCheck` to be stubbed.
- Use `makePatternCheck` for base patterns as well.
2025-11-09 11:03:15 +00:00
Michael B. Gale
71abac76d2 Fix comment in getCsharpHashPatterns 2025-11-09 10:32:10 +00:00
Michael B. Gale
5b58b8f9c5 Linter: check JSDoc parameter names exist 2025-11-09 10:26:36 +00:00
Michael B. Gale
46e03b48bc Fix JSDoc param name 2025-11-09 10:05:18 +00:00
Henry Mercer
71d0a56d44 Merge pull request #3278 from github/henrymercer/type-fun
Use generic types for durations in status report
2025-11-06 10:47:44 +00:00
Henry Mercer
04285cbe85 Use generic types for durations in status report
This means we don't need to update this interface for every new language.
2025-11-05 18:54:44 +00:00
Michael B. Gale
26804552e4 Use undefined instead of NoMatchingFilesError
Add tests for `makePatternCheck` and `checkHashPatterns`
2025-11-05 17:23:22 +00:00
Michael B. Gale
03b2dc2a3f Add and use getFeaturePrefix for dependency caching 2025-11-05 16:33:21 +00:00
Michael B. Gale
0cbd930deb Move createCacheKeyHash to caching-utils 2025-11-05 16:15:26 +00:00
Michael B. Gale
0324490286 Use additional files for C# key hashes if Feature.CsharpNewCacheKey is enabled 2025-11-05 16:03:41 +00:00
Michael B. Gale
6b48207907 Move check whether there are files for hashing into getHashPatterns 2025-11-05 16:03:39 +00:00
Mario Campos
320a6b661b Merge pull request #3272 from github/mario-campos/v4-warning
Update deprecation warnings for CodeQL Action to v4
2025-11-05 10:01:33 -06:00
Michael B. Gale
ab1c84236a Change hash to be a function that can use Features 2025-11-05 15:57:57 +00:00
Michael B. Gale
2a7680fca6 Change getDefaultCacheConfig to be a const by turning paths into a function
Changing `paths` to be a function is necessary to allow `getTemporaryDirectory` to be called
2025-11-05 15:57:55 +00:00
Michael B. Gale
2aa1f55f3d Propagate features into cachePrefix function 2025-11-05 15:54:28 +00:00
Michael B. Gale
1ca20ab026 Add CsharpNewCacheKey FF 2025-11-05 15:48:04 +00:00
Mario Campos
b5e5a258e6 Merge branch 'main' into mario-campos/v4-warning 2025-11-05 09:39:54 -06:00
Mario Campos
74f662193b Reformat with eslint 2025-11-05 09:37:42 -06:00
Mario Campos
ecee3ea8f5 Update CHANGELOG.md. 2025-11-05 09:18:30 -06:00
Mario Campos
6a63bc6af3 Change warning message to just v3 (exclude v1, v2). 2025-11-05 09:15:53 -06:00
Henry Mercer
e2ef519c75 Merge pull request #3224 from github/henrymercer/clean-up-resolve-languages-ff
Clean up `resolve_supported_languages_using_cli` FF
2025-11-05 11:57:35 +00:00
Michael B. Gale
423d14e583 Merge pull request #3277 from github/mbg/ci/update-bundle-python
Install Python in `update-bundle` workflow
2025-11-05 11:53:17 +00:00
Michael B. Gale
6dd11f73d3 Update .github/workflows/script/bundle_changelog.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 11:34:26 +00:00
Michael B. Gale
c9f82f2adf Move python script to file 2025-11-05 11:22:38 +00:00
Michael B. Gale
33684ef869 Add setup-python step to update-bundle workflow 2025-11-05 11:20:06 +00:00
Michael B. Gale
5aa2d63d5b Merge branch 'main' into mario-campos/v4-warning 2025-11-05 11:03:25 +00:00
Michael B. Gale
862f5666b3 Merge pull request #3275 from github/mbg/checks/filter-ccr
Filter CCR jobs in `update-required-checks.sh`
2025-11-05 10:15:57 +00:00
Michael B. Gale
b00addd1d3 Merge pull request #3274 from github/mbg/macos-13 2025-11-05 09:01:53 +00:00
Michael B. Gale
d03fd76232 Filter CCR jobs in update-required-checks.sh 2025-11-04 22:23:12 +00:00
Michael B. Gale
9d5565fba2 Remove macos-13 from codeql workflow 2025-11-04 21:29:25 +00:00
Mario Campos
a570795dfc Clarify the CHANGELOG.md entry to reflect the whole action, and not just init. 2025-11-04 12:02:15 -06:00
Mario Campos
9366f80399 Reference GHES 3.20 in the comment, not 3.19.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 12:00:11 -06:00
Mario Campos
c443dff433 Simplify warning language to not enumerate deprecated versions. 2025-11-04 11:57:44 -06:00
Mario Campos
b32a1e0627 Update test cases for GitHub Enterprise Server versions 3.11 and 3.12 to reflect correct deprecation status 2025-11-04 11:51:23 -06:00
Mario Campos
08dc635f27 Restore use of sinon.match(). 2025-11-04 11:11:08 -06:00
Mario Campos
f1ca6a4f47 Update tests to reflect deprecation status of CodeQL Action v3 2025-11-04 11:07:15 -06:00
Mario Campos
ba82f9bd34 Fix deprecation warning to reflect that v3 is not actually deprecated yet. 2025-11-04 10:13:07 -06:00
Mario Campos
5a9b49de7e Update CHANGELOG to reflect warning for v3 users migrating to v4 of CodeQL Action 2025-11-04 10:09:47 -06:00
Mario Campos
1aade295bc Update deprecation warnings for CodeQL Action to v4 2025-11-04 09:59:16 -06:00
Henry Mercer
95b1867cf7 Merge pull request #3269 from github/dependabot/npm_and_yarn/actions/io-2.0.0
Bump @actions/io from 1.1.3 to 2.0.0
2025-11-04 14:23:27 +00:00
github-actions[bot]
238f5f2946 Rebuild 2025-11-04 14:00:06 +00:00
dependabot[bot]
a53b4967d7 Bump @actions/io from 1.1.3 to 2.0.0
Bumps [@actions/io](https://github.com/actions/toolkit/tree/HEAD/packages/io) from 1.1.3 to 2.0.0.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/io/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/io)

---
updated-dependencies:
- dependency-name: "@actions/io"
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 13:58:17 +00:00
Henry Mercer
493ffd8e5c Merge pull request #3268 from github/dependabot/npm_and_yarn/types/archiver-7.0.0
Bump @types/archiver from 6.0.4 to 7.0.0
2025-11-04 13:56:51 +00:00
github-actions[bot]
f23547cd26 Rebuild 2025-11-04 12:25:05 +00:00
dependabot[bot]
58f5e3dab8 Bump @types/archiver from 6.0.4 to 7.0.0
Bumps [@types/archiver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/archiver) from 6.0.4 to 7.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/archiver)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 12:19:24 +00:00
Henry Mercer
98e0ffef96 Merge pull request #3266 from github/dependabot/npm_and_yarn/npm-minor-29e8df1594
Bump the npm-minor group with 7 updates
2025-11-04 11:09:56 +00:00
github-actions[bot]
85eb524170 Rebuild 2025-11-04 10:34:56 +00:00
dependabot[bot]
a3ea4ef532 Bump the npm-minor group with 7 updates
Bumps the npm-minor group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@octokit/request-error](https://github.com/octokit/request-error.js) | `7.0.1` | `7.0.2` |
| [octokit](https://github.com/octokit/octokit.js) | `5.0.4` | `5.0.5` |
| [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat) | `1.4.0` | `1.4.1` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.38.0` | `9.39.0` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.46.2` | `8.46.3` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.46.2` | `8.46.3` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.11` | `0.25.12` |


Updates `@octokit/request-error` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/octokit/request-error.js/releases)
- [Commits](https://github.com/octokit/request-error.js/compare/v7.0.1...v7.0.2)

Updates `octokit` from 5.0.4 to 5.0.5
- [Release notes](https://github.com/octokit/octokit.js/releases)
- [Commits](https://github.com/octokit/octokit.js/compare/v5.0.4...v5.0.5)

Updates `@eslint/compat` from 1.4.0 to 1.4.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-v1.4.1/packages/compat)

Updates `@eslint/js` from 9.38.0 to 9.39.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.0/packages/js)

Updates `@typescript-eslint/eslint-plugin` from 8.46.2 to 8.46.3
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.3/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.46.2 to 8.46.3
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.3/packages/parser)

Updates `esbuild` from 0.25.11 to 0.25.12
- [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.25.11...v0.25.12)

---
updated-dependencies:
- dependency-name: "@octokit/request-error"
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: octokit
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@eslint/compat"
  dependency-version: 1.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@eslint/js"
  dependency-version: 9.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.46.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: esbuild
  dependency-version: 0.25.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 18:47:33 +00:00
Henry Mercer
97580d7533 Merge pull request #3267 from github/dependabot/npm_and_yarn/octokit/types-16.0.0
Bump @octokit/types from 15.0.1 to 16.0.0
2025-11-03 18:12:51 +00:00
Henry Mercer
7ad64f0258 Merge pull request #3270 from github/dependabot/npm_and_yarn/actions/http-client-3.0.0
Bump @actions/http-client from 2.2.3 to 3.0.0
2025-11-03 18:11:30 +00:00
github-actions[bot]
61bcb70dce Rebuild 2025-11-03 17:22:15 +00:00
github-actions[bot]
5313cd14a8 Rebuild 2025-11-03 17:21:50 +00:00
dependabot[bot]
8fa298d060 Bump @actions/http-client from 2.2.3 to 3.0.0
Bumps [@actions/http-client](https://github.com/actions/toolkit/tree/HEAD/packages/http-client) from 2.2.3 to 3.0.0.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/http-client/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/http-client)

---
updated-dependencies:
- dependency-name: "@actions/http-client"
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 17:20:38 +00:00
dependabot[bot]
7d1bed2dd0 Bump @octokit/types from 15.0.1 to 16.0.0
Bumps [@octokit/types](https://github.com/octokit/types.ts) from 15.0.1 to 16.0.0.
- [Release notes](https://github.com/octokit/types.ts/releases)
- [Commits](https://github.com/octokit/types.ts/compare/v15.0.1...v16.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 17:20:12 +00:00
Michael B. Gale
338146ca93 Merge pull request #3264 from github/mbg/ci/publish-on-tag
Create immutable action version on tag push
2025-10-31 16:48:43 +00:00
Michael B. Gale
64db1da706 Create immutable action version on tag push 2025-10-31 16:24:23 +00:00
Henry Mercer
9bd8638576 Merge pull request #3262 from github/mergeback/v4.31.2-to-main-0499de31
Mergeback v4.31.2 refs/heads/releases/v4 into main
2025-10-30 15:07:33 +00:00
github-actions[bot]
65da12b256 Rebuild 2025-10-30 14:36:57 +00:00
github-actions[bot]
c228fecc25 Update changelog and version after v4.31.2 2025-10-30 14:33:10 +00:00
Henry Mercer
0499de31b9 Merge pull request #3261 from github/henrymercer/setup-python
Set up Python in mergeback workflow
2025-10-30 14:30:19 +00:00
Henry Mercer
3b96745d2b Set up Python in mergeback workflow 2025-10-30 14:06:12 +00:00
Michael B. Gale
8a06050a8c Merge pull request #3259 from github/update-v4.31.2-9576b5cbe
Merge main into releases/v4
2025-10-30 13:47:48 +00:00
github-actions[bot]
752a642cb2 Update changelog for v4.31.2 2025-10-30 13:27:33 +00:00
Michael B. Gale
9576b5cbe8 Merge pull request #3258 from github/mbg/enablement-errors/case-insensitive
Make `isEnablementError` case-insensitive
2025-10-30 11:59:43 +00:00
Henry Mercer
cc8843728c Merge pull request #3257 from github/henrymercer/ubuntu-slim
Run lightweight workflows on `ubuntu-slim`
2025-10-30 11:44:22 +00:00
Michael B. Gale
f0e9bf07f4 Make isEnablementError case-insensitive 2025-10-30 11:37:07 +00:00
Henry Mercer
2a3599c520 Run lightweight workflows on ubuntu-slim 2025-10-30 11:25:32 +00:00
Henry Mercer
514ff4d116 Merge pull request #3256 from github/henrymercer/resolve-bad-merge
Remove unused `console-log-level` dependency
2025-10-30 11:19:24 +00:00
Henry Mercer
aab1c2f931 Merge pull request #3253 from github/mergeback/v4.31.1-to-main-5fe9434c
Mergeback v4.31.1 refs/heads/releases/v4 into main
2025-10-30 11:04:54 +00:00
Henry Mercer
b2bffa615d Remove unused console-log-level dependency
This was added back by a bad merge.
2025-10-30 10:40:23 +00:00
github-actions[bot]
e7811794d3 Rebuild 2025-10-30 10:31:33 +00:00
github-actions[bot]
c7a5b09374 Merge remote-tracking branch 'origin/main' into mergeback/v4.31.1-to-main-5fe9434c 2025-10-30 10:30:29 +00:00
github-actions[bot]
cbcb06a3ae Update changelog and version after v4.31.1 2025-10-30 10:29:22 +00:00
Henry Mercer
5fe9434cd2 Merge pull request #3252 from github/update-v4.31.1-777daa0c7
Merge main into releases/v4
2025-10-30 10:28:34 +00:00
Henry Mercer
8d50be301c Merge pull request #3245 from github/dependabot/github_actions/dot-github/workflows/actions/download-artifact-6
Bump actions/download-artifact from 5 to 6 in /.github/workflows
2025-10-30 10:02:36 +00:00
github-actions[bot]
237497c8f0 Update changelog for v4.31.1 2025-10-30 09:40:55 +00:00
Michael B. Gale
777daa0c71 Merge pull request #3239 from github/mbg/remove/add-snippets
Remove `add-snippets` input
2025-10-29 13:32:20 +00:00
Michael B. Gale
74c8748a6f Update analyze/action.yml
Co-authored-by: Esben Sparre Andreasen <esbena@github.com>
2025-10-29 10:34:13 +00:00
Michael B. Gale
34c50c1d29 Merge pull request #3251 from github/mbg/user-error/enablement
Turn enablement errors into configuration errors
2025-10-29 09:57:38 +00:00
Michael B. Gale
4ae68afd84 Warn if the add-snippets input is used 2025-10-29 09:29:28 +00:00
Michael B. Gale
52a7bd7b6e Check for 403 status 2025-10-29 08:35:19 +00:00
Michael B. Gale
194ba0ee2d Make error message tests less brittle 2025-10-29 08:29:11 +00:00
Michael B. Gale
53acf0b8aa Turn enablement errors into configuration errors 2025-10-28 21:17:30 +00:00
Henry Mercer
ac9aeee226 Merge pull request #3249 from github/henrymercer/api-logging
Use Actions logger in API client
2025-10-28 17:05:58 +00:00
Henry Mercer
d49e837b8c Merge branch 'main' into henrymercer/api-logging 2025-10-28 16:49:10 +00:00
Henry Mercer
3d988b275a Pass minimal copy of core 2025-10-28 16:33:21 +00:00
Henry Mercer
8cc18acfa4 Merge pull request #3250 from github/henrymercer/prefer-fs-delete
Use Node `fs` APIs instead of `del`
2025-10-28 15:45:56 +00:00
Henry Mercer
ea5cb4a016 Merge branch 'main' into henrymercer/prefer-fs-delete 2025-10-28 15:26:42 +00:00
Henry Mercer
e1c8976a56 Merge pull request #3247 from github/henrymercer/disk-usage-node-api
Check disk usage using Node.js API
2025-10-28 15:24:12 +00:00
Henry Mercer
4256e2e2a0 Merge branch 'main' into henrymercer/disk-usage-node-api 2025-10-28 15:05:13 +00:00
Henry Mercer
66459ea37c Apply suggestion 2025-10-28 15:04:31 +00:00
Henry Mercer
1af9394995 Merge pull request #3244 from github/dependabot/github_actions/dot-github/workflows/actions-minor-b11285d543
Bump ruby/setup-ruby from 1.265.0 to 1.267.0 in /.github/workflows in the actions-minor group across 1 directory
2025-10-28 13:28:36 +00:00
Henry Mercer
311fc42780 Merge pull request #3242 from github/dependabot/npm_and_yarn/npm-minor-75b724c14c
Bump the npm-minor group with 4 updates
2025-10-28 13:25:25 +00:00
Henry Mercer
284bf9b047 Merge pull request #3241 from github/dependabot/github_actions/dot-github/workflows/actions/upload-artifact-5
Bump actions/upload-artifact from 4 to 5 in /.github/workflows
2025-10-28 13:23:08 +00:00
Henry Mercer
a53e78ee2a Merge pull request #3243 from github/dependabot/npm_and_yarn/actions/artifact-4.0.0
Bump @actions/artifact from 2.3.1 to 4.0.0
2025-10-28 13:12:23 +00:00
Henry Mercer
d84f470a9a Improve method naming 2025-10-28 13:06:14 +00:00
Henry Mercer
41c0a26213 Use Node fs APIs instead of del 2025-10-28 13:00:25 +00:00
Henry Mercer
d4ba404a20 Tweak assertions 2025-10-28 12:50:22 +00:00
Henry Mercer
55895ef678 Stub GITHUB_WORKSPACE in test 2025-10-28 12:45:22 +00:00
Henry Mercer
fe16891f40 Add unit test for checkDiskUsage 2025-10-28 12:40:23 +00:00
Henry Mercer
57c7b6afb6 Disable SIP disablement check 2025-10-28 12:35:41 +00:00
Michael B. Gale
44aeac1a37 Merge branch 'main' into dependabot/github_actions/dot-github/workflows/actions/download-artifact-6 2025-10-28 12:21:17 +00:00
Henry Mercer
8b1e55d11e Use Actions logger in API client
This allows us to remove the `console-log-level` dependency.
2025-10-28 12:15:03 +00:00
Henry Mercer
20900ee769 Build: Run npm install when package-lock.json out of date 2025-10-28 11:52:42 +00:00
Michael B. Gale
ad8ad9829e Merge pull request #3240 from github/mbg/allow-skip-workflow-validation
Support skipping workflow validation
2025-10-28 11:46:08 +00:00
Henry Mercer
239e305d18 Check disk usage using Node.js API
This was introduced in Node.js 18
2025-10-27 18:34:23 +00:00
dependabot[bot]
9c39f0afb0 Bump actions/download-artifact from 5 to 6 in /.github/workflows
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 18:05:01 +00:00
github-actions[bot]
fcc1377ac6 Rebuild 2025-10-27 17:54:41 +00:00
dependabot[bot]
b5bbb5ab73 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.265.0 to 1.267.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](ab177d40ee...d5126b9b35)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:52:58 +00:00
github-actions[bot]
723a9469fd Rebuild 2025-10-27 17:41:35 +00:00
dependabot[bot]
f9eed03ba2 Bump @actions/artifact from 2.3.1 to 4.0.0
Bumps [@actions/artifact](https://github.com/actions/toolkit/tree/HEAD/packages/artifact) from 2.3.1 to 4.0.0.
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/artifact/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/@actions/cache@4.0.0/packages/artifact)

---
updated-dependencies:
- dependency-name: "@actions/artifact"
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:39:59 +00:00
github-actions[bot]
df9e49e9e8 Rebuild 2025-10-27 17:37:24 +00:00
dependabot[bot]
c9d47e2ee9 Bump the npm-minor group with 4 updates
Bumps the npm-minor group with 4 updates: [@octokit/types](https://github.com/octokit/types.ts), [@types/archiver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/archiver), [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser).


Updates `@octokit/types` from 15.0.0 to 15.0.1
- [Release notes](https://github.com/octokit/types.ts/releases)
- [Commits](https://github.com/octokit/types.ts/compare/v15.0.0...v15.0.1)

Updates `@types/archiver` from 6.0.3 to 6.0.4
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/archiver)

Updates `@typescript-eslint/eslint-plugin` from 8.46.1 to 8.46.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.2/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.46.1 to 8.46.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.2/packages/parser)

---
updated-dependencies:
- dependency-name: "@octokit/types"
  dependency-version: 15.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@types/archiver"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.46.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:35:52 +00:00
github-actions[bot]
714962e17a Rebuild 2025-10-27 17:30:37 +00:00
dependabot[bot]
42f957bb51 Bump actions/upload-artifact from 4 to 5 in /.github/workflows
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:28:57 +00:00
Michael B. Gale
52cec4178d Downgrade log message from warning to debug level 2025-10-27 17:02:01 +00:00
Michael B. Gale
55c083790a Move checkWorkflow to workflow.ts 2025-10-27 17:01:23 +00:00
Michael B. Gale
50601762ea Also skip workflow validation for dynamic workflows 2025-10-27 16:10:08 +00:00
Michael B. Gale
06fbd897c4 Move workflow check to a function in init.ts and add tests 2025-10-27 15:57:44 +00:00
Michael B. Gale
127851b399 Add environment variable for skipping workflow validation 2025-10-27 15:42:43 +00:00
Kasper Svendsen
8d77149e0c Merge pull request #3238 from github/kaspersv/extract-diff-range-computation
Move diff-range computation into utils
2025-10-27 15:40:12 +01:00
Michael B. Gale
db47d17142 Remove add-snippets input 2025-10-27 12:53:23 +00:00
Kasper Svendsen
cc17bed958 Move diff-range computation tests 2025-10-27 09:46:16 +01:00
Kasper Svendsen
91ec0ed58f Move diff-range computation into utils for reuse 2025-10-27 09:43:11 +01:00
Kasper Svendsen
4e0b2cd814 Merge pull request #3232 from github/kaspersv/unique-overlay-base-keys
Ensure uniqueness of overlay-base database cache keys
2025-10-27 08:36:12 +01:00
Michael B. Gale
ae78991f55 Merge pull request #3236 from github/mergeback/v4.31.0-to-main-4e94bd11
Mergeback v4.31.0 refs/heads/releases/v4 into main
2025-10-24 18:30:37 +01:00
github-actions[bot]
dd565f3332 Rebuild 2025-10-24 17:11:09 +00:00
github-actions[bot]
fa46f22b12 Update changelog and version after v4.31.0 2025-10-24 17:08:58 +00:00
Michael B. Gale
4e94bd11f7 Merge pull request #3235 from github/update-v4.31.0-1d36546c1
Merge main into releases/v4
2025-10-24 18:08:08 +01:00
github-actions[bot]
8f11182164 Update changelog for v4.31.0 2025-10-24 16:33:59 +00:00
Michael B. Gale
1d36546c14 Merge pull request #3234 from github/mbg/changelog/post-processing
Add changelog entry for post-processing change
2025-10-24 17:26:22 +01:00
Michael B. Gale
08ada26e6a Add changelog entry for post-processing change 2025-10-24 17:07:13 +01:00
Michael B. Gale
b843cbeed0 Merge pull request #3233 from github/mbg/getOptionalEnvVar
Add `getOptionalEnvVar` helper
2025-10-24 16:55:48 +01:00
Michael B. Gale
1ecd563919 Use getOptionalEnvVar in writePostProcessedFiles 2025-10-24 16:18:09 +01:00
Henry Mercer
e576807920 Merge pull request #3223 from github/henrymercer/bump-minimum
Bump minimum CodeQL Bundle version to 2.17.6
2025-10-24 15:11:27 +01:00
Michael B. Gale
ad35676669 Add getOptionalEnvVar function
Also add tests for it and `getRequiredEnvParam`
2025-10-24 15:00:42 +01:00
Michael B. Gale
d75645b13f Merge pull request #3222 from github/mbg/upload-lib/post-process
Perform SARIF post-processing independently of upload
2025-10-24 14:59:04 +01:00
Kasper Svendsen
66759e57b2 Improve error handling for overlay-base cache key creation 2025-10-24 15:49:26 +02:00
Kasper Svendsen
cbcae45fff Reorder components of overlay-base cache key postfix 2025-10-24 15:46:17 +02:00
Michael B. Gale
710606cc35 Check that outputPath is non-empty 2025-10-24 14:42:36 +01:00
Michael B. Gale
f0452d5366 Consistently use "post-processing" 2025-10-24 10:20:25 +01:00
Kasper Svendsen
956c56734d Merge pull request #3231 from github/kaspersv/lower-overlay-base-size-limit
Overlay: Lower size limit for overlay base databases
2025-10-24 11:12:25 +02:00
Kasper Svendsen
b4ce335286 Ensure uniqueness of overlay-base database cache keys 2025-10-24 11:11:57 +02:00
Michael B. Gale
b9cd36824e Merge remote-tracking branch 'origin/main' into mbg/upload-lib/post-process 2025-10-24 10:08:38 +01:00
Chuan-kai Lin
c4b73722ba Add overlay-base database cache key tests 2025-10-24 10:47:17 +02:00
Kasper Svendsen
22d29ca74d Overlay: Lower size limit for overlay base databases 2025-10-24 08:06:42 +02:00
Michael B. Gale
9625890712 Merge pull request #3227 from github/mbg/permission-warning
Update wording in some log messages
2025-10-23 16:30:13 +01:00
Michael B. Gale
690d276755 Merge branch 'main' into mbg/permission-warning 2025-10-23 15:50:48 +01:00
Michael B. Gale
1c3c8066c3 Merge pull request #3228 from github/mbg/test/timeout
Bump timeout for `analyze-action-env` test
2025-10-23 15:49:27 +01:00
Michael B. Gale
da64a41e37 Bump timeout for analyze-action-input test 2025-10-23 15:23:21 +01:00
Michael B. Gale
8376af204a Bump timeout for analyze-action-env test 2025-10-23 13:39:38 +01:00
Michael B. Gale
f48b54af10 Fix fallback not being guarded by uploadKind check 2025-10-23 13:34:03 +01:00
Michael B. Gale
40b4cdd21f Update status report messages 2025-10-23 13:12:19 +01:00
Michael B. Gale
e849c567ec Update debug message 2025-10-23 13:04:06 +01:00
Michael B. Gale
d1b51f05c9 Update API permissions warning 2025-10-23 13:02:31 +01:00
Michael B. Gale
aed27f7231 Fix linter issue 2025-10-22 19:25:34 +01:00
Michael B. Gale
8ff870a6c2 Rename new input to processed-sarif-path 2025-10-22 19:12:57 +01:00
Michael B. Gale
6f0fcbeea7 Rename uploadSarif 2025-10-22 19:09:39 +01:00
Michael B. Gale
89d3359017 Improve test name 2025-10-22 19:05:05 +01:00
Michael B. Gale
d79c0a1339 Fix incomplete comment 2025-10-22 19:03:23 +01:00
Michael B. Gale
5e37670026 Use post-process-output in PR check 2025-10-22 19:01:42 +01:00
Michael B. Gale
def04c1c0e Add test for uploadSarif with output directory 2025-10-22 19:01:42 +01:00
Michael B. Gale
12f3cfef09 Write processed SARIF files if post-process-output input is provided 2025-10-22 19:01:40 +01:00
Michael B. Gale
c2bec36917 Add post-process-output input to analyze action 2025-10-22 19:00:33 +01:00
Michael B. Gale
14139c9f77 Add test for uploadSarif with upload: never 2025-10-22 19:00:33 +01:00
Michael B. Gale
596de7f1bc Move UploadKind check into uploadSarif 2025-10-22 19:00:29 +01:00
Michael B. Gale
899bf2fd1e Use postProcessSarifFiles and uploadProcessedFiles in uploadSarif 2025-10-22 18:48:24 +01:00
Michael B. Gale
6fbdd5f4e9 Split SARIF uploading steps from uploadSpecifiedFiles into a function 2025-10-22 18:48:03 +01:00
Michael B. Gale
489ed914f1 Split SARIF post-processing steps from uploadSpecifiedFiles into a function 2025-10-22 18:48:00 +01:00
Michael B. Gale
42642085de Merge pull request #3206 from github/mbg/analyze/use-upload-sarif
Use `uploadSarif` rather than `uploadFiles` in `analyze` action
2025-10-22 17:45:25 +01:00
Henry Mercer
4bd7dfe989 Merge pull request #3226 from github/henrymercer/prefer-optional-chaining
Linting: Prefer optional chaining
2025-10-22 17:13:00 +01:00
Michael B. Gale
ebd514f490 Address review comments 2025-10-22 17:11:19 +01:00
Henry Mercer
a691497d40 Clean up resolve_supported_languages_using_cli FF 2025-10-22 17:04:42 +01:00
Henry Mercer
e5f165b8f5 Linting: Prefer optional chaining 2025-10-22 16:55:06 +01:00
Michael B. Gale
c98d5a9a4f Use checkoutPath and category constants consistently 2025-10-22 16:12:07 +01:00
Michael B. Gale
b7c814cb39 Gate uploadSarif behind FF, use old implementation otherwise 2025-10-22 15:54:51 +01:00
Michael B. Gale
f88cb01694 Add AnalyzeUseNewUpload feature 2025-10-22 15:49:28 +01:00
Henry Mercer
3cd3374657 Bump minor version number 2025-10-22 12:27:15 +01:00
Henry Mercer
3934593862 Remove analysisSummaryV2Default FF 2025-10-22 12:25:25 +01:00
Henry Mercer
bab3f2b5f5 Remove sarifMergeRunsFromEqualCategory FF 2025-10-22 12:22:55 +01:00
Henry Mercer
9924f476ba Add changelog note 2025-10-22 12:20:17 +01:00
Henry Mercer
bd5f49c7ca Bump minimum version to 2.17.6 2025-10-22 12:19:35 +01:00
Michael B. Gale
02b2c3aafc Fix style inconsistency 2025-10-22 12:04:04 +01:00
Michael B. Gale
aa048acb05 Merge branch 'main' into mbg/analyze/use-upload-sarif 2025-10-22 00:42:55 +01:00
Michael B. Gale
0c5185d061 Merge pull request #3221 from github/mbg/code-quality/skip-db-upload
Always skip database upload if `AnalysisKind.CodeScanning` is not enabled
2025-10-21 13:10:37 +01:00
Michael B. Gale
79ed9569a3 Always skip database upload if AnalysisKind.CodeScanning is not enabled 2025-10-21 12:33:56 +01:00
Henry Mercer
8e53c48f94 Merge pull request #3217 from github/henrymercer/http-error-handling
Wrap API configuration errors when setting up CodeQL
2025-10-21 12:15:21 +01:00
Henry Mercer
804fc665f9 Merge branch 'main' into henrymercer/http-error-handling 2025-10-21 10:37:41 +01:00
Henry Mercer
e6e649a8f3 Simplify API error checks 2025-10-21 10:31:53 +01:00
Henry Mercer
40e26468f3 Require message field too 2025-10-21 10:27:54 +01:00
Michael B. Gale
9b0ac1cc3b Merge pull request #3203 from github/mbg/errors/more-user-errors
Handle user errors for invalid `UserConfig`s and missing query files
2025-10-20 19:32:51 +01:00
Michael B. Gale
ffed63adb8 Merge pull request #3219 from github/dependabot/npm_and_yarn/npm-minor-5ed6ededba
Bump the npm-minor group with 5 updates
2025-10-20 19:14:12 +01:00
Michael B. Gale
bee06ec042 Merge pull request #3220 from github/dependabot/github_actions/dot-github/workflows/actions/setup-node-6
Bump actions/setup-node from 5 to 6 in /.github/workflows
2025-10-20 19:09:32 +01:00
github-actions[bot]
06f31ec789 Rebuild 2025-10-20 17:27:37 +00:00
dependabot[bot]
53588c5ad2 Bump actions/setup-node from 5 to 6 in /.github/workflows
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 17:26:07 +00:00
github-actions[bot]
2357c43cad Rebuild 2025-10-20 17:18:26 +00:00
dependabot[bot]
a3ff966dbf Bump the npm-minor group with 5 updates
Bumps the npm-minor group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [octokit](https://github.com/octokit/octokit.js) | `5.0.3` | `5.0.4` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.37.0` | `9.38.0` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.46.0` | `8.46.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.46.0` | `8.46.1` |
| [esbuild](https://github.com/evanw/esbuild) | `0.25.10` | `0.25.11` |


Updates `octokit` from 5.0.3 to 5.0.4
- [Release notes](https://github.com/octokit/octokit.js/releases)
- [Commits](https://github.com/octokit/octokit.js/compare/v5.0.3...v5.0.4)

Updates `@eslint/js` from 9.37.0 to 9.38.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.38.0/packages/js)

Updates `@typescript-eslint/eslint-plugin` from 8.46.0 to 8.46.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.46.0 to 8.46.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.46.1/packages/parser)

Updates `esbuild` from 0.25.10 to 0.25.11
- [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.25.10...v0.25.11)

---
updated-dependencies:
- dependency-name: octokit
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@eslint/js"
  dependency-version: 9.38.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.46.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.46.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: esbuild
  dependency-version: 0.25.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 17:17:00 +00:00
Henry Mercer
6562050a4e Merge pull request #3218 from github/henrymercer/pr-sizes
Add experimental functionality for labelling PRs by their size
2025-10-20 17:45:46 +01:00
Henry Mercer
e9daf5bcd9 Comment version that is pinned
Co-authored-by: Michael B. Gale <mbg@github.com>
2025-10-20 17:25:01 +01:00
Henry Mercer
c13672ee32 Bump sizes a bit 2025-10-20 16:48:51 +01:00
Henry Mercer
f2f52d0d47 Add score for XL 2025-10-20 15:13:53 +01:00
Henry Mercer
08e53bec85 Update .github/sizeup.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-20 15:12:50 +01:00
Henry Mercer
519594fe94 Update workflow name 2025-10-20 15:12:25 +01:00
Henry Mercer
8c324fe288 Add experimental functionality for labelling PRs by their size 2025-10-20 15:10:40 +01:00
Henry Mercer
a6b9514fab Wrap API configuration errors when setting up CodeQL 2025-10-20 15:01:44 +01:00
Henry Mercer
c64c4070cc Handle HTTP errors with httpStatusCode property 2025-10-20 14:38:02 +01:00
Henry Mercer
d88a5540c3 Merge pull request #3215 from github/mergeback/v4.30.9-to-main-16140ae1
Mergeback v4.30.9 refs/heads/releases/v4 into main
2025-10-17 17:06:54 +01:00
github-actions[bot]
aa0f6ea898 Rebuild 2025-10-17 15:40:22 +00:00
github-actions[bot]
b03dcd5d9d Update changelog and version after v4.30.9 2025-10-17 15:23:37 +00:00
Henry Mercer
16140ae1a1 Merge pull request #3213 from github/update-v4.30.9-70205d3d1
Merge main into releases/v4
2025-10-17 16:22:48 +01:00
github-actions[bot]
30db5fee08 Update changelog for v4.30.9 2025-10-17 14:54:08 +00:00
Michael B. Gale
2ade8a09a3 Use uploadSarif rather than uploadFiles in analyze action 2025-10-14 19:49:42 +01:00
101 changed files with 75508 additions and 78779 deletions

View File

@@ -16,9 +16,9 @@ runs:
shell: bash
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: '3.12'
- name: Install dependencies
run: |

55
.github/sizeup.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
labeling:
applyCategoryLabels: true
categoryLabelPrefix: "size/"
commenting:
addCommentWhenScoreThresholdHasBeenExceeded: false
sizeup:
categories:
- name: extra small
lte: 25
label:
name: XS
description: Should be very easy to review
color: 3cbf00
- name: small
lte: 100
label:
name: S
description: Should be easy to review
color: 5d9801
- name: medium
lte: 250
label:
name: M
description: Should be of average difficulty to review
color: 7f7203
- name: large
lte: 500
label:
name: L
description: May be hard to review
color: a14c05
- name: extra large
lte: 1000
label:
name: XL
description: May be very hard to review
color: c32607
- name: extra extra large
label:
name: XXL
description: May be extremely hard to review
color: e50009
ignoredFilePatterns:
- ".github/workflows/__*"
- "lib/**/*"
- "package-lock.json"
testFilePatterns:
- "**/*.test.ts"
scoring:
# This formula and the aliases below it are written in prefix notation.
# For an explanation of how this works, please see:
# https://github.com/lerebear/sizeup-core/blob/main/README.md#prefix-notation
formula: "- - + additions deletions comments whitespace"

View File

@@ -79,7 +79,7 @@ jobs:
output: ${{ runner.temp }}/results
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-zstd-bundle.sarif
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -67,7 +67,7 @@ jobs:
output: ${{ runner.temp }}/results
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: config-export-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: npm

View File

@@ -78,7 +78,7 @@ jobs:
output: ${{ runner.temp }}/results
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: diagnostics-export-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -85,7 +85,7 @@ jobs:
with:
output: ${{ runner.temp }}/results
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: with-baseline-information-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -64,7 +64,7 @@ jobs:
with:
output: ${{ runner.temp }}/results
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -73,7 +73,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: npm

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: npm

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: npm

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: npm

View File

@@ -80,9 +80,10 @@ jobs:
with:
output: ${{ runner.temp }}/results
upload-database: false
post-processed-sarif-path: ${{ runner.temp }}/post-processed
- name: Upload security SARIF
if: contains(matrix.analysis-kinds, 'code-scanning')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: |
quality-queries-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.sarif.json
@@ -90,12 +91,20 @@ jobs:
retention-days: 7
- name: Upload quality SARIF
if: contains(matrix.analysis-kinds, 'code-quality')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: |
quality-queries-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.quality.sarif.json
path: ${{ runner.temp }}/results/javascript.quality.sarif
retention-days: 7
- name: Upload post-processed SARIF
uses: actions/upload-artifact@v5
with:
name: |
post-processed-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.sarif.json
path: ${{ runner.temp }}/post-processed
retention-days: 7
if-no-files-found: error
- name: Check quality query does not appear in security SARIF
if: contains(matrix.analysis-kinds, 'code-scanning')
uses: actions/github-script@v8

View File

@@ -56,7 +56,7 @@ jobs:
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Set up Ruby
uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0
uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -15,7 +15,7 @@ defaults:
jobs:
check-expected-release-files:
runs-on: ubuntu-latest
runs-on: ubuntu-slim
permissions:
contents: read

View File

@@ -81,7 +81,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04,ubuntu-24.04,windows-2022,windows-2025,macos-13,macos-14,macos-15]
os: [ubuntu-22.04,ubuntu-24.04,windows-2022,windows-2025,macos-14,macos-15]
tools: ${{ fromJson(needs.check-codeql-versions.outputs.versions) }}
runs-on: ${{ matrix.os }}

View File

@@ -56,7 +56,7 @@ jobs:
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'

View File

@@ -79,7 +79,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
- name: Check expected artifacts exist
run: |
LANGUAGES="cpp csharp go java javascript python"

View File

@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
- name: Check expected artifacts exist
run: |
VERSIONS="stable-v2.20.3 default linked nightly-latest"

26
.github/workflows/label-pr-size.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Label PR with size
on:
pull_request:
types:
- opened
- synchronize
- reopened
- edited
- ready_for_review
permissions:
contents: read
pull-requests: write
jobs:
sizeup:
name: Label PR with size
runs-on: ubuntu-slim
steps:
- name: Run sizeup
uses: lerebear/sizeup-action@b7beb3dd273e36039e16e48e7bc690c189e61951 # 0.8.12
with:
token: "${{ secrets.GITHUB_TOKEN }}"
configuration-file-path: ".github/sizeup.yml"

View File

@@ -24,7 +24,7 @@ defaults:
jobs:
merge-back:
runs-on: ubuntu-latest
runs-on: ubuntu-slim
environment: Automation
if: github.repository == 'github/codeql-action'
env:
@@ -47,7 +47,10 @@ jobs:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # ensure we have all tags and can push commits
- uses: actions/setup-node@v5
- uses: actions/setup-node@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Update git config
run: |

View File

@@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

View File

@@ -29,7 +29,7 @@ defaults:
jobs:
prepare:
name: "Prepare release"
runs-on: ubuntu-latest
runs-on: ubuntu-slim
if: github.repository == 'github/codeql-action'
permissions:

View File

@@ -1,8 +1,10 @@
name: 'Publish Immutable Action Version'
on:
release:
types: [published]
push:
tags:
# Match version tags, but not the major version tags.
- 'v[0-9]+.**'
defaults:
run:
@@ -10,30 +12,16 @@ defaults:
jobs:
publish:
runs-on: ubuntu-latest
runs-on: ubuntu-slim
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Check release name
id: check
env:
RELEASE_NAME: ${{ github.event.release.name }}
run: |
echo "Release name: ${{ github.event.release.name }}"
if [[ $RELEASE_NAME == v* ]]; then
echo "This is a CodeQL Action release. Create an Immutable Action"
echo "is-action-release=true" >> $GITHUB_OUTPUT
else
echo "This is a CodeQL Bundle release. Do not create an Immutable Action"
echo "is-action-release=false" >> $GITHUB_OUTPUT
fi
- name: Checking out
if: steps.check.outputs.is-action-release == 'true'
- name: Checkout repository
uses: actions/checkout@v5
- name: Publish
if: steps.check.outputs.is-action-release == 'true'
- name: Publish immutable release
id: publish
uses: actions/publish-immutable-action@v0.0.4

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

View File

@@ -0,0 +1,18 @@
import os
import re
# Get the PR number from the PR URL.
pr_number = os.environ['PR_URL'].split('/')[-1]
changelog_note = f"- Update default CodeQL bundle version to {os.environ['CLI_VERSION']}. [#{pr_number}]({os.environ['PR_URL']})"
# If the "[UNRELEASED]" section starts with "no user facing changes", remove that line.
with open('CHANGELOG.md', 'r') as f:
changelog = f.read()
changelog = changelog.replace('## [UNRELEASED]\n\nNo user facing changes.', '## [UNRELEASED]\n')
# Add the changelog note to the bottom of the "[UNRELEASED]" section.
changelog = re.sub(r'\n## (\d+\.\d+\.\d+)', f'{changelog_note}\n\n## \\1', changelog, count=1)
with open('CHANGELOG.md', 'w') as f:
f.write(changelog)

View File

@@ -29,7 +29,7 @@ fi
echo "Getting checks for $GITHUB_SHA"
# Ignore any checks with "https://", CodeQL, LGTM, Update, and ESLint checks.
CHECKS="$(gh api repos/github/codeql-action/commits/"${GITHUB_SHA}"/check-runs --paginate | jq --slurp --compact-output --raw-output '[.[].check_runs.[] | select(.conclusion != "skipped") | .name | select(contains("https://") or . == "CodeQL" or . == "Dependabot" or . == "check-expected-release-files" or contains("Update") or contains("ESLint") or contains("update") or contains("test-setup-python-scripts") | not)] | unique | sort')"
CHECKS="$(gh api repos/github/codeql-action/commits/"${GITHUB_SHA}"/check-runs --paginate | jq --slurp --compact-output --raw-output '[.[].check_runs.[] | select(.conclusion != "skipped") | .name | select(contains("https://") or . == "CodeQL" or . == "Dependabot" or . == "check-expected-release-files" or contains("Update") or contains("ESLint") or contains("update") or contains("test-setup-python-scripts") or . == "Agent" or . == "Cleanup artifacts" or . == "Prepare" or . == "Upload results" | not)] | unique | sort')"
echo "$CHECKS" | jq

View File

@@ -20,7 +20,7 @@ defaults:
jobs:
update-bundle:
if: github.event.release.prerelease && startsWith(github.event.release.tag_name, 'codeql-bundle-')
runs-on: ubuntu-latest
runs-on: ubuntu-slim
permissions:
contents: write # needed to push commits
pull-requests: write # needed to create pull requests
@@ -40,8 +40,13 @@ jobs:
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Set up Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
@@ -78,28 +83,8 @@ jobs:
echo "PR_URL=$pr_url" | tee -a "$GITHUB_ENV"
- name: Create changelog note
shell: python
run: |
import os
import re
# Get the PR number from the PR URL.
pr_number = os.environ['PR_URL'].split('/')[-1]
changelog_note = f"- Update default CodeQL bundle version to {os.environ['CLI_VERSION']}. [#{pr_number}]({os.environ['PR_URL']})"
# If the "[UNRELEASED]" section starts with "no user facing changes", remove that line.
# Use perl to avoid having to escape the newline character.
with open('CHANGELOG.md', 'r') as f:
changelog = f.read()
changelog = changelog.replace('## [UNRELEASED]\n\nNo user facing changes.', '## [UNRELEASED]\n')
# Add the changelog note to the bottom of the "[UNRELEASED]" section.
changelog = re.sub(r'\n## (\d+\.\d+\.\d+)', f'{changelog_note}\n\n## \\1', changelog, count=1)
with open('CHANGELOG.md', 'w') as f:
f.write(changelog)
python .github/workflows/script/bundle_changelog.py
- name: Push changelog note
run: |

View File

@@ -26,7 +26,7 @@ jobs:
update:
timeout-minutes: 45
runs-on: ubuntu-latest
runs-on: ubuntu-slim
if: github.event_name == 'workflow_dispatch'
needs: [prepare]
env:
@@ -77,7 +77,7 @@ jobs:
backport:
timeout-minutes: 45
runs-on: ubuntu-latest
runs-on: ubuntu-slim
environment: Automation
needs: [prepare]
if: ${{ (github.event_name == 'push') && needs.prepare.outputs.backport_target_branches != '[]' }}

View File

@@ -4,12 +4,18 @@ on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
pull_request:
branches:
- main
paths:
- .github/workflows/update-supported-enterprise-server-versions.yml
- .github/workflows/update-supported-enterprise-server-versions/update.py
jobs:
update-supported-enterprise-server-versions:
name: Update Supported Enterprise Server Versions
timeout-minutes: 45
runs-on: ubuntu-latest
runs-on: ubuntu-slim
if: github.repository == 'github/codeql-action'
permissions:
contents: write # needed to push commits
@@ -28,6 +34,7 @@ jobs:
repository: github/enterprise-releases
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
path: ${{ github.workspace }}/enterprise-releases/
sparse-checkout: releases.json
- name: Update Supported Enterprise Server Versions
run: |
cd ./.github/workflows/update-supported-enterprise-server-versions/
@@ -35,6 +42,7 @@ jobs:
pipenv install
pipenv run ./update.py
rm --recursive "$ENTERPRISE_RELEASES_PATH"
npm ci
npm run build
env:
ENTERPRISE_RELEASES_PATH: ${{ github.workspace }}/enterprise-releases/
@@ -44,25 +52,33 @@ jobs:
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Commit changes and open PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit changes
id: prepare-commit
run: |
if [[ -z $(git status --porcelain) ]]; then
echo "No changes to commit"
echo "committed=false" >> $GITHUB_OUTPUT
else
git checkout -b update-supported-enterprise-server-versions
git add .
git commit --message "Update supported GitHub Enterprise Server versions"
git push origin update-supported-enterprise-server-versions
body="This PR updates the list of supported GitHub Enterprise Server versions, either because a new "
body+="version is about to be feature frozen, or because an old release has been deprecated."
body+=$'\n\n'
body+="If an old release has been deprecated, please follow the instructions in CONTRIBUTING.md to "
body+="deprecate the corresponding version of CodeQL."
gh pr create --draft \
--title "Update supported GitHub Enterprise Server versions" \
--body "$body"
echo "committed=true" >> $GITHUB_OUTPUT
fi
- name: Open PR
if: github.event_name != 'pull_request' && steps.prepare-commit.outputs.committed == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git push origin update-supported-enterprise-server-versions
body="This PR updates the list of supported GitHub Enterprise Server versions, either because a new "
body+="version is about to be feature frozen, or because an old release has been deprecated."
body+=$'\n\n'
body+="If an old release has been deprecated, please follow the instructions in CONTRIBUTING.md to "
body+="deprecate the corresponding version of CodeQL."
gh pr create --draft \
--title "Update supported GitHub Enterprise Server versions" \
--body "$body"

View File

@@ -4,6 +4,23 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
- CodeQL Action v3 will be deprecated in December 2026. The Action now logs a warning for customers who are running v3 but could be running v4. For more information, see [Upcoming deprecation of CodeQL Action v3](https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/).
## 4.31.2 - 30 Oct 2025
No user facing changes.
## 4.31.1 - 30 Oct 2025
- The `add-snippets` input has been removed from the `analyze` action. This input has been deprecated since CodeQL Action 3.26.4 in August 2024 when this removal was announced.
## 4.31.0 - 24 Oct 2025
- Bump minimum CodeQL bundle version to 2.17.6. [#3223](https://github.com/github/codeql-action/pull/3223)
- When SARIF files are uploaded by the `analyze` or `upload-sarif` actions, the CodeQL Action automatically performs post-processing steps to prepare the data for the upload. Previously, these post-processing steps were only performed before an upload took place. We are now changing this so that the post-processing steps will always be performed, even when the SARIF files are not uploaded. This does not change anything for the `upload-sarif` action. For `analyze`, this may affect Advanced Setup for CodeQL users who specify a value other than `always` for the `upload` input. [#3222](https://github.com/github/codeql-action/pull/3222)
## 4.30.9 - 17 Oct 2025
- Update default CodeQL bundle version to 2.23.3. [#3205](https://github.com/github/codeql-action/pull/3205)
- Experimental: A new `setup-codeql` action has been added which is similar to `init`, except it only installs the CodeQL CLI and does not initialize a database. Do not use this in production as it is part of an internal experiment and subject to change at any time. [#3204](https://github.com/github/codeql-action/pull/3204)

View File

@@ -6,7 +6,7 @@ inputs:
description: The name of the check run to add text to.
required: false
output:
description: The path of the directory in which to save the SARIF results
description: The path of the directory in which to save the SARIF results from the CodeQL CLI.
required: false
default: "../results"
upload:
@@ -32,14 +32,10 @@ inputs:
and 13GB for macOS).
required: false
add-snippets:
description: Specify whether or not to add code snippets to the output sarif file.
description: Does not have any effect.
required: false
default: "false"
deprecationMessage: >-
The input "add-snippets" is deprecated and will be removed on the first release in August 2025.
When this input is set to true it is expected to add code snippets with an alert to the SARIF file.
However, since Code Scanning ignores code snippets provided as part of a SARIF file this is currently
a no operation. No alternative is available.
The input "add-snippets" has been removed and no longer has any effect.
skip-queries:
description: If this option is set, the CodeQL database will be built but no queries will be run on it. Thus, no results will be produced.
required: false
@@ -70,6 +66,12 @@ inputs:
description: Whether to upload the resulting CodeQL database
required: false
default: "true"
post-processed-sarif-path:
description: >-
Before uploading the SARIF files produced by the CodeQL CLI, the CodeQL Action may perform some post-processing
on them. Ordinarily, these post-processed SARIF files are not saved to disk. However, if a path is provided as an
argument for this input, they are written to the specified directory.
required: false
wait-for-processing:
description: If true, the Action will wait for the uploaded SARIF to be processed before completing.
required: true

View File

@@ -12,6 +12,7 @@ import filenames from "eslint-plugin-filenames";
import github from "eslint-plugin-github";
import _import from "eslint-plugin-import";
import noAsyncForeach from "eslint-plugin-no-async-foreach";
import jsdoc from "eslint-plugin-jsdoc";
import globals from "globals";
const __filename = fileURLToPath(import.meta.url);
@@ -52,6 +53,7 @@ export default [
github: fixupPluginRules(github),
import: fixupPluginRules(_import),
"no-async-foreach": noAsyncForeach,
"jsdoc": jsdoc,
},
languageOptions: {
@@ -131,7 +133,18 @@ export default [
"no-sequences": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"one-var": ["error", "never"],
// Check param names to ensure that we don't have outdated JSDocs.
"jsdoc/check-param-names": [
"error",
{
// We don't currently require full JSDoc coverage, so this rule
// should not error on missing @param annotations.
disableMissingParamChecks: true,
}
],
},
},
{

View File

@@ -1,29 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";
import { compileFromFile } from 'json-schema-to-typescript';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SRC_DIR = path.join(__dirname, "schemas");
const OUT_DIR = path.join(__dirname, "src");
async function generateTypings() {
const schemas = globSync(`${SRC_DIR}/*.json`);
for (const schema of schemas) {
const outPath = path.join(
OUT_DIR,
`${path.basename(schema, ".json")}.d.ts`,
);
const ts = await compileFromFile(schema, {
bannerComment:
"/* This file was automatically generated by `npm run generate:schemas`. Do not edit by hand. */",
});
fs.writeFileSync(outPath, ts, "utf-8");
}
}
await generateTypings();

13821
lib/analyze-action-post.js generated

File diff suppressed because one or more lines are too long

14172
lib/analyze-action.js generated

File diff suppressed because it is too large Load Diff

5802
lib/autobuild-action.js generated

File diff suppressed because it is too large Load Diff

22922
lib/init-action-post.js generated

File diff suppressed because one or more lines are too long

13696
lib/init-action.js generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

13167
lib/setup-codeql-action.js generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

5513
lib/start-proxy-action.js generated

File diff suppressed because it is too large Load Diff

13034
lib/upload-lib.js generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

13347
lib/upload-sarif-action.js generated

File diff suppressed because it is too large Load Diff

1914
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.30.9",
"version": "4.31.3",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -12,8 +12,7 @@
"ava": "npm run transpile && ava --serial --verbose",
"test": "npm run ava -- src/",
"test-debug": "npm run test -- --timeout=20m",
"transpile": "npm run generate:schemas && tsc --build --verbose",
"generate:schemas": "node json-schemas.mjs"
"transpile": "tsc --build --verbose"
},
"ava": {
"typescript": {
@@ -25,23 +24,20 @@
},
"license": "MIT",
"dependencies": {
"@actions/artifact": "^2.3.1",
"@actions/artifact": "^4.0.0",
"@actions/artifact-legacy": "npm:@actions/artifact@^1.1.2",
"@actions/cache": "^4.1.0",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/glob": "^0.5.0",
"@actions/http-client": "^2.2.3",
"@actions/io": "^1.1.3",
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^2.0.2",
"@octokit/plugin-retry": "^6.0.0",
"@octokit/request-error": "^7.0.1",
"@octokit/request-error": "^7.0.2",
"@schemastore/package": "0.0.10",
"archiver": "^7.0.1",
"check-disk-space": "^3.4.0",
"console-log-level": "^1.4.1",
"del": "^8.0.0",
"fast-deep-equal": "^3.1.3",
"follow-redirects": "^1.15.11",
"get-folder-size": "^5.0.0",
@@ -49,37 +45,36 @@
"jsonschema": "1.4.1",
"long": "^5.3.2",
"node-forge": "^1.3.1",
"octokit": "^5.0.3",
"octokit": "^5.0.5",
"semver": "^7.7.3",
"uuid": "^13.0.0"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
"@eslint/compat": "^1.4.0",
"@eslint/compat": "^1.4.1",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.37.0",
"@eslint/js": "^9.39.1",
"@microsoft/eslint-formatter-sarif": "^3.1.0",
"@octokit/types": "^15.0.0",
"@types/archiver": "^6.0.3",
"@types/console-log-level": "^1.4.5",
"@octokit/types": "^16.0.0",
"@types/archiver": "^7.0.0",
"@types/follow-redirects": "^1.14.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "20.19.9",
"@types/node-forge": "^1.3.14",
"@types/semver": "^7.7.1",
"@types/sinon": "^17.0.4",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.41.0",
"ava": "^6.4.1",
"esbuild": "^0.25.10",
"esbuild": "^0.27.0",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.8.7",
"eslint-plugin-filenames": "^1.3.2",
"eslint-plugin-github": "^5.1.8",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsdoc": "^61.1.12",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^11.0.3",
"json-schema-to-typescript": "^15.0.4",
"nock": "^14.0.10",
"sinon": "^21.0.0",
"typescript": "^5.9.3"

View File

@@ -27,7 +27,7 @@ steps:
output: ${{ runner.temp }}/results
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-zstd-bundle.sarif
path: ${{ runner.temp }}/results/javascript.sarif

View File

@@ -12,7 +12,7 @@ steps:
output: "${{ runner.temp }}/results"
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: config-export-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: "${{ runner.temp }}/results/javascript.sarif"

View File

@@ -25,7 +25,7 @@ steps:
output: "${{ runner.temp }}/results"
upload-database: false
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: diagnostics-export-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: "${{ runner.temp }}/results/javascript.sarif"

View File

@@ -17,7 +17,7 @@ steps:
with:
output: "${{ runner.temp }}/results"
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: with-baseline-information-${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: "${{ runner.temp }}/results/javascript.sarif"

View File

@@ -11,7 +11,7 @@ steps:
with:
output: "${{ runner.temp }}/results"
- name: Upload SARIF
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: ${{ matrix.os }}-${{ matrix.version }}.sarif.json
path: "${{ runner.temp }}/results/javascript.sarif"

View File

@@ -36,9 +36,10 @@ steps:
with:
output: "${{ runner.temp }}/results"
upload-database: false
post-processed-sarif-path: "${{ runner.temp }}/post-processed"
- name: Upload security SARIF
if: contains(matrix.analysis-kinds, 'code-scanning')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: |
quality-queries-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.sarif.json
@@ -46,12 +47,20 @@ steps:
retention-days: 7
- name: Upload quality SARIF
if: contains(matrix.analysis-kinds, 'code-quality')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: |
quality-queries-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.quality.sarif.json
path: "${{ runner.temp }}/results/javascript.quality.sarif"
retention-days: 7
- name: Upload post-processed SARIF
uses: actions/upload-artifact@v5
with:
name: |
post-processed-${{ matrix.os }}-${{ matrix.version }}-${{ matrix.analysis-kinds }}.sarif.json
path: "${{ runner.temp }}/post-processed"
retention-days: 7
if-no-files-found: error
- name: Check quality query does not appear in security SARIF
if: contains(matrix.analysis-kinds, 'code-scanning')
uses: actions/github-script@v8

View File

@@ -4,7 +4,7 @@ description: "Tests using RuboCop to analyze a multi-language repository and the
versions: ["default"]
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0
uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -117,7 +117,7 @@ for file in sorted((this_dir / 'checks').glob('*.yml')):
steps.extend([
{
'name': 'Install Node.js',
'uses': 'actions/setup-node@v5',
'uses': 'actions/setup-node@v6',
'with': {
'node-version': '20.x',
'cache': 'npm',

View File

@@ -9,9 +9,15 @@ if [ "$GITHUB_ACTIONS" = "true" ]; then
fi
# Check if npm install is likely needed before proceeding
if [ ! -d node_modules ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then
echo "Running 'npm install' because 'node_modules/.package-lock.json' appears to be outdated..."
if [ ! -d node_modules ]; then
echo "Running 'npm install' because 'node_modules' directory is missing."
npm install
elif [ package.json -nt package-lock.json ]; then
echo "Running 'npm install' because 'package-lock.json' appears to be outdated."
npm install
elif [ package-lock.json -nt node_modules/.package-lock.json ]; then
echo "Running 'npm install' because 'node_modules/.package-lock.json' appears to be outdated."
npm install
else
echo "Skipping 'npm install' because 'node_modules/.package-lock.json' appears to be up-to-date."
echo "Skipping 'npm install' because everything appears to be up-to-date."
fi

View File

@@ -24,6 +24,9 @@ setupTests(test);
// but the first test would fail.
test("analyze action with RAM & threads from environment variables", async (t) => {
// This test frequently times out on Windows with the default timeout, so we bump
// it a bit to 20s.
t.timeout(1000 * 20);
await util.withTmpDir(async (tmpDir) => {
process.env["GITHUB_SERVER_URL"] = util.GITHUB_DOTCOM_URL;
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
@@ -75,7 +78,7 @@ test("analyze action with RAM & threads from environment variables", async (t) =
t.deepEqual(runFinalizeStub.firstCall.args[1], "--threads=-1");
t.deepEqual(runFinalizeStub.firstCall.args[2], "--ram=4992");
t.assert(runQueriesStub.calledOnce);
t.deepEqual(runQueriesStub.firstCall.args[3], "--threads=-1");
t.deepEqual(runQueriesStub.firstCall.args[2], "--threads=-1");
t.deepEqual(runQueriesStub.firstCall.args[1], "--ram=4992");
});
});

View File

@@ -24,6 +24,7 @@ setupTests(test);
// but the first test would fail.
test("analyze action with RAM & threads from action inputs", async (t) => {
t.timeout(1000 * 20);
await util.withTmpDir(async (tmpDir) => {
process.env["GITHUB_SERVER_URL"] = util.GITHUB_DOTCOM_URL;
process.env["GITHUB_REPOSITORY"] = "github/codeql-action-fake-repository";
@@ -75,7 +76,7 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
t.deepEqual(runFinalizeStub.firstCall.args[1], "--threads=-1");
t.deepEqual(runFinalizeStub.firstCall.args[2], "--ram=3012");
t.assert(runQueriesStub.calledOnce);
t.deepEqual(runQueriesStub.firstCall.args[3], "--threads=-1");
t.deepEqual(runQueriesStub.firstCall.args[2], "--threads=-1");
t.deepEqual(runQueriesStub.firstCall.args[1], "--ram=3012");
});
});

View File

@@ -52,6 +52,7 @@ import {
} from "./trap-caching";
import * as uploadLib from "./upload-lib";
import { UploadResult } from "./upload-lib";
import { postProcessAndUploadSarif } from "./upload-sarif";
import * as util from "./util";
interface AnalysisStatusReport
@@ -211,7 +212,9 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
async function run() {
const startedAt = new Date();
let uploadResult: UploadResult | undefined = undefined;
let uploadResults:
| Partial<Record<analyses.AnalysisKind, UploadResult>>
| undefined = undefined;
let runStats: QueriesStatusReport | undefined = undefined;
let config: Config | undefined = undefined;
let trapCacheCleanupTelemetry: TrapCacheCleanupStatusReport | undefined =
@@ -321,10 +324,16 @@ async function run() {
);
if (actionsUtil.getRequiredInput("skip-queries") !== "true") {
// Warn if the removed `add-snippets` input is used.
if (actionsUtil.getOptionalInput("add-snippets") !== undefined) {
logger.warning(
"The `add-snippets` input has been removed and no longer has any effect.",
);
}
runStats = await runQueries(
outputDir,
memory,
util.getAddSnippetsFlag(actionsUtil.getRequiredInput("add-snippets")),
threads,
diffRangePackDir,
actionsUtil.getOptionalInput("category"),
@@ -341,31 +350,67 @@ async function run() {
}
core.setOutput("db-locations", dbLocations);
core.setOutput("sarif-output", path.resolve(outputDir));
const uploadInput = actionsUtil.getOptionalInput("upload");
if (runStats && actionsUtil.getUploadValue(uploadInput) === "always") {
if (isCodeScanningEnabled(config)) {
uploadResult = await uploadLib.uploadFiles(
outputDir,
actionsUtil.getRequiredInput("checkout_path"),
actionsUtil.getOptionalInput("category"),
features,
const uploadKind = actionsUtil.getUploadValue(
actionsUtil.getOptionalInput("upload"),
);
if (runStats) {
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
const category = actionsUtil.getOptionalInput("category");
if (await features.getValue(Feature.AnalyzeUseNewUpload)) {
uploadResults = await postProcessAndUploadSarif(
logger,
analyses.CodeScanning,
features,
uploadKind,
checkoutPath,
outputDir,
category,
actionsUtil.getOptionalInput("post-processed-sarif-path"),
);
core.setOutput("sarif-id", uploadResult.sarifID);
} else if (uploadKind === "always") {
uploadResults = {};
if (isCodeScanningEnabled(config)) {
uploadResults[analyses.AnalysisKind.CodeScanning] =
await uploadLib.uploadFiles(
outputDir,
checkoutPath,
category,
features,
logger,
analyses.CodeScanning,
);
}
if (isCodeQualityEnabled(config)) {
uploadResults[analyses.AnalysisKind.CodeQuality] =
await uploadLib.uploadFiles(
outputDir,
checkoutPath,
category,
features,
logger,
analyses.CodeQuality,
);
}
} else {
uploadResults = {};
logger.info("Not uploading results");
}
if (isCodeQualityEnabled(config)) {
const analysis = analyses.CodeQuality;
const qualityUploadResult = await uploadLib.uploadFiles(
outputDir,
actionsUtil.getRequiredInput("checkout_path"),
actionsUtil.getOptionalInput("category"),
features,
logger,
analysis,
// Set the SARIF id outputs only if we have results for them, to avoid
// having keys with empty values in the action output.
if (uploadResults[analyses.AnalysisKind.CodeScanning] !== undefined) {
core.setOutput(
"sarif-id",
uploadResults[analyses.AnalysisKind.CodeScanning].sarifID,
);
}
if (uploadResults[analyses.AnalysisKind.CodeQuality] !== undefined) {
core.setOutput(
"quality-sarif-id",
uploadResults[analyses.AnalysisKind.CodeQuality].sarifID,
);
core.setOutput("quality-sarif-id", qualityUploadResult.sarifID);
}
} else {
logger.info("Not uploading results");
@@ -393,14 +438,11 @@ async function run() {
// Store dependency cache(s) if dependency caching is enabled.
if (shouldStoreCache(config.dependencyCachingEnabled)) {
const minimizeJavaJars = await features.getValue(
Feature.JavaMinimizeDependencyJars,
codeql,
);
dependencyCacheResults = await uploadDependencyCaches(
codeql,
features,
config,
logger,
minimizeJavaJars,
);
}
@@ -408,12 +450,12 @@ async function run() {
if (util.isInTestMode()) {
logger.debug("In test mode. Waiting for processing is disabled.");
} else if (
uploadResult !== undefined &&
uploadResults?.[analyses.AnalysisKind.CodeScanning] !== undefined &&
actionsUtil.getRequiredInput("wait-for-processing") === "true"
) {
await uploadLib.waitForProcessing(
getRepositoryNwo(),
uploadResult.sarifID,
uploadResults[analyses.AnalysisKind.CodeScanning].sarifID,
getActionsLogger(),
);
}
@@ -450,13 +492,16 @@ async function run() {
return;
}
if (runStats && uploadResult) {
if (
runStats !== undefined &&
uploadResults?.[analyses.AnalysisKind.CodeScanning] !== undefined
) {
await sendStatusReport(
startedAt,
config,
{
...runStats,
...uploadResult.statusReport,
...uploadResults[analyses.AnalysisKind.CodeScanning].statusReport,
},
undefined,
trapCacheUploadTime,
@@ -466,7 +511,7 @@ async function run() {
dependencyCacheResults,
logger,
);
} else if (runStats) {
} else if (runStats !== undefined) {
await sendStatusReport(
startedAt,
config,

View File

@@ -4,10 +4,8 @@ import * as path from "path";
import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { CodeQuality, CodeScanning } from "./analyses";
import {
exportedForTesting,
runQueries,
defaultSuites,
resolveQuerySuiteAlias,
@@ -39,7 +37,6 @@ test("status report fields", async (t) => {
setupActionsVars(tmpDir, tmpDir);
const memoryFlag = "";
const addSnippetsFlag = "";
const threadsFlag = "";
sinon.stub(uploadLib, "validateSarifFileSchema");
@@ -105,7 +102,6 @@ test("status report fields", async (t) => {
const statusReport = await runQueries(
tmpDir,
memoryFlag,
addSnippetsFlag,
threadsFlag,
undefined,
undefined,
@@ -131,204 +127,6 @@ test("status report fields", async (t) => {
});
});
function runGetDiffRanges(changes: number, patch: string[] | undefined): any {
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("checkout_path")
.returns("/checkout/path");
return exportedForTesting.getDiffRanges(
{
filename: "test.txt",
changes,
patch: patch?.join("\n"),
},
getRunnerLogger(true),
);
}
test("getDiffRanges: file unchanged", async (t) => {
const diffRanges = runGetDiffRanges(0, undefined);
t.deepEqual(diffRanges, []);
});
test("getDiffRanges: file diff too large", async (t) => {
const diffRanges = runGetDiffRanges(1000000, undefined);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 0,
endLine: 0,
},
]);
});
test("getDiffRanges: diff thunk with single addition range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,6 +50,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 54,
},
]);
});
test("getDiffRanges: diff thunk with single deletion range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,8 +50,6 @@",
" a",
" b",
" c",
"-1",
"-2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, []);
});
test("getDiffRanges: diff thunk with single update range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,7 @@",
" a",
" b",
" c",
"-1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 53,
},
]);
});
test("getDiffRanges: diff thunk with addition ranges", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,9 @@",
" a",
" b",
" c",
"+1",
" c",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 53,
},
{
path: "/checkout/path/test.txt",
startLine: 55,
endLine: 55,
},
]);
});
test("getDiffRanges: diff thunk with mixed ranges", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,7 @@",
" a",
" b",
" c",
"-1",
" d",
"-2",
"+3",
" e",
" f",
"+4",
"+5",
" g",
" h",
" i",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 54,
endLine: 54,
},
{
path: "/checkout/path/test.txt",
startLine: 57,
endLine: 58,
},
]);
});
test("getDiffRanges: multiple diff thunks", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,6 +50,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
"@@ -130,6 +150,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 54,
},
{
path: "/checkout/path/test.txt",
startLine: 153,
endLine: 154,
},
]);
});
test("getDiffRanges: no diff context lines", async (t) => {
const diffRanges = runGetDiffRanges(2, ["@@ -30 +50,2 @@", "+1", "+2"]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 50,
endLine: 51,
},
]);
});
test("getDiffRanges: malformed thunk header", async (t) => {
const diffRanges = runGetDiffRanges(2, ["@@ 30 +50,2 @@", "+1", "+2"]);
t.deepEqual(diffRanges, undefined);
});
test("resolveQuerySuiteAlias", (t) => {
// default query suite names should resolve to something language-specific ending in `.qls`.
for (const suite of defaultSuites) {

View File

@@ -3,16 +3,10 @@ import * as path from "path";
import { performance } from "perf_hooks";
import * as io from "@actions/io";
import * as del from "del";
import * as yaml from "js-yaml";
import {
getRequiredInput,
getTemporaryDirectory,
PullRequestBranches,
} from "./actions-util";
import { getTemporaryDirectory, PullRequestBranches } from "./actions-util";
import * as analyses from "./analyses";
import { getApiClient } from "./api-client";
import { setupCppAutobuild } from "./autobuild";
import { type CodeQL } from "./codeql";
import * as configUtils from "./config-utils";
@@ -21,13 +15,13 @@ import { addDiagnostic, makeDiagnostic } from "./diagnostics";
import {
DiffThunkRange,
writeDiffRangesJsonFile,
getPullRequestEditedDiffRanges,
} from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { FeatureEnablement, Feature } from "./feature-flags";
import { KnownLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { OverlayDatabaseMode } from "./overlay-database-utils";
import { getRepositoryNwoFromEnv } from "./repository";
import { DatabaseCreationTimings, EventReport } from "./status-report";
import { endTracingForCluster } from "./tracer-config";
import * as util from "./util";
@@ -44,89 +38,26 @@ export class CodeQLAnalysisError extends Error {
}
}
export interface QueriesStatusReport {
/**
* Time taken in ms to run queries for actions (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_actions_duration_ms?: number;
/**
* Time taken in ms to run queries for cpp (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_cpp_duration_ms?: number;
/**
* Time taken in ms to run queries for csharp (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_csharp_duration_ms?: number;
/**
* Time taken in ms to run queries for go (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_go_duration_ms?: number;
/**
* Time taken in ms to run queries for java (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_java_duration_ms?: number;
/**
* Time taken in ms to run queries for javascript (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_javascript_duration_ms?: number;
/**
* Time taken in ms to run queries for python (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_python_duration_ms?: number;
/**
* Time taken in ms to run queries for ruby (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_ruby_duration_ms?: number;
/** Time taken in ms to run queries for swift (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
analyze_builtin_queries_swift_duration_ms?: number;
type KnownLanguageKey = keyof typeof KnownLanguage;
/** Time taken in ms to interpret results for actions (or undefined if this language was not analyzed). */
interpret_results_actions_duration_ms?: number;
/** Time taken in ms to interpret results for cpp (or undefined if this language was not analyzed). */
interpret_results_cpp_duration_ms?: number;
/** Time taken in ms to interpret results for csharp (or undefined if this language was not analyzed). */
interpret_results_csharp_duration_ms?: number;
/** Time taken in ms to interpret results for go (or undefined if this language was not analyzed). */
interpret_results_go_duration_ms?: number;
/** Time taken in ms to interpret results for java (or undefined if this language was not analyzed). */
interpret_results_java_duration_ms?: number;
/** Time taken in ms to interpret results for javascript (or undefined if this language was not analyzed). */
interpret_results_javascript_duration_ms?: number;
/** Time taken in ms to interpret results for python (or undefined if this language was not analyzed). */
interpret_results_python_duration_ms?: number;
/** Time taken in ms to interpret results for ruby (or undefined if this language was not analyzed). */
interpret_results_ruby_duration_ms?: number;
/** Time taken in ms to interpret results for swift (or undefined if this language was not analyzed). */
interpret_results_swift_duration_ms?: number;
type RunQueriesDurationStatusReport = {
/**
* Time taken in ms to run queries for the language (or undefined if this language was not analyzed).
*
* The "builtin" designation is now outdated with the move to CLI config parsing: this is the time
* taken to run _all_ the queries.
*/
[L in KnownLanguageKey as `analyze_builtin_queries_${L}_duration_ms`]?: number;
};
type InterpretResultsDurationStatusReport = {
/** Time taken in ms to interpret results for the language (or undefined if this language was not analyzed). */
[L in KnownLanguageKey as `interpret_results_${L}_duration_ms`]?: number;
};
export interface QueriesStatusReport
extends RunQueriesDurationStatusReport,
InterpretResultsDurationStatusReport {
/**
* Whether the analysis is diff-informed (in the sense that the action generates a diff-range data
* extension for the analysis, regardless of whether the data extension is actually used by queries).
@@ -313,185 +244,6 @@ export async function setupDiffInformedQueryRun(
);
}
/**
* Return the file line ranges that were added or modified in the pull request.
*
* @param branches The base and head branches of the pull request.
* @param logger
* @returns An array of tuples, where each tuple contains the absolute path of a
* file, the start line and the end line (both 1-based and inclusive) of an
* added or modified range in that file. Returns `undefined` if the action was
* not triggered by a pull request or if there was an error.
*/
async function getPullRequestEditedDiffRanges(
branches: PullRequestBranches,
logger: Logger,
): Promise<DiffThunkRange[] | undefined> {
const fileDiffs = await getFileDiffsWithBasehead(branches, logger);
if (fileDiffs === undefined) {
return undefined;
}
if (fileDiffs.length >= 300) {
// The "compare two commits" API returns a maximum of 300 changed files. If
// we see that many changed files, it is possible that there could be more,
// with the rest being truncated. In this case, we should not attempt to
// compute the diff ranges, as the result would be incomplete.
logger.warning(
`Cannot retrieve the full diff because there are too many ` +
`(${fileDiffs.length}) changed files in the pull request.`,
);
return undefined;
}
const results: DiffThunkRange[] = [];
for (const filediff of fileDiffs) {
const diffRanges = getDiffRanges(filediff, logger);
if (diffRanges === undefined) {
return undefined;
}
results.push(...diffRanges);
}
return results;
}
/**
* This interface is an abbreviated version of the file diff object returned by
* the GitHub API.
*/
interface FileDiff {
filename: string;
changes: number;
// A patch may be absent if the file is binary, if the file diff is too large,
// or if the file is unchanged.
patch?: string | undefined;
}
async function getFileDiffsWithBasehead(
branches: PullRequestBranches,
logger: Logger,
): Promise<FileDiff[] | undefined> {
// Check CODE_SCANNING_REPOSITORY first. If it is empty or not set, fall back
// to GITHUB_REPOSITORY.
const repositoryNwo = getRepositoryNwoFromEnv(
"CODE_SCANNING_REPOSITORY",
"GITHUB_REPOSITORY",
);
const basehead = `${branches.base}...${branches.head}`;
try {
const response = await getApiClient().rest.repos.compareCommitsWithBasehead(
{
owner: repositoryNwo.owner,
repo: repositoryNwo.repo,
basehead,
per_page: 1,
},
);
logger.debug(
`Response from compareCommitsWithBasehead(${basehead}):` +
`\n${JSON.stringify(response, null, 2)}`,
);
return response.data.files;
} catch (error: any) {
if (error.status) {
logger.warning(`Error retrieving diff ${basehead}: ${error.message}`);
logger.debug(
`Error running compareCommitsWithBasehead(${basehead}):` +
`\nRequest: ${JSON.stringify(error.request, null, 2)}` +
`\nError Response: ${JSON.stringify(error.response, null, 2)}`,
);
return undefined;
} else {
throw error;
}
}
}
function getDiffRanges(
fileDiff: FileDiff,
logger: Logger,
): DiffThunkRange[] | undefined {
// Diff-informed queries expect the file path to be absolute. CodeQL always
// uses forward slashes as the path separator, so on Windows we need to
// replace any backslashes with forward slashes.
const filename = path
.join(getRequiredInput("checkout_path"), fileDiff.filename)
.replaceAll(path.sep, "/");
if (fileDiff.patch === undefined) {
if (fileDiff.changes === 0) {
// There are situations where a changed file legitimately has no diff.
// For example, the file may be a binary file, or that the file may have
// been renamed with no changes to its contents. In these cases, the
// file would be reported as having 0 changes, and we can return an empty
// array to indicate no diff range in this file.
return [];
}
// If a file is reported to have nonzero changes but no patch, that may be
// due to the file diff being too large. In this case, we should fall back
// to a special diff range that covers the entire file.
return [
{
path: filename,
startLine: 0,
endLine: 0,
},
];
}
// The 1-based file line number of the current line
let currentLine = 0;
// The 1-based file line number that starts the current range of added lines
let additionRangeStartLine: number | undefined = undefined;
const diffRanges: DiffThunkRange[] = [];
const diffLines = fileDiff.patch.split("\n");
// Adding a fake context line at the end ensures that the following loop will
// always terminate the last range of added lines.
diffLines.push(" ");
for (const diffLine of diffLines) {
if (diffLine.startsWith("-")) {
// Ignore deletions completely -- we do not even want to consider them when
// calculating consecutive ranges of added lines.
continue;
}
if (diffLine.startsWith("+")) {
if (additionRangeStartLine === undefined) {
additionRangeStartLine = currentLine;
}
currentLine++;
continue;
}
if (additionRangeStartLine !== undefined) {
// Any line that does not start with a "+" or "-" terminates the current
// range of added lines.
diffRanges.push({
path: filename,
startLine: additionRangeStartLine,
endLine: currentLine - 1,
});
additionRangeStartLine = undefined;
}
if (diffLine.startsWith("@@ ")) {
// A new hunk header line resets the current line number.
const match = diffLine.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (match === null) {
logger.warning(
`Cannot parse diff hunk header for ${fileDiff.filename}: ${diffLine}`,
);
return undefined;
}
currentLine = parseInt(match[1], 10);
continue;
}
if (diffLine.startsWith(" ")) {
// An unchanged context line advances the current line number.
currentLine++;
continue;
}
}
return diffRanges;
}
/**
* Create an extension pack in the temporary directory that contains the file
* line ranges that were added or modified in the pull request.
@@ -621,7 +373,6 @@ export function addSarifExtension(
export async function runQueries(
sarifFolder: string,
memoryFlag: string,
addSnippetsFlag: string,
threadsFlag: string,
diffRangePackDir: string | undefined,
automationDetailsId: string | undefined,
@@ -811,7 +562,6 @@ export async function runQueries(
databasePath,
queries,
sarifFile,
addSnippetsFlag,
threadsFlag,
enableDebugLogging ? "-vv" : "-v",
sarifRunPropertyFlag,
@@ -855,7 +605,7 @@ export async function runFinalize(
logger: Logger,
): Promise<DatabaseCreationTimings> {
try {
await del.deleteAsync(outputDir, { force: true });
await fs.promises.rm(outputDir, { force: true, recursive: true });
} catch (error: any) {
if (error?.code !== "ENOENT") {
throw error;
@@ -922,7 +672,3 @@ export async function warnIfGoInstalledAfterInit(
}
}
}
export const exportedForTesting = {
getDiffRanges,
};

View File

@@ -169,4 +169,32 @@ test("wrapApiConfigurationError correctly wraps specific configuration errors",
res,
new util.ConfigurationError("Resource not accessible by integration"),
);
// Enablement errors.
const enablementErrorMessages = [
"Code Security must be enabled for this repository to use code scanning",
"Advanced Security must be enabled for this repository to use code scanning",
"Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.",
];
const transforms = [
(msg: string) => msg,
(msg: string) => msg.toLowerCase(),
(msg: string) => msg.toLocaleUpperCase(),
];
for (const enablementErrorMessage of enablementErrorMessages) {
for (const transform of transforms) {
const enablementError = new util.HTTPError(
transform(enablementErrorMessage),
403,
);
res = api.wrapApiConfigurationError(enablementError);
t.deepEqual(
res,
new util.ConfigurationError(
api.getFeatureEnablementError(enablementError.message),
),
);
}
}
});

View File

@@ -1,18 +1,17 @@
import * as core from "@actions/core";
import * as githubUtils from "@actions/github/lib/utils";
import * as retry from "@octokit/plugin-retry";
import consoleLogLevel from "console-log-level";
import { getActionVersion, getRequiredInput } from "./actions-util";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
import {
asHTTPError,
ConfigurationError,
getRequiredEnvParam,
GITHUB_DOTCOM_URL,
GitHubVariant,
GitHubVersion,
isHTTPError,
parseGitHubUrl,
parseMatrixInput,
} from "./util";
@@ -50,7 +49,12 @@ function createApiClientWithDetails(
githubUtils.getOctokitOptions(auth, {
baseUrl: apiDetails.apiURL,
userAgent: `CodeQL-Action/${getActionVersion()}`,
log: consoleLogLevel({ level: "debug" }),
log: {
debug: core.debug,
info: core.info,
warn: core.warning,
error: core.error,
},
}),
);
}
@@ -279,23 +283,49 @@ export async function getRepositoryProperties(repositoryNwo: RepositoryNwo) {
});
}
function isEnablementError(msg: string) {
return [
/Code Security must be enabled/i,
/Advanced Security must be enabled/i,
/Code Scanning is not enabled/i,
].some((pattern) => pattern.test(msg));
}
// TODO: Move to `error-messages.ts` after refactoring import order to avoid cycle
// since `error-messages.ts` currently depends on this file.
export function getFeatureEnablementError(message: string): string {
return `Please verify that the necessary features are enabled: ${message}`;
}
export function wrapApiConfigurationError(e: unknown) {
if (isHTTPError(e)) {
const httpError = asHTTPError(e);
if (httpError !== undefined) {
if (
e.message.includes("API rate limit exceeded for installation") ||
e.message.includes("commit not found") ||
e.message.includes("Resource not accessible by integration") ||
/ref .* not found in this repository/.test(e.message)
[
/API rate limit exceeded/,
/commit not found/,
/Resource not accessible by integration/,
/ref .* not found in this repository/,
].some((pattern) => pattern.test(httpError.message))
) {
return new ConfigurationError(e.message);
} else if (
e.message.includes("Bad credentials") ||
e.message.includes("Not Found")
return new ConfigurationError(httpError.message);
}
if (
httpError.message.includes("Bad credentials") ||
httpError.message.includes("Not Found")
) {
return new ConfigurationError(
"Please check that your token is valid and has the required permissions: contents: read, security-events: write",
);
}
if (httpError.status === 403 && isEnablementError(httpError.message)) {
return new ConfigurationError(
getFeatureEnablementError(httpError.message),
);
}
if (httpError.status === 429) {
return new ConfigurationError("API rate limit exceeded");
}
}
return e;
}

View File

@@ -1,3 +1,5 @@
import * as crypto from "crypto";
import * as core from "@actions/core";
import { getOptionalInput, isDefaultSetup } from "./actions-util";
@@ -71,6 +73,33 @@ export function getCachingKind(input: string | undefined): CachingKind {
}
}
// The length to which `createCacheKeyHash` truncates hash strings.
export const cacheKeyHashLength = 16;
/**
* Creates a SHA-256 hash of the cache key components to ensure uniqueness
* while keeping the cache key length manageable.
*
* @param components Object containing all components that should influence cache key uniqueness
* @returns A short SHA-256 hash (first 16 characters) of the components
*/
export function createCacheKeyHash(components: Record<string, any>): string {
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
//
// "Properties are visited using the same algorithm as Object.keys(), which
// has a well-defined order and is stable across implementations. For example,
// JSON.stringify on the same object will always produce the same string, and
// JSON.parse(JSON.stringify(obj)) would produce an object with the same key
// ordering as the original (assuming the object is completely
// JSON-serializable)."
const componentsJson = JSON.stringify(components);
return crypto
.createHash("sha256")
.update(componentsJson)
.digest("hex")
.substring(0, cacheKeyHashLength);
}
/** Determines whether dependency caching is enabled. */
export function getDependencyCachingEnabled(): CachingKind {
// If the workflow specified something always respect that

View File

@@ -5,7 +5,6 @@ import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import * as toolcache from "@actions/tool-cache";
import test, { ExecutionContext } from "ava";
import * as del from "del";
import * as yaml from "js-yaml";
import nock from "nock";
import * as sinon from "sinon";
@@ -36,7 +35,6 @@ import {
createTestConfig,
} from "./testing-utils";
import { ToolsDownloadStatusReport } from "./tools-download";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
import { initializeEnvironment } from "./util";
@@ -558,7 +556,7 @@ const injectedConfigMacro = test.macro({
const augmentedConfig = yaml.load(fs.readFileSync(configFile, "utf8"));
t.deepEqual(augmentedConfig, expectedConfig);
await del.deleteAsync(configFile, { force: true });
await fs.promises.rm(configFile, { force: true });
});
},
@@ -870,84 +868,6 @@ test("does not pass a qlconfig to the CLI when it is undefined", async (t: Execu
});
});
const NEW_ANALYSIS_SUMMARY_TEST_CASES = [
{
codeqlVersion: makeVersionInfo("2.15.0", {
[ToolsFeature.AnalysisSummaryV2IsDefault]: true,
}),
githubVersion: {
type: util.GitHubVariant.DOTCOM,
},
flagPassed: false,
negativeFlagPassed: false,
},
{
codeqlVersion: makeVersionInfo("2.15.0"),
githubVersion: {
type: util.GitHubVariant.DOTCOM,
},
flagPassed: true,
negativeFlagPassed: false,
},
{
codeqlVersion: makeVersionInfo("2.15.0"),
githubVersion: {
type: util.GitHubVariant.GHES,
version: "3.10.0",
},
flagPassed: true,
negativeFlagPassed: false,
},
];
for (const {
codeqlVersion,
flagPassed,
githubVersion,
negativeFlagPassed,
} of NEW_ANALYSIS_SUMMARY_TEST_CASES) {
test(`database interpret-results passes ${
flagPassed
? "--new-analysis-summary"
: negativeFlagPassed
? "--no-new-analysis-summary"
: "nothing"
} for CodeQL version ${JSON.stringify(codeqlVersion)} and ${
util.GitHubVariant[githubVersion.type]
} ${githubVersion.version ? ` ${githubVersion.version}` : ""}`, async (t) => {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves(codeqlVersion);
// io throws because of the test CodeQL object.
sinon.stub(io, "which").resolves("");
await codeqlObject.databaseInterpretResults(
"",
[],
"",
"",
"",
"-v",
undefined,
"",
Object.assign({}, stubConfig, { gitHubVersion: githubVersion }),
createFeatures([]),
);
const actualArgs = runnerConstructorStub.firstCall.args[1] as string[];
t.is(
actualArgs.includes("--new-analysis-summary"),
flagPassed,
`--new-analysis-summary should${flagPassed ? "" : "n't"} be passed`,
);
t.is(
actualArgs.includes("--no-new-analysis-summary"),
negativeFlagPassed,
`--no-new-analysis-summary should${
negativeFlagPassed ? "" : "n't"
} be passed`,
);
});
}
test("runTool summarizes several fatal errors", async (t) => {
const heapError =
"A fatal error occurred: Evaluator heap must be at least 384.00 MiB";
@@ -1125,7 +1045,7 @@ test("Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OP
);
t.truthy(configArg, "Should have injected a codescanning config");
const configFile = configArg!.split("=")[1];
await del.deleteAsync(configFile, { force: true });
await fs.promises.rm(configFile, { force: true });
});
export function stubToolRunnerConstructor(

View File

@@ -3,7 +3,6 @@ import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import { RequestError } from "@octokit/request-error";
import * as yaml from "js-yaml";
import {
@@ -168,7 +167,6 @@ export interface CodeQL {
databasePath: string,
querySuitePaths: string[] | undefined,
sarifFile: string,
addSnippetsFlag: string,
threadsFlag: string,
verbosityFlag: string | undefined,
sarifRunPropertyFlag: string | undefined,
@@ -268,7 +266,7 @@ let cachedCodeQL: CodeQL | undefined = undefined;
* The version flags below can be used to conditionally enable certain features
* on versions newer than this.
*/
const CODEQL_MINIMUM_VERSION = "2.16.6";
const CODEQL_MINIMUM_VERSION = "2.17.6";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
@@ -371,11 +369,11 @@ export async function setupCodeQL(
toolsVersion,
zstdAvailability,
};
} catch (e) {
} catch (rawError) {
const e = api.wrapApiConfigurationError(rawError);
const ErrorClass =
e instanceof util.ConfigurationError ||
(e instanceof Error && e.message.includes("ENOSPC")) || // out of disk space
(e instanceof RequestError && e.status === 429) // rate limited
(e instanceof Error && e.message.includes("ENOSPC")) // out of disk space
? util.ConfigurationError
: Error;
@@ -818,7 +816,6 @@ export async function getCodeQLForCmd(
databasePath: string,
querySuitePaths: string[] | undefined,
sarifFile: string,
addSnippetsFlag: string,
threadsFlag: string,
verbosityFlag: string,
sarifRunPropertyFlag: string | undefined,
@@ -837,7 +834,6 @@ export async function getCodeQLForCmd(
"--format=sarif-latest",
verbosityFlag,
`--output=${sarifFile}`,
addSnippetsFlag,
"--print-diagnostics-summary",
"--print-metrics-summary",
"--sarif-add-baseline-file-info",
@@ -861,14 +857,6 @@ export async function getCodeQLForCmd(
} else {
codeqlArgs.push("--no-sarif-include-diagnostics");
}
if (
!isSupportedToolsFeature(
await this.getVersion(),
ToolsFeature.AnalysisSummaryV2IsDefault,
)
) {
codeqlArgs.push("--new-analysis-summary");
}
codeqlArgs.push(databasePath);
if (querySuitePaths) {
codeqlArgs.push(...querySuitePaths);
@@ -1083,8 +1071,11 @@ export async function getCodeQLForCmd(
/**
* Gets the options for `path` of `options` as an array of extra option strings.
*
* @param ignoringOptions Options that should be ignored, for example because they have already
* been passed and it is an error to pass them more than once.
* @param paths The CLI command components to get extra options for.
* @param args Additional arguments for this function.
* @param args.ignoringOptions
* Options that should be ignored, for example because they have already
* been passed and it is an error to pass them more than once.
*/
function getExtraOptionsFromEnv(
paths: string[],
@@ -1166,8 +1157,9 @@ async function runCli(
/**
* Writes the code scanning configuration that is to be used by the CLI.
*
* @param codeql The CodeQL object to use.
* @param config The CodeQL Action state to use.
* @param config The CodeQL Action state to write.
* @param logger The logger to use.
*
* @returns The path to the generated user configuration file.
*/
async function writeCodeScanningConfigFile(

View File

@@ -873,71 +873,62 @@ const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
expectedLanguages: ["javascript"],
},
].forEach((args) => {
for (const resolveSupportedLanguagesUsingCli of [true, false]) {
test(`getLanguages${resolveSupportedLanguagesUsingCli ? " (supported languages via CLI)" : ""}: ${args.name}`, async (t) => {
const features = createFeatures(
resolveSupportedLanguagesUsingCli
? [Feature.ResolveSupportedLanguagesUsingCli]
: [],
);
const mockRequest = mockLanguagesInRepo(args.languagesInRepository);
const stubExtractorEntry = {
extractor_root: "",
};
const codeQL = createStubCodeQL({
betterResolveLanguages: (options) =>
Promise.resolve({
aliases: {
"c#": KnownLanguage.csharp,
c: KnownLanguage.cpp,
kotlin: KnownLanguage.java,
typescript: KnownLanguage.javascript,
},
extractors: {
cpp: [stubExtractorEntry],
csharp: [stubExtractorEntry],
java: [stubExtractorEntry],
javascript: [stubExtractorEntry],
python: [stubExtractorEntry],
...(options?.filterToLanguagesWithQueries
? {}
: {
html: [stubExtractorEntry],
}),
},
}),
});
if (args.expectedLanguages) {
// happy path
const actualLanguages = await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
features,
mockLogger,
);
t.deepEqual(actualLanguages.sort(), args.expectedLanguages.sort());
} else {
// there is an error
await t.throwsAsync(
async () =>
await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
features,
mockLogger,
),
{ message: args.expectedError },
);
}
t.deepEqual(mockRequest.called, args.expectedApiCall);
test(`getLanguages: ${args.name}`, async (t) => {
const mockRequest = mockLanguagesInRepo(args.languagesInRepository);
const stubExtractorEntry = {
extractor_root: "",
};
const codeQL = createStubCodeQL({
betterResolveLanguages: (options) =>
Promise.resolve({
aliases: {
"c#": KnownLanguage.csharp,
c: KnownLanguage.cpp,
kotlin: KnownLanguage.java,
typescript: KnownLanguage.javascript,
},
extractors: {
cpp: [stubExtractorEntry],
csharp: [stubExtractorEntry],
java: [stubExtractorEntry],
javascript: [stubExtractorEntry],
python: [stubExtractorEntry],
...(options?.filterToLanguagesWithQueries
? {}
: {
html: [stubExtractorEntry],
}),
},
}),
});
}
if (args.expectedLanguages) {
// happy path
const actualLanguages = await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
mockLogger,
);
t.deepEqual(actualLanguages.sort(), args.expectedLanguages.sort());
} else {
// there is an error
await t.throwsAsync(
async () =>
await configUtils.getLanguages(
codeQL,
args.languagesInput,
mockRepositoryNwo,
".",
mockLogger,
),
{ message: args.expectedError },
);
}
t.deepEqual(mockRequest.called, args.expectedApiCall);
});
});
for (const { displayName, language, feature } of [

View File

@@ -34,6 +34,7 @@ import {
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { RepositoryNwo } from "./repository";
import { ToolsFeature } from "./tools-features";
import { downloadTrapCaches } from "./trap-caching";
import {
GitHubVersion,
@@ -177,12 +178,10 @@ export interface Config {
export async function getSupportedLanguageMap(
codeql: CodeQL,
features: FeatureEnablement,
logger: Logger,
): Promise<Record<string, string>> {
const resolveSupportedLanguagesUsingCli = await features.getValue(
Feature.ResolveSupportedLanguagesUsingCli,
codeql,
const resolveSupportedLanguagesUsingCli = await codeql.supportsFeature(
ToolsFeature.BuiltinExtractorsSpecifyDefaultQueries,
);
const resolveResult = await codeql.betterResolveLanguages({
filterToLanguagesWithQueries: resolveSupportedLanguagesUsingCli,
@@ -283,7 +282,6 @@ export async function getLanguages(
languagesInput: string | undefined,
repository: RepositoryNwo,
sourceRoot: string,
features: FeatureEnablement,
logger: Logger,
): Promise<Language[]> {
// Obtain languages without filtering them.
@@ -294,7 +292,7 @@ export async function getLanguages(
logger,
);
const languageMap = await getSupportedLanguageMap(codeql, features, logger);
const languageMap = await getSupportedLanguageMap(codeql, logger);
const languagesSet = new Set<Language>();
const unknownLanguages: string[] = [];
@@ -431,7 +429,6 @@ export async function initActionState(
languagesInput,
repository,
sourceRoot,
features,
logger,
);
@@ -943,7 +940,7 @@ async function getRemoteConfig(
);
const pieces = format.exec(configFile);
// 5 = 4 groups + the whole expression
if (pieces === null || pieces.groups === undefined || pieces.length < 5) {
if (pieces?.groups === undefined || pieces.length < 5) {
throw new ConfigurationError(
errorMessages.getConfigFileRepoFormatInvalidMessage(configFile),
);
@@ -1036,7 +1033,6 @@ export async function getConfig(
* pack.
*
* @param registriesInput The value of the `registries` input.
* @param codeQL a codeQL object, used only for checking the version of CodeQL.
* @param tempDir a temporary directory to store the generated qlconfig.yml file.
* @param logger a logger object.
* @returns The path to the generated `qlconfig.yml` file and the auth tokens to

View File

@@ -4,7 +4,6 @@ import * as yaml from "js-yaml";
import * as jsonschema from "jsonschema";
import * as semver from "semver";
import type { UserConfig as DbConfig, QuerySpec } from "../db-config-schema";
import * as errorMessages from "../error-messages";
import {
RepositoryProperties,
@@ -14,8 +13,6 @@ import { Language } from "../languages";
import { Logger } from "../logging";
import { cloneObject, ConfigurationError, prettyPrintPack } from "../util";
export type { QuerySpec } from "../db-config-schema";
export interface ExcludeQueryFilter {
exclude: Record<string, string[] | string>;
}
@@ -26,14 +23,30 @@ export interface IncludeQueryFilter {
export type QueryFilter = ExcludeQueryFilter | IncludeQueryFilter;
export interface QuerySpec {
name?: string;
uses: string;
}
/**
* Format of the config file supplied by the user.
*/
export type UserConfig = DbConfig & {
export interface UserConfig {
name?: string;
"disable-default-queries"?: boolean;
queries?: QuerySpec[];
"paths-ignore"?: string[];
paths?: string[];
// If this is a multi-language analysis, then the packages must be split by
// language. If this is a single language analysis, then no split by
// language is necessary.
packs?: Record<string, string[]> | string[];
// Set of query filters to include and exclude extra queries based on
// codeql query suite `include` and `exclude` properties
"query-filters"?: QueryFilter[];
};
}
/**
* Represents additional configuration data from a source other than
@@ -147,7 +160,6 @@ const PACK_IDENTIFIER_PATTERN = (function () {
* Version and path are optional.
*
* @param packStr the package specification to verify.
* @param configFile Config file to use for error reporting
*/
export function parsePacksSpecification(packStr: string): Pack {
if (typeof packStr !== "string") {
@@ -367,10 +379,7 @@ function combineQueries(
const result: QuerySpec[] = [];
// Query settings obtained from the repository properties have the highest precedence.
if (
augmentationProperties.repoPropertyQueries &&
augmentationProperties.repoPropertyQueries.input
) {
if (augmentationProperties.repoPropertyQueries?.input) {
logger.info(
`Found query configuration in the repository properties (${RepositoryPropertyName.EXTRA_QUERIES}): ` +
`${augmentationProperties.repoPropertyQueries.input.map((q) => q.uses).join(", ")}`,
@@ -483,7 +492,7 @@ export function parseUserConfig(
try {
const schema =
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("../../schemas/db-config-schema.json") as jsonschema.Schema;
require("../../src/db-config-schema.json") as jsonschema.Schema;
const doc = yaml.load(contents);

View File

@@ -5,6 +5,7 @@ import test from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import { GitHubApiDetails } from "./api-client";
import * as apiClient from "./api-client";
import { createStubCodeQL } from "./codeql";
@@ -108,6 +109,39 @@ test("Abort database upload if 'upload-database' input set to false", async (t)
});
});
test("Abort database upload if 'analysis-kinds: code-scanning' is not enabled", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("upload-database")
.returns("true");
sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true);
await mockHttpRequests(201);
const loggedMessages = [];
await uploadDatabases(
testRepoName,
getCodeQL(),
{
...getTestConfig(tmpDir),
analysisKinds: [AnalysisKind.CodeQuality],
},
testApiDetails,
getRecordingLogger(loggedMessages),
);
t.assert(
loggedMessages.find(
(v: LoggedMessage) =>
v.type === "debug" &&
v.message ===
"Not uploading database because 'analysis-kinds: code-scanning' is not enabled.",
) !== undefined,
);
});
});
test("Abort database upload if running against GHES", async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);

View File

@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as actionsUtil from "./actions-util";
import { AnalysisKind } from "./analyses";
import { getApiClient, GitHubApiDetails } from "./api-client";
import { type CodeQL } from "./codeql";
import { Config } from "./config-utils";
@@ -22,6 +23,13 @@ export async function uploadDatabases(
return;
}
if (!config.analysisKinds.includes(AnalysisKind.CodeScanning)) {
logger.debug(
`Not uploading database because 'analysis-kinds: ${AnalysisKind.CodeScanning}' is not enabled.`,
);
return;
}
if (util.isInTestMode()) {
logger.debug("In test mode. Skipping database upload.");
return;

View File

@@ -1,53 +0,0 @@
/* This file was automatically generated by `npm run generate:schemas`. Do not edit by hand. */
/**
* Format of the config file supplied by the user for CodeQL analysis
*/
export interface UserConfig {
/**
* Name of the configuration
*/
name?: string;
/**
* Whether to disable default queries
*/
"disable-default-queries"?: boolean;
/**
* List of additional queries to run
*/
queries?: QuerySpec[];
/**
* Paths to ignore during analysis
*/
"paths-ignore"?: string[];
/**
* Paths to include in analysis
*/
paths?: string[];
/**
* Query packs to include. Can be a simple array for single-language analysis or an object with language-specific arrays for multi-language analysis
*/
packs?:
| string[]
| {
[k: string]: string[];
};
/**
* Set of query filters to include and exclude extra queries based on CodeQL query suite include and exclude properties
*/
"query-filters"?: unknown[];
[k: string]: unknown;
}
/**
* Detailed query specification object
*/
export interface QuerySpec {
/**
* Optional name for the query
*/
name?: string;
/**
* The query or query suite to use
*/
uses: string;
}

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "UserConfig",
"title": "CodeQL Database Configuration",
"description": "Format of the config file supplied by the user for CodeQL analysis",
"type": "object",
"properties": {

View File

@@ -5,7 +5,6 @@ import * as artifact from "@actions/artifact";
import * as artifactLegacy from "@actions/artifact-legacy";
import * as core from "@actions/core";
import archiver from "archiver";
import * as del from "del";
import { getOptionalInput, getTemporaryDirectory } from "./actions-util";
import { dbIsFinalized } from "./analyze";
@@ -345,7 +344,7 @@ async function createPartialDatabaseBundle(
);
// See `bundleDb` for explanation behind deleting existing db bundle.
if (fs.existsSync(databaseBundlePath)) {
await del.deleteAsync(databaseBundlePath, { force: true });
await fs.promises.rm(databaseBundlePath, { force: true });
}
const output = fs.createWriteStream(databaseBundlePath);
const zip = archiver("zip");

View File

@@ -0,0 +1,389 @@
import * as fs from "fs";
import path from "path";
import * as actionsCache from "@actions/cache";
import * as glob from "@actions/glob";
import test from "ava";
import * as sinon from "sinon";
import { cacheKeyHashLength } from "./caching-utils";
import { createStubCodeQL } from "./codeql";
import {
CacheConfig,
checkHashPatterns,
getCsharpHashPatterns,
getFeaturePrefix,
makePatternCheck,
internal,
CSHARP_BASE_PATTERNS,
CSHARP_EXTRA_PATTERNS,
downloadDependencyCaches,
CacheHitKind,
cacheKey,
} from "./dependency-caching";
import { Feature } from "./feature-flags";
import { KnownLanguage } from "./languages";
import {
setupTests,
createFeatures,
getRecordingLogger,
checkExpectedLogMessages,
LoggedMessage,
} from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
function makeAbsolutePatterns(tmpDir: string, patterns: string[]): string[] {
return patterns.map((pattern) => path.join(tmpDir, pattern));
}
test("makePatternCheck - returns undefined if no patterns match", async (t) => {
await withTmpDir(async (tmpDir) => {
fs.writeFileSync(path.join(tmpDir, "test.java"), "");
const result = await makePatternCheck(
makeAbsolutePatterns(tmpDir, ["**/*.cs"]),
);
t.is(result, undefined);
});
});
test("makePatternCheck - returns all patterns if any pattern matches", async (t) => {
await withTmpDir(async (tmpDir) => {
fs.writeFileSync(path.join(tmpDir, "test.java"), "");
const patterns = makeAbsolutePatterns(tmpDir, ["**/*.cs", "**/*.java"]);
const result = await makePatternCheck(patterns);
t.deepEqual(result, patterns);
});
});
test("getCsharpHashPatterns - returns base patterns if any pattern matches", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([]);
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).rejects();
await t.notThrowsAsync(async () => {
const result = await getCsharpHashPatterns(codeql, features);
t.deepEqual(result, CSHARP_BASE_PATTERNS);
});
});
test("getCsharpHashPatterns - returns base patterns if any base pattern matches and CsharpNewCacheKey is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.CsharpNewCacheKey]);
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub
.withArgs(CSHARP_EXTRA_PATTERNS)
.resolves(CSHARP_EXTRA_PATTERNS);
await t.notThrowsAsync(async () => {
const result = await getCsharpHashPatterns(codeql, features);
t.deepEqual(result, CSHARP_BASE_PATTERNS);
});
});
test("getCsharpHashPatterns - returns extra patterns if any extra pattern matches and CsharpNewCacheKey is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.CsharpNewCacheKey]);
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined);
makePatternCheckStub
.withArgs(CSHARP_EXTRA_PATTERNS)
.resolves(CSHARP_EXTRA_PATTERNS);
await t.notThrowsAsync(async () => {
const result = await getCsharpHashPatterns(codeql, features);
t.deepEqual(result, CSHARP_EXTRA_PATTERNS);
});
});
test("getCsharpHashPatterns - returns undefined if neither base nor extra patterns match", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.CsharpNewCacheKey]);
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub.withArgs(CSHARP_BASE_PATTERNS).resolves(undefined);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);
await t.notThrowsAsync(async () => {
const result = await getCsharpHashPatterns(codeql, features);
t.deepEqual(result, undefined);
});
});
test("checkHashPatterns - logs when no patterns match", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([]);
const messages: LoggedMessage[] = [];
const config: CacheConfig = {
getDependencyPaths: () => [],
getHashPatterns: async () => undefined,
};
const result = await checkHashPatterns(
codeql,
features,
KnownLanguage.csharp,
config,
"download",
getRecordingLogger(messages),
);
t.is(result, undefined);
checkExpectedLogMessages(t, messages, [
"Skipping download of dependency cache",
]);
});
test("checkHashPatterns - returns patterns when patterns match", async (t) => {
await withTmpDir(async (tmpDir) => {
const codeql = createStubCodeQL({});
const features = createFeatures([]);
const messages: LoggedMessage[] = [];
const patterns = makeAbsolutePatterns(tmpDir, ["**/*.cs", "**/*.java"]);
fs.writeFileSync(path.join(tmpDir, "test.java"), "");
const config: CacheConfig = {
getDependencyPaths: () => [],
getHashPatterns: async () => makePatternCheck(patterns),
};
const result = await checkHashPatterns(
codeql,
features,
KnownLanguage.csharp,
config,
"upload",
getRecordingLogger(messages),
);
t.deepEqual(result, patterns);
t.deepEqual(messages, []);
});
});
type RestoreCacheFunc = (
paths: string[],
primaryKey: string,
restoreKeys: string[] | undefined,
) => Promise<string | undefined>;
/**
* Constructs a function that `actionsCache.restoreCache` can be stubbed with.
*
* @param mockCacheKeys The keys of caches that we want to exist in the Actions cache.
*
* @returns Returns a function that `actionsCache.restoreCache` can be stubbed with.
*/
function makeMockCacheCheck(mockCacheKeys: string[]): RestoreCacheFunc {
return async (
_paths: string[],
primaryKey: string,
restoreKeys: string[] | undefined,
) => {
// The behaviour here mirrors what the real `restoreCache` would do:
// - Starting with the primary restore key, check all caches for a match:
// even for the primary restore key, this only has to be a prefix match.
// - If the primary restore key doesn't prefix-match any cache, then proceed
// in the same way for each restore key in turn.
for (const restoreKey of [primaryKey, ...(restoreKeys || [])]) {
for (const mockCacheKey of mockCacheKeys) {
if (mockCacheKey.startsWith(restoreKey)) {
return mockCacheKey;
}
}
}
// Only if no restore key matches any cache key prefix, there is no matching
// cache and we return `undefined`.
return undefined;
};
}
test("downloadDependencyCaches - does not restore caches with feature keys if no features are enabled", async (t) => {
process.env["RUNNER_OS"] = "Linux";
const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
sinon.stub(glob, "hashFiles").resolves("abcdef");
const keyWithFeature = await cacheKey(
codeql,
createFeatures([Feature.CsharpNewCacheKey]),
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);
const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);
const results = await downloadDependencyCaches(
codeql,
createFeatures([]),
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Miss);
t.assert(restoreCacheStub.calledOnce);
});
test("downloadDependencyCaches - restores caches with feature keys if features are enabled", async (t) => {
process.env["RUNNER_OS"] = "Linux";
const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
const features = createFeatures([Feature.CsharpNewCacheKey]);
sinon.stub(glob, "hashFiles").resolves("abcdef");
const keyWithFeature = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);
const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);
const results = await downloadDependencyCaches(
codeql,
features,
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Exact);
t.assert(restoreCacheStub.calledOnce);
});
test("downloadDependencyCaches - restores caches with feature keys if features are enabled for partial matches", async (t) => {
process.env["RUNNER_OS"] = "Linux";
const codeql = createStubCodeQL({});
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
const features = createFeatures([Feature.CsharpNewCacheKey]);
const hashFilesStub = sinon.stub(glob, "hashFiles");
hashFilesStub.onFirstCall().resolves("abcdef");
hashFilesStub.onSecondCall().resolves("123456");
const keyWithFeature = await cacheKey(
codeql,
features,
KnownLanguage.csharp,
// Patterns don't matter here because we have stubbed `hashFiles` to always return a specific hash above.
[],
);
const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.callsFake(makeMockCacheCheck([keyWithFeature]));
const makePatternCheckStub = sinon.stub(internal, "makePatternCheck");
makePatternCheckStub
.withArgs(CSHARP_BASE_PATTERNS)
.resolves(CSHARP_BASE_PATTERNS);
makePatternCheckStub.withArgs(CSHARP_EXTRA_PATTERNS).resolves(undefined);
const results = await downloadDependencyCaches(
codeql,
features,
[KnownLanguage.csharp],
logger,
);
t.is(results.length, 1);
t.is(results[0].language, KnownLanguage.csharp);
t.is(results[0].hit_kind, CacheHitKind.Partial);
t.assert(restoreCacheStub.calledOnce);
});
test("getFeaturePrefix - returns empty string if no features are enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([]);
for (const knownLanguage of Object.values(KnownLanguage)) {
const result = await getFeaturePrefix(codeql, features, knownLanguage);
t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`);
}
});
test("getFeaturePrefix - Java - returns 'minify-' if JavaMinimizeDependencyJars is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.JavaMinimizeDependencyJars]);
const result = await getFeaturePrefix(codeql, features, KnownLanguage.java);
t.deepEqual(result, "minify-");
});
test("getFeaturePrefix - non-Java - returns '' if JavaMinimizeDependencyJars is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.JavaMinimizeDependencyJars]);
for (const knownLanguage of Object.values(KnownLanguage)) {
// Skip Java since we expect a result for it, which is tested in the previous test.
if (knownLanguage === KnownLanguage.java) {
continue;
}
const result = await getFeaturePrefix(codeql, features, knownLanguage);
t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`);
}
});
test("getFeaturePrefix - C# - returns prefix if CsharpNewCacheKey is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.CsharpNewCacheKey]);
const result = await getFeaturePrefix(codeql, features, KnownLanguage.csharp);
t.notDeepEqual(result, "");
t.assert(result.endsWith("-"));
// Check the length of the prefix, which should correspond to `cacheKeyHashLength` + 1 for the trailing `-`.
t.is(result.length, cacheKeyHashLength + 1);
});
test("getFeaturePrefix - non-C# - returns '' if CsharpNewCacheKey is enabled", async (t) => {
const codeql = createStubCodeQL({});
const features = createFeatures([Feature.CsharpNewCacheKey]);
for (const knownLanguage of Object.values(KnownLanguage)) {
// Skip C# since we expect a result for it, which is tested in the previous test.
if (knownLanguage === KnownLanguage.csharp) {
continue;
}
const result = await getFeaturePrefix(codeql, features, knownLanguage);
t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`);
}
});

View File

@@ -6,9 +6,11 @@ import * as glob from "@actions/glob";
import { getTemporaryDirectory } from "./actions-util";
import { listActionsCaches } from "./api-client";
import { getTotalCacheSize } from "./caching-utils";
import { createCacheKeyHash, getTotalCacheSize } from "./caching-utils";
import { CodeQL } from "./codeql";
import { Config } from "./config-utils";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
import { KnownLanguage, Language } from "./languages";
import { Logger } from "./logging";
import { getErrorMessage, getRequiredEnvParam } from "./util";
@@ -16,15 +18,21 @@ import { getErrorMessage, getRequiredEnvParam } from "./util";
/**
* Caching configuration for a particular language.
*/
interface CacheConfig {
/** The paths of directories on the runner that should be included in the cache. */
paths: string[];
export interface CacheConfig {
/** Gets the paths of directories on the runner that should be included in the cache. */
getDependencyPaths: () => string[];
/**
* Patterns for the paths of files whose contents affect which dependencies are used
* by a project. We find all files which match these patterns, calculate a hash for
* their contents, and use that hash as part of the cache key.
* Gets an array of glob patterns for the paths of files whose contents affect which dependencies are used
* by a project. This function also checks whether there are any matching files and returns
* `undefined` if no files match.
*
* The glob patterns are intended to be used for cache keys, where we find all files which match these
* patterns, calculate a hash for their contents, and use that hash as part of the cache key.
*/
hash: string[];
getHashPatterns: (
codeql: CodeQL,
features: FeatureEnablement,
) => Promise<string[] | undefined>;
}
const CODEQL_DEPENDENCY_CACHE_PREFIX = "codeql-dependencies";
@@ -39,21 +47,105 @@ export function getJavaTempDependencyDir(): string {
return join(getTemporaryDirectory(), "codeql_java", "repository");
}
/**
* Returns an array of paths of directories on the runner that should be included in a dependency cache
* for a Java analysis. It is important that this is a function, because we call `getTemporaryDirectory`
* which would otherwise fail in tests if we haven't had a chance to initialise `RUNNER_TEMP`.
*
* @returns The paths of directories on the runner that should be included in a dependency cache
* for a Java analysis.
*/
export function getJavaDependencyDirs(): string[] {
return [
// Maven
join(os.homedir(), ".m2", "repository"),
// Gradle
join(os.homedir(), ".gradle", "caches"),
// CodeQL Java build-mode: none
getJavaTempDependencyDir(),
];
}
/**
* Checks that there are files which match `patterns`. If there are matching files for any of the patterns,
* this function returns all `patterns`. Otherwise, `undefined` is returned.
*
* @param patterns The glob patterns to find matching files for.
* @returns The array of glob patterns if there are matching files, or `undefined` otherwise.
*/
export async function makePatternCheck(
patterns: string[],
): Promise<string[] | undefined> {
const globber = await makeGlobber(patterns);
if ((await globber.glob()).length === 0) {
return undefined;
}
return patterns;
}
/** These files contain accurate information about dependencies, including the exact versions
* that the relevant package manager has determined for the project. Using these gives us
* stable hashes unless the dependencies change.
*/
export const CSHARP_BASE_PATTERNS = [
// NuGet
"**/packages.lock.json",
// Paket
"**/paket.lock",
];
/** These are less accurate for use in cache key calculations, because they:
*
* - Don't contain the exact versions used. They may only contain version ranges or none at all.
* - They contain information unrelated to dependencies, which we don't care about.
*
* As a result, the hash we compute from these files may change, even if
* the dependencies haven't changed.
*/
export const CSHARP_EXTRA_PATTERNS = [
"**/*.csproj",
"**/packages.config",
"**/nuget.config",
];
/**
* Returns the list of glob patterns that should be used to calculate the cache key hash
* for a C# dependency cache. This will try to use `CSHARP_BASE_PATTERNS` whenever possible.
* As a fallback, it will also use `CSHARP_EXTRA_PATTERNS` if the corresponding FF is enabled.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @returns A list of glob patterns to use for hashing.
*/
export async function getCsharpHashPatterns(
codeql: CodeQL,
features: FeatureEnablement,
): Promise<string[] | undefined> {
const basePatterns = await internal.makePatternCheck(CSHARP_BASE_PATTERNS);
if (basePatterns !== undefined) {
return basePatterns;
}
if (await features.getValue(Feature.CsharpNewCacheKey, codeql)) {
return internal.makePatternCheck(CSHARP_EXTRA_PATTERNS);
}
// If we get to this point, we didn't find any files with `CSHARP_BASE_PATTERNS`,
// and `Feature.CsharpNewCacheKey` is not enabled.
return undefined;
}
/**
* Default caching configurations per language.
*/
function getDefaultCacheConfig(): { [language: string]: CacheConfig } {
return {
java: {
paths: [
// Maven
join(os.homedir(), ".m2", "repository"),
// Gradle
join(os.homedir(), ".gradle", "caches"),
// CodeQL Java build-mode: none
getJavaTempDependencyDir(),
],
hash: [
const defaultCacheConfigs: { [language: string]: CacheConfig } = {
java: {
getDependencyPaths: getJavaDependencyDirs,
getHashPatterns: async () =>
internal.makePatternCheck([
// Maven
"**/pom.xml",
// Gradle
@@ -63,23 +155,17 @@ function getDefaultCacheConfig(): { [language: string]: CacheConfig } {
"buildSrc/**/Dependencies.kt",
"gradle/*.versions.toml",
"**/versions.properties",
],
},
csharp: {
paths: [join(os.homedir(), ".nuget", "packages")],
hash: [
// NuGet
"**/packages.lock.json",
// Paket
"**/paket.lock",
],
},
go: {
paths: [join(os.homedir(), "go", "pkg", "mod")],
hash: ["**/go.sum"],
},
};
}
]),
},
csharp: {
getDependencyPaths: () => [join(os.homedir(), ".nuget", "packages")],
getHashPatterns: getCsharpHashPatterns,
},
go: {
getDependencyPaths: () => [join(os.homedir(), "go", "pkg", "mod")],
getHashPatterns: async () => internal.makePatternCheck(["**/go.sum"]),
},
};
async function makeGlobber(patterns: string[]): Promise<glob.Globber> {
return glob.create(patterns.join("\n"));
@@ -107,23 +193,57 @@ export interface DependencyCacheRestoreStatus {
/** An array of `DependencyCacheRestoreStatus` objects for each analysed language with a caching configuration. */
export type DependencyCacheRestoreStatusReport = DependencyCacheRestoreStatus[];
/**
* A wrapper around `cacheConfig.getHashPatterns` which logs when there are no files to calculate
* a hash for the cache key from.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @param language The language the `CacheConfig` is for. For use in the log message.
* @param cacheConfig The caching configuration to call `getHashPatterns` on.
* @param checkType Whether we are checking the patterns for a download or upload.
* @param logger The logger to write the log message to if there is an error.
* @returns An array of glob patterns to use for hashing files, or `undefined` if there are no matching files.
*/
export async function checkHashPatterns(
codeql: CodeQL,
features: FeatureEnablement,
language: Language,
cacheConfig: CacheConfig,
checkType: "download" | "upload",
logger: Logger,
): Promise<string[] | undefined> {
const patterns = await cacheConfig.getHashPatterns(codeql, features);
if (patterns === undefined) {
logger.info(
`Skipping ${checkType} of dependency cache for ${language} as we cannot calculate a hash for the cache key.`,
);
}
return patterns;
}
/**
* Attempts to restore dependency caches for the languages being analyzed.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @param languages The languages being analyzed.
* @param logger A logger to record some informational messages to.
* @param minimizeJavaJars Whether the Java extractor should rewrite downloaded JARs to minimize their size.
*
* @returns An array of `DependencyCacheRestoreStatus` objects for each analysed language with a caching configuration.
*/
export async function downloadDependencyCaches(
codeql: CodeQL,
features: FeatureEnablement,
languages: Language[],
logger: Logger,
minimizeJavaJars: boolean,
): Promise<DependencyCacheRestoreStatusReport> {
const status: DependencyCacheRestoreStatusReport = [];
for (const language of languages) {
const cacheConfig = getDefaultCacheConfig()[language];
const cacheConfig = defaultCacheConfigs[language];
if (cacheConfig === undefined) {
logger.info(
@@ -134,19 +254,22 @@ export async function downloadDependencyCaches(
// Check that we can find files to calculate the hash for the cache key from, so we don't end up
// with an empty string.
const globber = await makeGlobber(cacheConfig.hash);
if ((await globber.glob()).length === 0) {
const patterns = await checkHashPatterns(
codeql,
features,
language,
cacheConfig,
"download",
logger,
);
if (patterns === undefined) {
status.push({ language, hit_kind: CacheHitKind.NoHash });
logger.info(
`Skipping download of dependency cache for ${language} as we cannot calculate a hash for the cache key.`,
);
continue;
}
const primaryKey = await cacheKey(language, cacheConfig, minimizeJavaJars);
const primaryKey = await cacheKey(codeql, features, language, patterns);
const restoreKeys: string[] = [
await cachePrefix(language, minimizeJavaJars),
await cachePrefix(codeql, features, language),
];
logger.info(
@@ -157,7 +280,7 @@ export async function downloadDependencyCaches(
const start = performance.now();
const hitKey = await actionsCache.restoreCache(
cacheConfig.paths,
cacheConfig.getDependencyPaths(),
primaryKey,
restoreKeys,
);
@@ -203,20 +326,22 @@ export type DependencyCacheUploadStatusReport = DependencyCacheUploadStatus[];
/**
* Attempts to store caches for the languages that were analyzed.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @param config The configuration for this workflow.
* @param logger A logger to record some informational messages to.
* @param minimizeJavaJars Whether the Java extractor should rewrite downloaded JARs to minimize their size.
*
* @returns An array of `DependencyCacheUploadStatus` objects for each analysed language with a caching configuration.
*/
export async function uploadDependencyCaches(
codeql: CodeQL,
features: FeatureEnablement,
config: Config,
logger: Logger,
minimizeJavaJars: boolean,
): Promise<DependencyCacheUploadStatusReport> {
const status: DependencyCacheUploadStatusReport = [];
for (const language of config.languages) {
const cacheConfig = getDefaultCacheConfig()[language];
const cacheConfig = defaultCacheConfigs[language];
if (cacheConfig === undefined) {
logger.info(
@@ -227,13 +352,16 @@ export async function uploadDependencyCaches(
// Check that we can find files to calculate the hash for the cache key from, so we don't end up
// with an empty string.
const globber = await makeGlobber(cacheConfig.hash);
if ((await globber.glob()).length === 0) {
const patterns = await checkHashPatterns(
codeql,
features,
language,
cacheConfig,
"upload",
logger,
);
if (patterns === undefined) {
status.push({ language, result: CacheStoreResult.NoHash });
logger.info(
`Skipping upload of dependency cache for ${language} as we cannot calculate a hash for the cache key.`,
);
continue;
}
@@ -247,7 +375,11 @@ export async function uploadDependencyCaches(
// use the cache quota that we compete with. In that case, we do not wish to use up all of the quota
// with the dependency caches. For this, we could use the Cache API to check whether other workflows
// are using the quota and how full it is.
const size = await getTotalCacheSize(cacheConfig.paths, logger, true);
const size = await getTotalCacheSize(
cacheConfig.getDependencyPaths(),
logger,
true,
);
// Skip uploading an empty cache.
if (size === 0) {
@@ -258,7 +390,7 @@ export async function uploadDependencyCaches(
continue;
}
const key = await cacheKey(language, cacheConfig, minimizeJavaJars);
const key = await cacheKey(codeql, features, language, patterns);
logger.info(
`Uploading cache of size ${size} for ${language} with key ${key}...`,
@@ -266,7 +398,7 @@ export async function uploadDependencyCaches(
try {
const start = performance.now();
await actionsCache.saveCache(cacheConfig.paths, key);
await actionsCache.saveCache(cacheConfig.getDependencyPaths(), key);
const upload_duration_ms = Math.round(performance.now() - start);
status.push({
@@ -299,31 +431,86 @@ export async function uploadDependencyCaches(
/**
* Computes a cache key for the specified language.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @param language The language being analyzed.
* @param cacheConfig The cache configuration for the language.
* @param minimizeJavaJars Whether the Java extractor should rewrite downloaded JARs to minimize their size.
* @param patterns The file patterns to hash.
*
* @returns A cache key capturing information about the project(s) being analyzed in the specified language.
*/
async function cacheKey(
export async function cacheKey(
codeql: CodeQL,
features: FeatureEnablement,
language: Language,
cacheConfig: CacheConfig,
minimizeJavaJars: boolean = false,
patterns: string[],
): Promise<string> {
const hash = await glob.hashFiles(cacheConfig.hash.join("\n"));
return `${await cachePrefix(language, minimizeJavaJars)}${hash}`;
const hash = await glob.hashFiles(patterns.join("\n"));
return `${await cachePrefix(codeql, features, language)}${hash}`;
}
/**
* If experimental features which the cache contents depend on are enabled for the current language,
* this function returns a prefix that uniquely identifies the set of enabled features. The purpose of
* this is to avoid restoring caches whose contents depended on experimental features, if those
* experimental features are later disabled.
*
* @param codeql The CodeQL instance.
* @param features Information about enabled features.
* @param language The language we are creating the key for.
*
* @returns A cache key prefix identifying the enabled, experimental features that the cache depends on.
*/
export async function getFeaturePrefix(
codeql: CodeQL,
features: FeatureEnablement,
language: Language,
): Promise<string> {
const enabledFeatures: Feature[] = [];
const addFeatureIfEnabled = async (feature: Feature) => {
if (await features.getValue(feature, codeql)) {
enabledFeatures.push(feature);
}
};
if (language === KnownLanguage.java) {
// To ensure a safe rollout of JAR minimization, we change the key when the feature is enabled.
const minimizeJavaJars = await features.getValue(
Feature.JavaMinimizeDependencyJars,
codeql,
);
// To maintain backwards compatibility with this, we return "minify-" instead of a hash.
if (minimizeJavaJars) {
return "minify-";
}
} else if (language === KnownLanguage.csharp) {
await addFeatureIfEnabled(Feature.CsharpNewCacheKey);
}
// If any features that affect the cache are enabled, return a feature prefix by
// computing a hash of the feature array.
if (enabledFeatures.length > 0) {
return `${createCacheKeyHash(enabledFeatures)}-`;
}
// No feature prefix.
return "";
}
/**
* Constructs a prefix for the cache key, comprised of a CodeQL-specific prefix, a version number that
* can be changed to invalidate old caches, the runner's operating system, and the specified language name.
*
* @param codeql The CodeQL instance to use.
* @param features Information about which FFs are enabled.
* @param language The language being analyzed.
* @param minimizeJavaJars Whether the Java extractor should rewrite downloaded JARs to minimize their size.
* @returns The prefix that identifies what a cache is for.
*/
async function cachePrefix(
codeql: CodeQL,
features: FeatureEnablement,
language: Language,
minimizeJavaJars: boolean,
): Promise<string> {
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const customPrefix = process.env[EnvVar.DEPENDENCY_CACHING_PREFIX];
@@ -333,12 +520,18 @@ async function cachePrefix(
prefix = `${prefix}-${customPrefix}`;
}
// To ensure a safe rollout of JAR minimization, we change the key when the feature is enabled.
if (language === KnownLanguage.java && minimizeJavaJars) {
prefix = `minify-${prefix}`;
}
// Calculate the feature prefix for the cache, if any. This is a hash that identifies
// experimental features that affect the cache contents.
const featurePrefix = await getFeaturePrefix(codeql, features, language);
return `${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`;
// Assemble the cache key. For backwards compatibility with the JAR minification experiment's existing
// feature prefix usage, we add that feature prefix at the start. Other feature prefixes are inserted
// after the general CodeQL dependency cache prefix.
if (featurePrefix === "minify-") {
return `${featurePrefix}${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`;
} else {
return `${prefix}-${featurePrefix}${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`;
}
}
/** Represents information about our overall cache usage for CodeQL dependency caches. */
@@ -371,3 +564,7 @@ export async function getDependencyCacheUsage(
return undefined;
}
export const internal = {
makePatternCheck,
};

View File

@@ -4,7 +4,10 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import type { PullRequestBranches } from "./actions-util";
import * as apiClient from "./api-client";
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import {
shouldPerformDiffInformedAnalysis,
exportedForTesting,
} from "./diff-informed-analysis-utils";
import { Feature, Features } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
@@ -183,3 +186,201 @@ test(
},
false,
);
function runGetDiffRanges(changes: number, patch: string[] | undefined): any {
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("checkout_path")
.returns("/checkout/path");
return exportedForTesting.getDiffRanges(
{
filename: "test.txt",
changes,
patch: patch?.join("\n"),
},
getRunnerLogger(true),
);
}
test("getDiffRanges: file unchanged", async (t) => {
const diffRanges = runGetDiffRanges(0, undefined);
t.deepEqual(diffRanges, []);
});
test("getDiffRanges: file diff too large", async (t) => {
const diffRanges = runGetDiffRanges(1000000, undefined);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 0,
endLine: 0,
},
]);
});
test("getDiffRanges: diff thunk with single addition range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,6 +50,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 54,
},
]);
});
test("getDiffRanges: diff thunk with single deletion range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,8 +50,6 @@",
" a",
" b",
" c",
"-1",
"-2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, []);
});
test("getDiffRanges: diff thunk with single update range", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,7 @@",
" a",
" b",
" c",
"-1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 53,
},
]);
});
test("getDiffRanges: diff thunk with addition ranges", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,9 @@",
" a",
" b",
" c",
"+1",
" c",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 53,
},
{
path: "/checkout/path/test.txt",
startLine: 55,
endLine: 55,
},
]);
});
test("getDiffRanges: diff thunk with mixed ranges", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,7 +50,7 @@",
" a",
" b",
" c",
"-1",
" d",
"-2",
"+3",
" e",
" f",
"+4",
"+5",
" g",
" h",
" i",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 54,
endLine: 54,
},
{
path: "/checkout/path/test.txt",
startLine: 57,
endLine: 58,
},
]);
});
test("getDiffRanges: multiple diff thunks", async (t) => {
const diffRanges = runGetDiffRanges(2, [
"@@ -30,6 +50,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
"@@ -130,6 +150,8 @@",
" a",
" b",
" c",
"+1",
"+2",
" d",
" e",
" f",
]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 53,
endLine: 54,
},
{
path: "/checkout/path/test.txt",
startLine: 153,
endLine: 154,
},
]);
});
test("getDiffRanges: no diff context lines", async (t) => {
const diffRanges = runGetDiffRanges(2, ["@@ -30 +50,2 @@", "+1", "+2"]);
t.deepEqual(diffRanges, [
{
path: "/checkout/path/test.txt",
startLine: 50,
endLine: 51,
},
]);
});
test("getDiffRanges: malformed thunk header", async (t) => {
const diffRanges = runGetDiffRanges(2, ["@@ 30 +50,2 @@", "+1", "+2"]);
t.deepEqual(diffRanges, undefined);
});

View File

@@ -3,12 +3,25 @@ import * as path from "path";
import * as actionsUtil from "./actions-util";
import type { PullRequestBranches } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { getApiClient, getGitHubVersion } from "./api-client";
import type { CodeQL } from "./codeql";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
import { getRepositoryNwoFromEnv } from "./repository";
import { GitHubVariant, satisfiesGHESVersion } from "./util";
/**
* This interface is an abbreviated version of the file diff object returned by
* the GitHub API.
*/
interface FileDiff {
filename: string;
changes: number;
// A patch may be absent if the file is binary, if the file diff is too large,
// or if the file is unchanged.
patch?: string | undefined;
}
/**
* Check if the action should perform diff-informed analysis.
*/
@@ -93,3 +106,174 @@ export function readDiffRangesJsonFile(
);
return JSON.parse(jsonContents) as DiffThunkRange[];
}
/**
* Return the file line ranges that were added or modified in the pull request.
*
* @param branches The base and head branches of the pull request.
* @param logger
* @returns An array of tuples, where each tuple contains the absolute path of a
* file, the start line and the end line (both 1-based and inclusive) of an
* added or modified range in that file. Returns `undefined` if the action was
* not triggered by a pull request or if there was an error.
*/
export async function getPullRequestEditedDiffRanges(
branches: PullRequestBranches,
logger: Logger,
): Promise<DiffThunkRange[] | undefined> {
const fileDiffs = await getFileDiffsWithBasehead(branches, logger);
if (fileDiffs === undefined) {
return undefined;
}
if (fileDiffs.length >= 300) {
// The "compare two commits" API returns a maximum of 300 changed files. If
// we see that many changed files, it is possible that there could be more,
// with the rest being truncated. In this case, we should not attempt to
// compute the diff ranges, as the result would be incomplete.
logger.warning(
`Cannot retrieve the full diff because there are too many ` +
`(${fileDiffs.length}) changed files in the pull request.`,
);
return undefined;
}
const results: DiffThunkRange[] = [];
for (const filediff of fileDiffs) {
const diffRanges = getDiffRanges(filediff, logger);
if (diffRanges === undefined) {
return undefined;
}
results.push(...diffRanges);
}
return results;
}
async function getFileDiffsWithBasehead(
branches: PullRequestBranches,
logger: Logger,
): Promise<FileDiff[] | undefined> {
// Check CODE_SCANNING_REPOSITORY first. If it is empty or not set, fall back
// to GITHUB_REPOSITORY.
const repositoryNwo = getRepositoryNwoFromEnv(
"CODE_SCANNING_REPOSITORY",
"GITHUB_REPOSITORY",
);
const basehead = `${branches.base}...${branches.head}`;
try {
const response = await getApiClient().rest.repos.compareCommitsWithBasehead(
{
owner: repositoryNwo.owner,
repo: repositoryNwo.repo,
basehead,
per_page: 1,
},
);
logger.debug(
`Response from compareCommitsWithBasehead(${basehead}):` +
`\n${JSON.stringify(response, null, 2)}`,
);
return response.data.files;
} catch (error: any) {
if (error.status) {
logger.warning(`Error retrieving diff ${basehead}: ${error.message}`);
logger.debug(
`Error running compareCommitsWithBasehead(${basehead}):` +
`\nRequest: ${JSON.stringify(error.request, null, 2)}` +
`\nError Response: ${JSON.stringify(error.response, null, 2)}`,
);
return undefined;
} else {
throw error;
}
}
}
function getDiffRanges(
fileDiff: FileDiff,
logger: Logger,
): DiffThunkRange[] | undefined {
// Diff-informed queries expect the file path to be absolute. CodeQL always
// uses forward slashes as the path separator, so on Windows we need to
// replace any backslashes with forward slashes.
const filename = path
.join(actionsUtil.getRequiredInput("checkout_path"), fileDiff.filename)
.replaceAll(path.sep, "/");
if (fileDiff.patch === undefined) {
if (fileDiff.changes === 0) {
// There are situations where a changed file legitimately has no diff.
// For example, the file may be a binary file, or that the file may have
// been renamed with no changes to its contents. In these cases, the
// file would be reported as having 0 changes, and we can return an empty
// array to indicate no diff range in this file.
return [];
}
// If a file is reported to have nonzero changes but no patch, that may be
// due to the file diff being too large. In this case, we should fall back
// to a special diff range that covers the entire file.
return [
{
path: filename,
startLine: 0,
endLine: 0,
},
];
}
// The 1-based file line number of the current line
let currentLine = 0;
// The 1-based file line number that starts the current range of added lines
let additionRangeStartLine: number | undefined = undefined;
const diffRanges: DiffThunkRange[] = [];
const diffLines = fileDiff.patch.split("\n");
// Adding a fake context line at the end ensures that the following loop will
// always terminate the last range of added lines.
diffLines.push(" ");
for (const diffLine of diffLines) {
if (diffLine.startsWith("-")) {
// Ignore deletions completely -- we do not even want to consider them when
// calculating consecutive ranges of added lines.
continue;
}
if (diffLine.startsWith("+")) {
if (additionRangeStartLine === undefined) {
additionRangeStartLine = currentLine;
}
currentLine++;
continue;
}
if (additionRangeStartLine !== undefined) {
// Any line that does not start with a "+" or "-" terminates the current
// range of added lines.
diffRanges.push({
path: filename,
startLine: additionRangeStartLine,
endLine: currentLine - 1,
});
additionRangeStartLine = undefined;
}
if (diffLine.startsWith("@@ ")) {
// A new hunk header line resets the current line number.
const match = diffLine.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
if (match === null) {
logger.warning(
`Cannot parse diff hunk header for ${fileDiff.filename}: ${diffLine}`,
);
return undefined;
}
currentLine = parseInt(match[1], 10);
continue;
}
if (diffLine.startsWith(" ")) {
// An unchanged context line advances the current line number.
currentLine++;
continue;
}
}
return diffRanges;
}
export const exportedForTesting = {
getDiffRanges,
};

View File

@@ -137,4 +137,10 @@ export enum EnvVar {
* This setting is more specific than `CODEQL_ACTION_TEST_MODE`, which implies this option.
*/
SKIP_SARIF_UPLOAD = "CODEQL_ACTION_SKIP_SARIF_UPLOAD",
/**
* Whether to skip workflow validation. Intended for internal use, where we know that
* the workflow is valid and validation is not necessary.
*/
SKIP_WORKFLOW_VALIDATION = "CODEQL_ACTION_SKIP_WORKFLOW_VALIDATION",
}

View File

@@ -44,8 +44,10 @@ export interface FeatureEnablement {
*/
export enum Feature {
AllowToolcacheInput = "allow_toolcache_input",
AnalyzeUseNewUpload = "analyze_use_new_upload",
CleanupTrapCaches = "cleanup_trap_caches",
CppDependencyInstallation = "cpp_dependency_installation_enabled",
CsharpNewCacheKey = "csharp_new_cache_key",
DiffInformedQueries = "diff_informed_queries",
DisableCsharpBuildless = "disable_csharp_buildless",
DisableJavaBuildlessEnabled = "disable_java_buildless_enabled",
@@ -75,7 +77,6 @@ export enum Feature {
OverlayAnalysisSwift = "overlay_analysis_swift",
PythonDefaultIsToNotExtractStdlib = "python_default_is_to_not_extract_stdlib",
QaTelemetryEnabled = "qa_telemetry_enabled",
ResolveSupportedLanguagesUsingCli = "resolve_supported_languages_using_cli",
UseRepositoryProperties = "use_repository_properties",
ValidateDbConfig = "validate_db_config",
}
@@ -116,6 +117,11 @@ export const featureConfig: Record<
envVar: "CODEQL_ACTION_ALLOW_TOOLCACHE_INPUT",
minimumVersion: undefined,
},
[Feature.AnalyzeUseNewUpload]: {
defaultValue: false,
envVar: "CODEQL_ACTION_ANALYZE_USE_NEW_UPLOAD",
minimumVersion: undefined,
},
[Feature.CleanupTrapCaches]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
@@ -127,6 +133,11 @@ export const featureConfig: Record<
legacyApi: true,
minimumVersion: "2.15.0",
},
[Feature.CsharpNewCacheKey]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CSHARP_NEW_CACHE_KEY",
minimumVersion: undefined,
},
[Feature.DiffInformedQueries]: {
defaultValue: true,
envVar: "CODEQL_ACTION_DIFF_INFORMED_QUERIES",
@@ -155,12 +166,6 @@ export const featureConfig: Record<
legacyApi: true,
minimumVersion: undefined,
},
[Feature.ResolveSupportedLanguagesUsingCli]: {
defaultValue: false,
envVar: "CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI",
minimumVersion: undefined,
toolsFeature: ToolsFeature.BuiltinExtractorsSpecifyDefaultQueries,
},
[Feature.OverlayAnalysis]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",
@@ -647,7 +652,7 @@ class GitHubFeatureFlags {
}
this.logger.debug(
"Loaded the following default values for the feature flags from the Code Scanning API:",
"Loaded the following default values for the feature flags from the CodeQL Action API:",
);
for (const [feature, value] of Object.entries(remoteFlags).sort(
([nameA], [nameB]) => nameA.localeCompare(nameB),
@@ -657,12 +662,13 @@ class GitHubFeatureFlags {
this.hasAccessedRemoteFeatureFlags = true;
return remoteFlags;
} catch (e) {
if (util.isHTTPError(e) && e.status === 403) {
const httpError = util.asHTTPError(e);
if (httpError?.status === 403) {
this.logger.warning(
"This run of the CodeQL Action does not have permission to access Code Scanning API endpoints. " +
"This run of the CodeQL Action does not have permission to access the CodeQL Action API endpoints. " +
"As a result, it will not be opted into any experimental features. " +
"This could be because the Action is running on a pull request from a fork. If not, " +
`please ensure the Action has the 'security-events: write' permission. Details: ${e.message}`,
`please ensure the workflow has at least the 'security-events: read' permission. Details: ${httpError.message}`,
);
this.hasAccessedRemoteFeatureFlags = false;
return {};

View File

@@ -86,7 +86,7 @@ import {
getErrorMessage,
BuildMode,
} from "./util";
import { validateWorkflow } from "./workflow";
import { checkWorkflow } from "./workflow";
/**
* Sends a status report indicating that the `init` Action is starting.
@@ -288,16 +288,9 @@ async function run() {
toolsSource = initCodeQLResult.toolsSource;
zstdAvailability = initCodeQLResult.zstdAvailability;
core.startGroup("Validating workflow");
const validateWorkflowResult = await validateWorkflow(codeql, logger);
if (validateWorkflowResult === undefined) {
logger.info("Detected no issues with the code scanning workflow.");
} else {
logger.warning(
`Unable to validate code scanning workflow: ${validateWorkflowResult}`,
);
}
core.endGroup();
// Check the workflow for problems. If there are any problems, they are reported
// to the workflow log. No exceptions are thrown.
await checkWorkflow(logger, codeql);
// Set CODEQL_ENABLE_EXPERIMENTAL_FEATURES for Rust if between 2.19.3 (included) and 2.22.1 (excluded)
// We need to set this environment variable before initializing the config, otherwise Rust
@@ -585,15 +578,12 @@ async function run() {
}
// Restore dependency cache(s), if they exist.
const minimizeJavaJars = await features.getValue(
Feature.JavaMinimizeDependencyJars,
codeql,
);
if (shouldRestoreCache(config.dependencyCachingEnabled)) {
dependencyCachingResults = await downloadDependencyCaches(
codeql,
features,
config.languages,
logger,
minimizeJavaJars,
);
}
@@ -655,7 +645,7 @@ async function run() {
`${EnvVar.JAVA_EXTRACTOR_MINIMIZE_DEPENDENCY_JARS} is already set to '${process.env[EnvVar.JAVA_EXTRACTOR_MINIMIZE_DEPENDENCY_JARS]}', so the Action will not override it.`,
);
} else if (
minimizeJavaJars &&
(await features.getValue(Feature.JavaMinimizeDependencyJars, codeql)) &&
config.dependencyCachingEnabled &&
config.buildMode === BuildMode.None &&
config.languages.includes(KnownLanguage.java)

View File

@@ -13,7 +13,15 @@ export interface Logger {
}
export function getActionsLogger(): Logger {
return core;
return {
debug: core.debug,
info: core.info,
warning: core.warning,
error: core.error,
isDebug: core.isDebug,
startGroup: core.startGroup,
endGroup: core.endGroup,
};
}
export function getRunnerLogger(debugMode: boolean): Logger {

View File

@@ -11,6 +11,8 @@ import * as gitUtils from "./git-utils";
import { getRunnerLogger } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
getCacheRestoreKeyPrefix,
getCacheSaveKey,
OverlayDatabaseMode,
writeBaseDatabaseOidsFile,
writeOverlayChangesFile,
@@ -261,3 +263,48 @@ test(
},
false,
);
test("overlay-base database cache keys remain stable", async (t) => {
const logger = getRunnerLogger(true);
const config = createTestConfig({ languages: ["python", "javascript"] });
const codeQlVersion = "2.23.0";
const commitOid = "abc123def456";
sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/");
sinon.stub(gitUtils, "getCommitOid").resolves(commitOid);
sinon.stub(actionsUtil, "getWorkflowRunID").returns(12345);
sinon.stub(actionsUtil, "getWorkflowRunAttempt").returns(1);
const saveKey = await getCacheSaveKey(
config,
codeQlVersion,
"checkout-path",
logger,
);
const expectedSaveKey =
"codeql-overlay-base-database-1-c5666c509a2d9895-javascript_python-2.23.0-abc123def456-12345-1";
t.is(
saveKey,
expectedSaveKey,
"Cache save key changed unexpectedly. " +
"This may indicate breaking changes in the cache key generation logic.",
);
const restoreKeyPrefix = await getCacheRestoreKeyPrefix(
config,
codeQlVersion,
);
const expectedRestoreKeyPrefix =
"codeql-overlay-base-database-1-c5666c509a2d9895-javascript_python-2.23.0-";
t.is(
restoreKeyPrefix,
expectedRestoreKeyPrefix,
"Cache restore key prefix changed unexpectedly. " +
"This may indicate breaking changes in the cache key generation logic.",
);
t.true(
saveKey.startsWith(restoreKeyPrefix),
`Expected save key "${saveKey}" to start with restore key prefix "${restoreKeyPrefix}"`,
);
});

View File

@@ -1,16 +1,22 @@
import * as crypto from "crypto";
import * as fs from "fs";
import * as path from "path";
import * as actionsCache from "@actions/cache";
import { getRequiredInput, getTemporaryDirectory } from "./actions-util";
import {
getRequiredInput,
getTemporaryDirectory,
getWorkflowRunAttempt,
getWorkflowRunID,
} from "./actions-util";
import { getAutomationID } from "./api-client";
import { createCacheKeyHash } from "./caching-utils";
import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { getCommitOid, getFileOidsUnderPath } from "./git-utils";
import { Logger, withGroupAsync } from "./logging";
import {
getErrorMessage,
isInTestMode,
tryGetFolderBytes,
waitForResultWithTimeLimit,
@@ -34,15 +40,10 @@ export const CODEQL_OVERLAY_MINIMUM_VERSION = "2.22.4";
* Actions Cache client library. Instead we place a limit on the uncompressed
* size of the overlay-base database.
*
* Assuming 2.5:1 compression ratio, the 15 GB limit on uncompressed data would
* translate to a limit of around 6 GB after compression. This is a high limit
* compared to the default 10GB Actions Cache capacity, but enforcement of Actions
* Cache quotas is not immediate.
*
* TODO: revisit this limit before removing the restriction for overlay analysis
* to the `github` and `dsp-testing` orgs.
* Assuming 2.5:1 compression ratio, the 7.5 GB limit on uncompressed data would
* translate to a limit of around 3 GB after compression.
*/
const OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB = 15000;
const OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB = 7500;
const OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_BYTES =
OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB * 1_000_000;
@@ -271,6 +272,7 @@ export async function uploadOverlayBaseDatabaseToCache(
config,
codeQlVersion,
checkoutPath,
logger,
);
logger.info(
`Uploading overlay-base database to Actions cache with key ${cacheSaveKey}`,
@@ -448,17 +450,28 @@ export async function downloadOverlayBaseDatabaseFromCache(
* The key consists of the restore key prefix (which does not include the
* commit SHA) and the commit SHA of the current checkout.
*/
async function getCacheSaveKey(
export async function getCacheSaveKey(
config: Config,
codeQlVersion: string,
checkoutPath: string,
logger: Logger,
): Promise<string> {
let runId = 1;
let attemptId = 1;
try {
runId = getWorkflowRunID();
attemptId = getWorkflowRunAttempt();
} catch (e) {
logger.warning(
`Failed to get workflow run ID or attempt ID. Reason: ${getErrorMessage(e)}`,
);
}
const sha = await getCommitOid(checkoutPath);
const restoreKeyPrefix = await getCacheRestoreKeyPrefix(
config,
codeQlVersion,
);
return `${restoreKeyPrefix}${sha}`;
return `${restoreKeyPrefix}${sha}-${runId}-${attemptId}`;
}
/**
@@ -475,7 +488,7 @@ async function getCacheSaveKey(
* not include the commit SHA. This allows us to restore the most recent
* compatible overlay-base database.
*/
async function getCacheRestoreKeyPrefix(
export async function getCacheRestoreKeyPrefix(
config: Config,
codeQlVersion: string,
): Promise<string> {
@@ -501,27 +514,3 @@ async function getCacheRestoreKeyPrefix(
// easier to debug and understand the cache key structure.
return `${CACHE_PREFIX}-${CACHE_VERSION}-${componentsHash}-${languages}-${codeQlVersion}-`;
}
/**
* Creates a SHA-256 hash of the cache key components to ensure uniqueness
* while keeping the cache key length manageable.
*
* @param components Object containing all components that should influence cache key uniqueness
* @returns A short SHA-256 hash (first 16 characters) of the components
*/
function createCacheKeyHash(components: Record<string, any>): string {
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
//
// "Properties are visited using the same algorithm as Object.keys(), which
// has a well-defined order and is stable across implementations. For example,
// JSON.stringify on the same object will always produce the same string, and
// JSON.parse(JSON.stringify(obj)) would produce an object with the same key
// ordering as the original (assuming the object is completely
// JSON-serializable)."
const componentsJson = JSON.stringify(components);
return crypto
.createHash("sha256")
.update(componentsJson)
.digest("hex")
.substring(0, 16);
}

View File

@@ -168,7 +168,7 @@ export function tryGetTagNameFromUrl(
// assumes less about the structure of the URL.
const match = matches[matches.length - 1];
if (match === null || match.length !== 2) {
if (match?.length !== 2) {
logger.debug(
`Could not determine tag name for URL ${url}. Matched ${JSON.stringify(
match,

View File

@@ -30,7 +30,7 @@ async function runWrapper() {
logger,
);
if ((config && config.debugMode) || core.isDebug()) {
if (config?.debugMode || core.isDebug()) {
const logFilePath = core.getState("proxy-log-file");
logger.info(
"Debug mode is on. Uploading proxy log as Actions debugging artifact...",

View File

@@ -23,7 +23,6 @@ import { getRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import {
ConfigurationError,
isHTTPError,
getRequiredEnvParam,
getCachedCodeQlVersion,
isInTestMode,
@@ -33,6 +32,7 @@ import {
BuildMode,
getErrorMessage,
getTestingEnvironment,
asHTTPError,
} from "./util";
export enum ActionName {
@@ -252,7 +252,7 @@ export interface EventReport {
*
* @param actionName The name of the action, e.g. 'init', 'finish', 'upload-sarif'
* @param status The status. Must be 'success', 'failure', or 'starting'
* @param startedAt The time this action started executing.
* @param actionStartedAt The time this action started executing.
* @param cause Cause of failure (only supply if status is 'failure')
* @param exception Exception (only supply if status is 'failure')
* @returns undefined if an exception was thrown.
@@ -387,9 +387,9 @@ export async function createStatusReportBase(
}
const OUT_OF_DATE_MSG =
"CodeQL Action is out-of-date. Please upgrade to the latest version of codeql-action.";
"CodeQL Action is out-of-date. Please upgrade to the latest version of `codeql-action`.";
const INCOMPATIBLE_MSG =
"CodeQL Action version is incompatible with the code scanning endpoint. Please update to a compatible version of codeql-action.";
"CodeQL Action version is incompatible with the API endpoint. Please update to a compatible version of `codeql-action`.";
/**
* Send a status report to the code_scanning/analysis/status endpoint.
@@ -429,8 +429,9 @@ export async function sendStatusReport<S extends StatusReportBase>(
},
);
} catch (e) {
if (isHTTPError(e)) {
switch (e.status) {
const httpError = asHTTPError(e);
if (httpError !== undefined) {
switch (httpError.status) {
case 403:
if (
getWorkflowEventName() === "push" &&
@@ -438,16 +439,20 @@ export async function sendStatusReport<S extends StatusReportBase>(
) {
core.warning(
'Workflows triggered by Dependabot on the "push" event run with read-only access. ' +
"Uploading Code Scanning results requires write access. " +
'To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. ' +
"Uploading CodeQL results requires write access. " +
'To use CodeQL with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. ' +
`See ${DocUrl.SCANNING_ON_PUSH} for more information on how to configure these events.`,
);
} else {
core.warning(e.message);
core.warning(
"This run of the CodeQL Action does not have permission to access the CodeQL Action API endpoints. " +
"This could be because the Action is running on a pull request from a fork. If not, " +
`please ensure the workflow has at least the 'security-events: read' permission. Details: ${httpError.message}`,
);
}
return;
case 404:
core.warning(e.message);
core.warning(httpError.message);
return;
case 422:
// schema incompatibility when reporting status
@@ -465,7 +470,7 @@ export async function sendStatusReport<S extends StatusReportBase>(
// something else has gone wrong and the request/response will be logged by octokit
// it's possible this is a transient error and we should continue scanning
core.warning(
`An unexpected error occurred when sending code scanning status report: ${getErrorMessage(
`An unexpected error occurred when sending a status report: ${getErrorMessage(
e,
)}`,
);

View File

@@ -9,7 +9,7 @@ import * as semver from "semver";
import { CommandInvocationError } from "./actions-util";
import { Logger } from "./logging";
import { assertNever, cleanUpGlob, isBinaryAccessible } from "./util";
import { assertNever, cleanUpPath, isBinaryAccessible } from "./util";
const MIN_REQUIRED_BSD_TAR_VERSION = "3.4.3";
const MIN_REQUIRED_GNU_TAR_VERSION = "1.31";
@@ -35,14 +35,14 @@ async function getTarVersion(): Promise<TarVersion> {
// Return whether this is GNU tar or BSD tar, and the version number
if (stdout.includes("GNU tar")) {
const match = stdout.match(/tar \(GNU tar\) ([0-9.]+)/);
if (!match || !match[1]) {
if (!match?.[1]) {
throw new Error("Failed to parse output of tar --version.");
}
return { type: "gnu", version: match[1] };
} else if (stdout.includes("bsdtar")) {
const match = stdout.match(/bsdtar ([0-9.]+)/);
if (!match || !match[1]) {
if (!match?.[1]) {
throw new Error("Failed to parse output of tar --version.");
}
@@ -217,7 +217,7 @@ export async function extractTarZst(
});
});
} catch (e) {
await cleanUpGlob(dest, "extraction destination directory", logger);
await cleanUpPath(dest, "extraction destination directory", logger);
throw e;
}
}

View File

@@ -12,7 +12,7 @@ import * as semver from "semver";
import { formatDuration, Logger } from "./logging";
import * as tar from "./tar";
import { cleanUpGlob, getErrorMessage, getRequiredEnvParam } from "./util";
import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
/**
* High watermark to use when streaming the download and extraction of the CodeQL tools.
@@ -130,7 +130,7 @@ export async function downloadAndExtract(
// If we failed during processing, we want to clean up the destination directory
// before we try again.
await cleanUpGlob(dest, "CodeQL bundle", logger);
await cleanUpPath(dest, "CodeQL bundle", logger);
}
const toolsDownloadStart = performance.now();
@@ -167,7 +167,7 @@ export async function downloadAndExtract(
)}).`,
);
} finally {
await cleanUpGlob(archivedBundlePath, "CodeQL bundle archive", logger);
await cleanUpPath(archivedBundlePath, "CodeQL bundle archive", logger);
}
return {

View File

@@ -3,13 +3,11 @@ import * as semver from "semver";
import type { VersionInfo } from "./codeql";
export enum ToolsFeature {
AnalysisSummaryV2IsDefault = "analysisSummaryV2Default",
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",
DatabaseInterpretResultsSupportsSarifRunProperty = "databaseInterpretResultsSupportsSarifRunProperty",
ForceOverwrite = "forceOverwrite",
IndirectTracingSupportsStaticBinaries = "indirectTracingSupportsStaticBinaries",
PythonDefaultIsToNotExtractStdlib = "pythonDefaultIsToNotExtractStdlib",
SarifMergeRunsFromEqualCategory = "sarifMergeRunsFromEqualCategory",
}
/**

View File

@@ -13,8 +13,8 @@ import * as gitUtils from "./git-utils";
import { Language } from "./languages";
import { Logger } from "./logging";
import {
asHTTPError,
getErrorMessage,
isHTTPError,
tryGetFolderBytes,
waitForResultWithTimeLimit,
} from "./util";
@@ -236,7 +236,7 @@ export async function cleanupTrapCaches(
}
return { trap_cache_cleanup_size_bytes: totalBytesCleanedUp };
} catch (e) {
if (isHTTPError(e) && e.status === 403) {
if (asHTTPError(e)?.status === 403) {
logger.warning(
"Could not cleanup TRAP caches as the token did not have the required permissions. " +
'To clean up TRAP caches, ensure the token has the "actions:write" permission. ' +

View File

@@ -21,7 +21,6 @@ import * as gitUtils from "./git-utils";
import { initCodeQL } from "./init";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
import {
ConfigurationError,
@@ -269,32 +268,6 @@ async function combineSarifFilesUsingCLI(
codeQL = initCodeQLResult.codeql;
}
if (
!(await codeQL.supportsFeature(
ToolsFeature.SarifMergeRunsFromEqualCategory,
))
) {
await throwIfCombineSarifFilesDisabled(sarifObjects, gitHubVersion);
logger.warning(
"The CodeQL CLI does not support merging SARIF files. Merging files in the action.",
);
if (
await shouldShowCombineSarifFilesDeprecationWarning(
sarifObjects,
gitHubVersion,
)
) {
logger.warning(
`Uploading multiple CodeQL runs with the same category is deprecated ${deprecationWarningMessage} for CodeQL CLI 2.16.6 and earlier. Please update your CodeQL CLI version or update your workflow to set a distinct category for each CodeQL run. ${deprecationMoreInformationMessage}`,
);
core.exportVariable("CODEQL_MERGE_SARIF_DEPRECATION_WARNING", "true");
}
return combineSarifFiles(sarifFiles, logger);
}
const baseTempDir = path.resolve(tempDir, "combined-sarif");
fs.mkdirSync(baseTempDir, { recursive: true });
const outputDirectory = fs.mkdtempSync(path.resolve(baseTempDir, "output-"));
@@ -386,16 +359,17 @@ export async function uploadPayload(
logger.info("Successfully uploaded results");
return response.data.id as string;
} catch (e) {
if (util.isHTTPError(e)) {
switch (e.status) {
const httpError = util.asHTTPError(e);
if (httpError !== undefined) {
switch (httpError.status) {
case 403:
core.warning(e.message || GENERIC_403_MSG);
core.warning(httpError.message || GENERIC_403_MSG);
break;
case 404:
core.warning(e.message || GENERIC_404_MSG);
core.warning(httpError.message || GENERIC_404_MSG);
break;
default:
core.warning(e.message);
core.warning(httpError.message);
break;
}
}
@@ -687,51 +661,39 @@ export function buildPayload(
return payloadObj;
}
/**
* Uploads a single SARIF file or a directory of SARIF files depending on what `inputSarifPath` refers
* to.
*/
export async function uploadFiles(
inputSarifPath: string,
checkoutPath: string,
category: string | undefined,
features: FeatureEnablement,
logger: Logger,
uploadTarget: analyses.AnalysisConfig,
): Promise<UploadResult> {
const sarifPaths = getSarifFilePaths(
inputSarifPath,
uploadTarget.sarifPredicate,
);
return uploadSpecifiedFiles(
sarifPaths,
checkoutPath,
category,
features,
logger,
uploadTarget,
);
export interface PostProcessingResults {
sarif: util.SarifFile;
analysisKey: string;
environment: string;
}
/**
* Uploads the given array of SARIF files.
* Performs post-processing of the SARIF files given by `sarifPaths`.
*
* @param logger The logger to use.
* @param features Information about enabled features.
* @param checkoutPath The path where the repo was checked out at.
* @param sarifPaths The paths of the SARIF files to post-process.
* @param category The analysis category.
* @param analysis The analysis configuration.
*
* @returns Returns the results of post-processing the SARIF files,
* including the resulting SARIF file.
*/
export async function uploadSpecifiedFiles(
sarifPaths: string[],
checkoutPath: string,
category: string | undefined,
features: FeatureEnablement,
export async function postProcessSarifFiles(
logger: Logger,
uploadTarget: analyses.AnalysisConfig,
): Promise<UploadResult> {
logger.startGroup(`Uploading ${uploadTarget.name} results`);
logger.info(`Processing sarif files: ${JSON.stringify(sarifPaths)}`);
features: FeatureEnablement,
checkoutPath: string,
sarifPaths: string[],
category: string | undefined,
analysis: analyses.AnalysisConfig,
): Promise<PostProcessingResults> {
logger.info(`Post-processing sarif files: ${JSON.stringify(sarifPaths)}`);
const gitHubVersion = await getGitHubVersion();
let sarif: SarifFile;
category = uploadTarget.fixCategory(logger, category);
category = analysis.fixCategory(logger, category);
if (sarifPaths.length > 1) {
// Validate that the files we were asked to upload are all valid SARIF files
@@ -767,6 +729,113 @@ export async function uploadSpecifiedFiles(
environment,
);
return { sarif, analysisKey, environment };
}
/**
* Writes the post-processed SARIF file to disk, if needed based on `pathInput` or the `SARIF_DUMP_DIR`.
*
* @param logger The logger to use.
* @param pathInput The input provided for `post-processed-sarif-path`.
* @param uploadTarget The upload target.
* @param postProcessingResults The results of post-processing SARIF files.
*/
export async function writePostProcessedFiles(
logger: Logger,
pathInput: string | undefined,
uploadTarget: analyses.AnalysisConfig,
postProcessingResults: PostProcessingResults,
) {
// If there's an explicit input, use that. Otherwise, use the value from the environment variable.
const outputPath = pathInput || util.getOptionalEnvVar(EnvVar.SARIF_DUMP_DIR);
// If we have a non-empty output path, write the SARIF file to it.
if (outputPath !== undefined) {
dumpSarifFile(
JSON.stringify(postProcessingResults.sarif),
outputPath,
logger,
uploadTarget,
);
} else {
logger.debug(`Not writing post-processed SARIF files.`);
}
}
/**
* Uploads a single SARIF file or a directory of SARIF files depending on what `inputSarifPath` refers
* to.
*/
export async function uploadFiles(
inputSarifPath: string,
checkoutPath: string,
category: string | undefined,
features: FeatureEnablement,
logger: Logger,
uploadTarget: analyses.AnalysisConfig,
): Promise<UploadResult> {
const sarifPaths = getSarifFilePaths(
inputSarifPath,
uploadTarget.sarifPredicate,
);
return uploadSpecifiedFiles(
sarifPaths,
checkoutPath,
category,
features,
logger,
uploadTarget,
);
}
/**
* Uploads the given array of SARIF files.
*/
async function uploadSpecifiedFiles(
sarifPaths: string[],
checkoutPath: string,
category: string | undefined,
features: FeatureEnablement,
logger: Logger,
uploadTarget: analyses.AnalysisConfig,
): Promise<UploadResult> {
const processingResults: PostProcessingResults = await postProcessSarifFiles(
logger,
features,
checkoutPath,
sarifPaths,
category,
uploadTarget,
);
return uploadPostProcessedFiles(
logger,
checkoutPath,
uploadTarget,
processingResults,
);
}
/**
* Uploads the results of post-processing SARIF files to the specified upload target.
*
* @param logger The logger to use.
* @param checkoutPath The path at which the repository was checked out.
* @param uploadTarget The analysis configuration.
* @param postProcessingResults The results of post-processing SARIF files.
*
* @returns The results of uploading the `postProcessingResults` to `uploadTarget`.
*/
export async function uploadPostProcessedFiles(
logger: Logger,
checkoutPath: string,
uploadTarget: analyses.AnalysisConfig,
postProcessingResults: PostProcessingResults,
): Promise<UploadResult> {
logger.startGroup(`Uploading ${uploadTarget.name} results`);
const sarif = postProcessingResults.sarif;
const toolNames = util.getToolNames(sarif);
logger.debug(`Validating that each SARIF run has a unique category`);
@@ -774,11 +843,6 @@ export async function uploadSpecifiedFiles(
logger.debug(`Serializing SARIF for upload`);
const sarifPayload = JSON.stringify(sarif);
const dumpDir = process.env[EnvVar.SARIF_DUMP_DIR];
if (dumpDir) {
dumpSarifFile(sarifPayload, dumpDir, logger, uploadTarget);
}
logger.debug(`Compressing serialized SARIF`);
const zippedSarif = zlib.gzipSync(sarifPayload).toString("base64");
const checkoutURI = url.pathToFileURL(checkoutPath).href;
@@ -786,13 +850,13 @@ export async function uploadSpecifiedFiles(
const payload = buildPayload(
await gitUtils.getCommitOid(checkoutPath),
await gitUtils.getRef(),
analysisKey,
postProcessingResults.analysisKey,
util.getRequiredEnvParam("GITHUB_WORKFLOW"),
zippedSarif,
actionsUtil.getWorkflowRunID(),
actionsUtil.getWorkflowRunAttempt(),
checkoutURI,
environment,
postProcessingResults.environment,
toolNames,
await gitUtils.determineBaseBranchHeadCommitOid(),
);
@@ -838,14 +902,14 @@ function dumpSarifFile(
fs.mkdirSync(outputDir, { recursive: true });
} else if (!fs.lstatSync(outputDir).isDirectory()) {
throw new ConfigurationError(
`The path specified by the ${EnvVar.SARIF_DUMP_DIR} environment variable exists and is not a directory: ${outputDir}`,
`The path that processed SARIF files should be written to exists, but is not a directory: ${outputDir}`,
);
}
const outputFile = path.resolve(
outputDir,
`upload${uploadTarget.sarifExtension}`,
);
logger.info(`Dumping processed SARIF file to ${outputFile}`);
logger.info(`Writing processed SARIF file to ${outputFile}`);
fs.writeFileSync(outputFile, sarifPayload);
}

View File

@@ -16,7 +16,7 @@ import {
isThirdPartyAnalysis,
} from "./status-report";
import * as upload_lib from "./upload-lib";
import { uploadSarif } from "./upload-sarif";
import { postProcessAndUploadSarif } from "./upload-sarif";
import {
ConfigurationError,
checkActionVersion,
@@ -90,9 +90,10 @@ async function run() {
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
const category = actionsUtil.getOptionalInput("category");
const uploadResults = await uploadSarif(
const uploadResults = await postProcessAndUploadSarif(
logger,
features,
"always",
checkoutPath,
sarifPath,
category,

View File

@@ -9,7 +9,7 @@ import { getRunnerLogger } from "./logging";
import { createFeatures, setupTests } from "./testing-utils";
import { UploadResult } from "./upload-lib";
import * as uploadLib from "./upload-lib";
import { uploadSarif } from "./upload-sarif";
import { postProcessAndUploadSarif } from "./upload-sarif";
import * as util from "./util";
setupTests(test);
@@ -19,7 +19,27 @@ interface UploadSarifExpectedResult {
expectedFiles?: string[];
}
const uploadSarifMacro = test.macro({
function mockPostProcessSarifFiles() {
const postProcessSarifFiles = sinon.stub(uploadLib, "postProcessSarifFiles");
for (const analysisKind of Object.values(AnalysisKind)) {
const analysisConfig = getAnalysisConfig(analysisKind);
postProcessSarifFiles
.withArgs(
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
sinon.match.any,
analysisConfig,
)
.resolves({ sarif: { runs: [] }, analysisKey: "", environment: "" });
}
return postProcessSarifFiles;
}
const postProcessAndUploadSarifMacro = test.macro({
exec: async (
t: ExecutionContext<unknown>,
sarifFiles: string[],
@@ -33,21 +53,16 @@ const uploadSarifMacro = test.macro({
const toFullPath = (filename: string) => path.join(tempDir, filename);
const uploadSpecifiedFiles = sinon.stub(
const postProcessSarifFiles = mockPostProcessSarifFiles();
const uploadPostProcessedFiles = sinon.stub(
uploadLib,
"uploadSpecifiedFiles",
"uploadPostProcessedFiles",
);
for (const analysisKind of Object.values(AnalysisKind)) {
uploadSpecifiedFiles
.withArgs(
sinon.match.any,
sinon.match.any,
sinon.match.any,
features,
logger,
getAnalysisConfig(analysisKind),
)
const analysisConfig = getAnalysisConfig(analysisKind);
uploadPostProcessedFiles
.withArgs(logger, sinon.match.any, analysisConfig, sinon.match.any)
.resolves(expectedResult[analysisKind as AnalysisKind]?.uploadResult);
}
@@ -56,53 +71,57 @@ const uploadSarifMacro = test.macro({
fs.writeFileSync(sarifFile, "");
}
const actual = await uploadSarif(logger, features, "", testPath);
const actual = await postProcessAndUploadSarif(
logger,
features,
"always",
"",
testPath,
);
for (const analysisKind of Object.values(AnalysisKind)) {
const analysisKindResult = expectedResult[analysisKind];
if (analysisKindResult) {
// We are expecting a result for this analysis kind, check that we have it.
t.deepEqual(actual[analysisKind], analysisKindResult.uploadResult);
// Additionally, check that the mocked `uploadSpecifiedFiles` was called with only the file paths
// Additionally, check that the mocked `postProcessSarifFiles` was called with only the file paths
// that we expected it to be called with.
t.assert(
uploadSpecifiedFiles.calledWith(
postProcessSarifFiles.calledWith(
logger,
features,
sinon.match.any,
analysisKindResult.expectedFiles?.map(toFullPath) ??
fullSarifPaths,
sinon.match.any,
sinon.match.any,
features,
logger,
getAnalysisConfig(analysisKind),
),
);
} else {
// Otherwise, we are not expecting a result for this analysis kind. However, note that `undefined`
// is also returned by our mocked `uploadSpecifiedFiles` when there is no expected result for this
// is also returned by our mocked `uploadProcessedFiles` when there is no expected result for this
// analysis kind.
t.is(actual[analysisKind], undefined);
// Therefore, we also check that the mocked `uploadSpecifiedFiles` was not called for this analysis kind.
// Therefore, we also check that the mocked `uploadProcessedFiles` was not called for this analysis kind.
t.assert(
!uploadSpecifiedFiles.calledWith(
sinon.match.any,
sinon.match.any,
sinon.match.any,
features,
!uploadPostProcessedFiles.calledWith(
logger,
sinon.match.any,
getAnalysisConfig(analysisKind),
sinon.match.any,
),
`uploadSpecifiedFiles was called for ${analysisKind}, but should not have been.`,
`uploadProcessedFiles was called for ${analysisKind}, but should not have been.`,
);
}
}
});
},
title: (providedTitle = "") => `uploadSarif - ${providedTitle}`,
title: (providedTitle = "") => `processAndUploadSarif - ${providedTitle}`,
});
test(
"SARIF file",
uploadSarifMacro,
postProcessAndUploadSarifMacro,
["test.sarif"],
(tempDir) => path.join(tempDir, "test.sarif"),
{
@@ -117,7 +136,7 @@ test(
test(
"JSON file",
uploadSarifMacro,
postProcessAndUploadSarifMacro,
["test.json"],
(tempDir) => path.join(tempDir, "test.json"),
{
@@ -132,7 +151,7 @@ test(
test(
"Code Scanning files",
uploadSarifMacro,
postProcessAndUploadSarifMacro,
["test.json", "test.sarif"],
undefined,
{
@@ -148,7 +167,7 @@ test(
test(
"Code Quality file",
uploadSarifMacro,
postProcessAndUploadSarifMacro,
["test.quality.sarif"],
(tempDir) => path.join(tempDir, "test.quality.sarif"),
{
@@ -163,7 +182,7 @@ test(
test(
"Mixed files",
uploadSarifMacro,
postProcessAndUploadSarifMacro,
["test.sarif", "test.quality.sarif"],
undefined,
{
@@ -183,3 +202,65 @@ test(
},
},
);
test("postProcessAndUploadSarif doesn't upload if upload is disabled", async (t) => {
await util.withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const features = createFeatures([]);
const toFullPath = (filename: string) => path.join(tempDir, filename);
const postProcessSarifFiles = mockPostProcessSarifFiles();
const uploadPostProcessedFiles = sinon.stub(
uploadLib,
"uploadPostProcessedFiles",
);
fs.writeFileSync(toFullPath("test.sarif"), "");
fs.writeFileSync(toFullPath("test.quality.sarif"), "");
const actual = await postProcessAndUploadSarif(
logger,
features,
"never",
"",
tempDir,
);
t.truthy(actual);
t.assert(postProcessSarifFiles.calledTwice);
t.assert(uploadPostProcessedFiles.notCalled);
});
});
test("postProcessAndUploadSarif writes post-processed SARIF files if output directory is provided", async (t) => {
await util.withTmpDir(async (tempDir) => {
const logger = getRunnerLogger(true);
const features = createFeatures([]);
const toFullPath = (filename: string) => path.join(tempDir, filename);
const postProcessSarifFiles = mockPostProcessSarifFiles();
fs.writeFileSync(toFullPath("test.sarif"), "");
fs.writeFileSync(toFullPath("test.quality.sarif"), "");
const postProcessedOutPath = path.join(tempDir, "post-processed");
const actual = await postProcessAndUploadSarif(
logger,
features,
"never",
"",
tempDir,
"",
postProcessedOutPath,
);
t.truthy(actual);
t.assert(postProcessSarifFiles.calledTwice);
t.assert(fs.existsSync(path.join(postProcessedOutPath, "upload.sarif")));
t.assert(
fs.existsSync(path.join(postProcessedOutPath, "upload.quality.sarif")),
);
});
});

View File

@@ -1,3 +1,4 @@
import { UploadKind } from "./actions-util";
import * as analyses from "./analyses";
import { FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
@@ -10,22 +11,26 @@ export type UploadSarifResults = Partial<
>;
/**
* Finds SARIF files in `sarifPath` and uploads them to the appropriate services.
* Finds SARIF files in `sarifPath`, post-processes them, and uploads them to the appropriate services.
*
* @param logger The logger to use.
* @param features Information about enabled features.
* @param uploadKind The kind of upload that is requested.
* @param checkoutPath The path where the repository was checked out at.
* @param sarifPath The path to the file or directory to upload.
* @param category The analysis category.
* @param postProcessedOutputPath The path to a directory to which the post-processed SARIF files should be written to.
*
* @returns A partial mapping from analysis kinds to the upload results.
*/
export async function uploadSarif(
export async function postProcessAndUploadSarif(
logger: Logger,
features: FeatureEnablement,
uploadKind: UploadKind,
checkoutPath: string,
sarifPath: string,
category?: string,
postProcessedOutputPath?: string,
): Promise<UploadSarifResults> {
const sarifGroups = await upload_lib.getGroupedSarifFilePaths(
logger,
@@ -37,14 +42,33 @@ export async function uploadSarif(
sarifGroups,
)) {
const analysisConfig = analyses.getAnalysisConfig(analysisKind);
uploadResults[analysisKind] = await upload_lib.uploadSpecifiedFiles(
sarifFiles,
checkoutPath,
category,
features,
const postProcessingResults = await upload_lib.postProcessSarifFiles(
logger,
features,
checkoutPath,
sarifFiles,
category,
analysisConfig,
);
// Write the post-processed SARIF files to disk. This will only write them if needed based on user inputs
// or environment variables.
await upload_lib.writePostProcessedFiles(
logger,
postProcessedOutputPath,
analysisConfig,
postProcessingResults,
);
// Only perform the actual upload of the post-processed files if `uploadKind` is `always`.
if (uploadKind === "always") {
uploadResults[analysisKind] = await upload_lib.uploadPostProcessedFiles(
logger,
checkoutPath,
analysisConfig,
postProcessingResults,
);
}
}
return uploadResults;

View File

@@ -101,16 +101,6 @@ test("getMemoryFlag() throws if the ram input is < 0 or NaN", async (t) => {
}
});
test("getAddSnippetsFlag() should return the correct flag", (t) => {
t.deepEqual(util.getAddSnippetsFlag(true), "--sarif-add-snippets");
t.deepEqual(util.getAddSnippetsFlag("true"), "--sarif-add-snippets");
t.deepEqual(util.getAddSnippetsFlag(false), "--no-sarif-add-snippets");
t.deepEqual(util.getAddSnippetsFlag(undefined), "--no-sarif-add-snippets");
t.deepEqual(util.getAddSnippetsFlag("false"), "--no-sarif-add-snippets");
t.deepEqual(util.getAddSnippetsFlag("foo bar"), "--no-sarif-add-snippets");
});
test("getThreadsFlag() should return the correct --threads flag", (t) => {
const numCpus = os.cpus().length;
@@ -252,6 +242,35 @@ test("allowed API versions", async (t) => {
);
});
test("getRequiredEnvParam - gets environment variables", (t) => {
process.env.SOME_UNIT_TEST_VAR = "foo";
const result = util.getRequiredEnvParam("SOME_UNIT_TEST_VAR");
t.is(result, "foo");
});
test("getRequiredEnvParam - throws if an environment variable isn't set", (t) => {
t.throws(() => util.getRequiredEnvParam("SOME_UNIT_TEST_VAR"));
});
test("getOptionalEnvVar - gets environment variables", (t) => {
process.env.SOME_UNIT_TEST_VAR = "foo";
const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR");
t.is(result, "foo");
});
test("getOptionalEnvVar - gets undefined for empty environment variables", (t) => {
process.env.SOME_UNIT_TEST_VAR = "";
const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR");
t.is(result, undefined);
});
test("getOptionalEnvVar - doesn't throw for undefined environment variables", (t) => {
t.notThrows(() => {
const result = util.getOptionalEnvVar("SOME_UNIT_TEST_VAR");
t.is(result, undefined);
});
});
test("doesDirectoryExist", async (t) => {
// Returns false if no file/dir of this name exists
t.false(util.doesDirectoryExist("non-existent-file.txt"));
@@ -428,13 +447,21 @@ const CHECK_ACTION_VERSION_TESTS: Array<[string, util.GitHubVersion, boolean]> =
["2.2.1", { type: util.GitHubVariant.DOTCOM }, true],
["2.2.1", { type: util.GitHubVariant.GHE_DOTCOM }, true],
["2.2.1", { type: util.GitHubVariant.GHES, version: "3.10" }, false],
["2.2.1", { type: util.GitHubVariant.GHES, version: "3.11" }, true],
["2.2.1", { type: util.GitHubVariant.GHES, version: "3.12" }, true],
["3.2.1", { type: util.GitHubVariant.DOTCOM }, false],
["3.2.1", { type: util.GitHubVariant.GHE_DOTCOM }, false],
["2.2.1", { type: util.GitHubVariant.GHES, version: "3.11" }, false],
["2.2.1", { type: util.GitHubVariant.GHES, version: "3.12" }, false],
["3.2.1", { type: util.GitHubVariant.DOTCOM }, true],
["3.2.1", { type: util.GitHubVariant.GHE_DOTCOM }, true],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.10" }, false],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.11" }, false],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.12" }, false],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.19" }, false],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.20" }, true],
["3.2.1", { type: util.GitHubVariant.GHES, version: "3.21" }, true],
["4.2.1", { type: util.GitHubVariant.DOTCOM }, false],
["4.2.1", { type: util.GitHubVariant.GHE_DOTCOM }, false],
["4.2.1", { type: util.GitHubVariant.GHES, version: "3.19" }, false],
["4.2.1", { type: util.GitHubVariant.GHES, version: "3.20" }, false],
["4.2.1", { type: util.GitHubVariant.GHES, version: "3.21" }, false],
];
for (const [
@@ -461,9 +488,7 @@ for (const [
if (shouldReportError) {
t.true(
warningSpy.calledOnceWithExactly(
sinon.match(
"CodeQL Action major versions v1 and v2 have been deprecated.",
),
sinon.match("CodeQL Action v3 will be deprecated in December 2026."),
),
);
} else {
@@ -505,3 +530,12 @@ test("getCgroupCpuCountFromCpus returns undefined if the CPU file exists but is
);
});
});
test("checkDiskUsage succeeds and produces positive numbers", async (t) => {
process.env["GITHUB_WORKSPACE"] = os.tmpdir();
const diskUsage = await util.checkDiskUsage(getRunnerLogger(true));
if (t.truthy(diskUsage)) {
t.true(diskUsage.numAvailableBytes > 0);
t.true(diskUsage.numTotalBytes > 0);
}
});

View File

@@ -1,12 +1,11 @@
import * as fs from "fs";
import * as fsPromises from "fs/promises";
import * as os from "os";
import * as path from "path";
import * as core from "@actions/core";
import * as exec from "@actions/exec/lib/exec";
import * as io from "@actions/io";
import checkDiskSpace from "check-disk-space";
import * as del from "del";
import getFolderSize from "get-folder-size";
import * as yaml from "js-yaml";
import * as semver from "semver";
@@ -167,7 +166,7 @@ export async function withTmpDir<T>(
): Promise<T> {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codeql-action-"));
const result = await body(tmpDir);
await del.deleteAsync(tmpDir, { force: true });
await fs.promises.rm(tmpDir, { force: true, recursive: true });
return result;
}
@@ -343,21 +342,6 @@ export function getMemoryFlag(
return `--ram=${megabytes}`;
}
/**
* Get the codeql flag to specify whether to add code snippets to the sarif file.
*
* @returns string
*/
export function getAddSnippetsFlag(
userInput: string | boolean | undefined,
): string {
if (typeof userInput === "string") {
// have to process specifically because any non-empty string is truthy
userInput = userInput.toLowerCase() === "true";
}
return userInput ? "--sarif-add-snippets" : "--no-sarif-add-snippets";
}
/**
* Get the value of the codeql `--threads` flag specified for the `threads`
* input. If no value was specified, all available threads will be used.
@@ -673,6 +657,17 @@ export function getRequiredEnvParam(paramName: string): string {
return value;
}
/**
* Get an environment variable, but return `undefined` if it is not set or empty.
*/
export function getOptionalEnvVar(paramName: string): string | undefined {
const value = process.env[paramName];
if (value?.trim().length === 0) {
return undefined;
}
return value;
}
export class HTTPError extends Error {
public status: number;
@@ -692,8 +687,22 @@ export class ConfigurationError extends Error {
}
}
export function isHTTPError(arg: any): arg is HTTPError {
return arg?.status !== undefined && Number.isInteger(arg.status);
export function asHTTPError(arg: any): HTTPError | undefined {
if (
typeof arg !== "object" ||
arg === null ||
typeof arg.message !== "string"
) {
return undefined;
}
if (Number.isInteger(arg.status)) {
return new HTTPError(arg.message as string, arg.status as number);
}
// See https://github.com/actions/toolkit/blob/acb230b99a46ed33a3f04a758cd68b47b9a82908/packages/tool-cache/src/tool-cache.ts#L19
if (Number.isInteger(arg.httpStatusCode)) {
return new HTTPError(arg.message as string, arg.httpStatusCode as number);
}
return undefined;
}
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
@@ -731,7 +740,7 @@ export async function bundleDb(
// from somewhere else or someone trying to make the action upload a
// non-database file.
if (fs.existsSync(databaseBundlePath)) {
await del.deleteAsync(databaseBundlePath, { force: true });
await fs.promises.rm(databaseBundlePath, { force: true });
}
await codeql.databaseBundle(databasePath, databaseBundlePath, dbName);
return databaseBundlePath;
@@ -1074,24 +1083,17 @@ export async function checkDiskUsage(
logger: Logger,
): Promise<DiskUsage | undefined> {
try {
// We avoid running the `df` binary under the hood for macOS ARM runners with SIP disabled.
if (
process.platform === "darwin" &&
(process.arch === "arm" || process.arch === "arm64") &&
!(await checkSipEnablement(logger))
) {
return undefined;
}
const diskUsage = await checkDiskSpace(
const diskUsage = await fsPromises.statfs(
getRequiredEnvParam("GITHUB_WORKSPACE"),
);
const mbInBytes = 1024 * 1024;
const gbInBytes = 1024 * 1024 * 1024;
if (diskUsage.free < 2 * gbInBytes) {
const blockSizeInBytes = diskUsage.bsize;
const numBlocksPerMb = (1024 * 1024) / blockSizeInBytes;
const numBlocksPerGb = (1024 * 1024 * 1024) / blockSizeInBytes;
if (diskUsage.bavail < 2 * numBlocksPerGb) {
const message =
"The Actions runner is running low on disk space " +
`(${(diskUsage.free / mbInBytes).toPrecision(4)} MB available).`;
`(${(diskUsage.bavail / numBlocksPerMb).toPrecision(4)} MB available).`;
if (process.env[EnvVar.HAS_WARNED_ABOUT_DISK_SPACE] !== "true") {
logger.warning(message);
} else {
@@ -1100,8 +1102,8 @@ export async function checkDiskUsage(
core.exportVariable(EnvVar.HAS_WARNED_ABOUT_DISK_SPACE, "true");
}
return {
numAvailableBytes: diskUsage.free,
numTotalBytes: diskUsage.size,
numAvailableBytes: diskUsage.bavail * blockSizeInBytes,
numTotalBytes: diskUsage.blocks * blockSizeInBytes,
};
} catch (error) {
logger.warning(
@@ -1112,38 +1114,38 @@ export async function checkDiskUsage(
}
/**
* Prompt the customer to upgrade to CodeQL Action v3, if appropriate.
* Prompt the customer to upgrade to CodeQL Action v4, if appropriate.
*
* Check whether a customer is running v1 or v2. If they are, and we can determine that the GitHub
* instance supports v3, then log an error prompting the customer to upgrade to v3.
* Check whether a customer is running v3. If they are, and we can determine that the GitHub
* instance supports v4, then log an error prompting the customer to upgrade to v4.
*/
export function checkActionVersion(
version: string,
githubVersion: GitHubVersion,
) {
if (
!semver.satisfies(version, ">=3") && // do not log error if the customer is already running v3
!semver.satisfies(version, ">=4") && // do not log error if the customer is already running v4
!process.env[EnvVar.LOG_VERSION_DEPRECATION] // do not log error if we have already
) {
// Only error for versions of GHES that are compatible with CodeQL Action version 3.
// Only error for versions of GHES that are compatible with CodeQL Action version 4.
//
// GHES 3.11 shipped without the v3 tag, but it also shipped without this warning message code.
// Therefore users who are seeing this warning message code have pulled in a new version of the
// Action, and with it the v3 tag.
// GHES 3.20 is the first version to ship with the v4 tag and this warning message code.
// Therefore, users who are seeing this warning message code are running on GHES 3.20 or newer,
// and should update to CodeQL Action v4.
if (
githubVersion.type === GitHubVariant.DOTCOM ||
githubVersion.type === GitHubVariant.GHE_DOTCOM ||
(githubVersion.type === GitHubVariant.GHES &&
semver.satisfies(
semver.coerce(githubVersion.version) ?? "0.0.0",
">=3.11",
">=3.20",
))
) {
core.error(
"CodeQL Action major versions v1 and v2 have been deprecated. " +
"Please update all occurrences of the CodeQL Action in your workflow files to v3. " +
"CodeQL Action v3 will be deprecated in December 2026. " +
"Please update all occurrences of the CodeQL Action in your workflow files to v4. " +
"For more information, see " +
"https://github.blog/changelog/2025-01-10-code-scanning-codeql-action-v2-is-now-deprecated/",
"https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/",
);
// set LOG_VERSION_DEPRECATION env var to prevent the warning from being logged multiple times
core.exportVariable(EnvVar.LOG_VERSION_DEPRECATION, "true");
@@ -1238,19 +1240,13 @@ export async function checkSipEnablement(
}
}
export async function cleanUpGlob(glob: string, name: string, logger: Logger) {
export async function cleanUpPath(file: string, name: string, logger: Logger) {
logger.debug(`Cleaning up ${name}.`);
try {
const deletedPaths = await del.deleteAsync(glob, { force: true });
if (deletedPaths.length === 0) {
logger.warning(
`Failed to clean up ${name}: no files found matching ${glob}.`,
);
} else if (deletedPaths.length === 1) {
logger.debug(`Cleaned up ${name}.`);
} else {
logger.debug(`Cleaned up ${name} (${deletedPaths.length} files).`);
}
await fs.promises.rm(file, {
force: true,
recursive: true,
});
} catch (e) {
logger.warning(`Failed to clean up ${name}: ${e}.`);
}

View File

@@ -2,9 +2,17 @@ import test, { ExecutionContext } from "ava";
import * as yaml from "js-yaml";
import * as sinon from "sinon";
import { getCodeQLForTesting } from "./codeql";
import { setupTests } from "./testing-utils";
import * as actionsUtil from "./actions-util";
import { createStubCodeQL, getCodeQLForTesting } from "./codeql";
import { EnvVar } from "./environment";
import {
checkExpectedLogMessages,
getRecordingLogger,
LoggedMessage,
setupTests,
} from "./testing-utils";
import {
checkWorkflow,
CodedError,
formatWorkflowCause,
formatWorkflowErrors,
@@ -13,6 +21,7 @@ import {
Workflow,
WorkflowErrors,
} from "./workflow";
import * as workflow from "./workflow";
function errorCodes(
actual: CodedError[],
@@ -870,3 +879,78 @@ test("getCategoryInputOrThrow throws error for workflow with multiple calls to a
},
);
});
test("checkWorkflow - validates workflow if `SKIP_WORKFLOW_VALIDATION` is not set", async (t) => {
const messages: LoggedMessage[] = [];
const codeql = createStubCodeQL({});
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
validateWorkflow.resolves(undefined);
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(
validateWorkflow.calledOnce,
"`checkWorkflow` unexpectedly did not call `validateWorkflow`",
);
checkExpectedLogMessages(t, messages, [
"Detected no issues with the code scanning workflow.",
]);
});
test("checkWorkflow - logs problems with workflow validation", async (t) => {
const messages: LoggedMessage[] = [];
const codeql = createStubCodeQL({});
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
validateWorkflow.resolves("problem");
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(
validateWorkflow.calledOnce,
"`checkWorkflow` unexpectedly did not call `validateWorkflow`",
);
checkExpectedLogMessages(t, messages, [
"Unable to validate code scanning workflow: problem",
]);
});
test("checkWorkflow - skips validation if `SKIP_WORKFLOW_VALIDATION` is `true`", async (t) => {
process.env[EnvVar.SKIP_WORKFLOW_VALIDATION] = "true";
const messages: LoggedMessage[] = [];
const codeql = createStubCodeQL({});
sinon.stub(actionsUtil, "isDynamicWorkflow").returns(false);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(
validateWorkflow.notCalled,
"`checkWorkflow` called `validateWorkflow` unexpectedly",
);
t.is(messages.length, 0);
});
test("checkWorkflow - skips validation for `dynamic` workflows", async (t) => {
const messages: LoggedMessage[] = [];
const codeql = createStubCodeQL({});
const isDynamicWorkflow = sinon
.stub(actionsUtil, "isDynamicWorkflow")
.returns(true);
const validateWorkflow = sinon.stub(workflow.internal, "validateWorkflow");
await checkWorkflow(getRecordingLogger(messages), codeql);
t.assert(isDynamicWorkflow.calledOnce);
t.assert(
validateWorkflow.notCalled,
"`checkWorkflow` called `validateWorkflow` unexpectedly",
);
t.is(messages.length, 0);
});

View File

@@ -5,8 +5,10 @@ import zlib from "zlib";
import * as core from "@actions/core";
import * as yaml from "js-yaml";
import { isDynamicWorkflow } from "./actions-util";
import * as api from "./api-client";
import { CodeQL } from "./codeql";
import { EnvVar } from "./environment";
import { Logger } from "./logging";
import {
getRequiredEnvParam,
@@ -216,7 +218,7 @@ function hasWorkflowTrigger(triggerName: string, doc: Workflow): boolean {
return Object.prototype.hasOwnProperty.call(doc.on, triggerName);
}
export async function validateWorkflow(
async function validateWorkflow(
codeql: CodeQL,
logger: Logger,
): Promise<undefined | string> {
@@ -371,7 +373,7 @@ function getInputOrThrow(
input = input.replace(`\${{matrix.${key}}}`, value);
}
}
if (input !== undefined && input.includes("${{")) {
if (input?.includes("${{")) {
throw new Error(
`Could not get ${inputName} input to ${actionName} since it contained an unrecognized dynamic value.`,
);
@@ -462,3 +464,36 @@ export function getCheckoutPathInputOrThrow(
) || getRequiredEnvParam("GITHUB_WORKSPACE") // if unspecified, checkout_path defaults to ${{ github.workspace }}
);
}
/**
* A wrapper around `validateWorkflow` which reports the outcome.
*
* @param logger The logger to use.
* @param codeql The CodeQL instance.
*/
export async function checkWorkflow(logger: Logger, codeql: CodeQL) {
// Check the workflow for problems, unless `SKIP_WORKFLOW_VALIDATION` is `true`
// or the workflow trigger is `dynamic`.
if (
!isDynamicWorkflow() &&
process.env[EnvVar.SKIP_WORKFLOW_VALIDATION] !== "true"
) {
core.startGroup("Validating workflow");
const validateWorkflowResult = await internal.validateWorkflow(
codeql,
logger,
);
if (validateWorkflowResult === undefined) {
logger.info("Detected no issues with the code scanning workflow.");
} else {
logger.debug(
`Unable to validate code scanning workflow: ${validateWorkflowResult}`,
);
}
core.endGroup();
}
}
export const internal = {
validateWorkflow,
};

Some files were not shown because too many files have changed in this diff Show More