Compare commits

...

103 Commits

Author SHA1 Message Date
Henry Mercer 794f5bc385 Fix the memory check debug message at equality
The comparison accepts exactly the minimum, so say "at or above", to
match the wording of the disk space check.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-06 16:48:44 +01:00
Henry Mercer 54109818e0 Address review feedback on the disk space check
Say "at or above" in the debug message logged when the check passes,
since the comparison accepts exactly the minimum.

Check each feature flag against the threshold its name declares, rather
than only exercising a few of them, so that a mistake in one of the
mappings cannot go unnoticed. Both sides of the boundary are needed to
pin a threshold down: a mapping to a lower value would still pass the
case at the limit, and one to a higher value would still fail the case
below it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-06 16:41:48 +01:00
Henry Mercer 99caaa8b90 Remove the overlay_analysis_resource_checks_v2 feature flag
The flag no longer has any effect now that its 14 GB threshold is the
unconditional default, so remove it. Setting
CODEQL_ACTION_OVERLAY_ANALYSIS_RESOURCE_CHECKS_V2 no longer does
anything.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-06 16:38:34 +01:00
Henry Mercer 6117bb503a Derive overlay minimum disk space from feature flags
Overlay analysis required 20 GB of available disk space, lowered to
14 GB when overlay_analysis_resource_checks_v2 was enabled. That gave
us a single step to roll out, and any further reduction needed another
flag and another release.

Determine the threshold from the new overlay_analysis_min_disk_N_gb
flags instead, taking the lowest one that is enabled so that a lower
limit can be rolled out to a subset of repositories without first
disabling the flag above it. When none are enabled, the 14 GB limit
now applies unconditionally, replacing the 20 GB default.

Thresholds remain in decimal MB, matching the bytes-per-MB convention
the disk check already used, so the effective byte values are unchanged
from the previous 14 GB path. Also log the available and required space
at debug level when the check passes, so that run logs show which
threshold took effect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-06 16:38:33 +01:00
Henry Mercer af767ec1f6 Add overlay_analysis_min_disk_N_gb feature flags
Add six feature flags, overlay_analysis_min_disk_8_gb through
overlay_analysis_min_disk_13_gb, which will be used to control the
amount of available disk space that overlay analysis requires.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-06 16:38:33 +01:00
Henry Mercer 7d9249f5a5 Merge pull request #4076 from github/dependabot/npm_and_yarn/npm-minor-b3aad9cfa5
Bump globals from 17.7.0 to 17.8.0 in the npm-minor group across 1 directory
2026-08-06 11:26:17 +00:00
Henry Mercer 8ebf1091b0 Merge pull request #4077 from github/dependabot/github_actions/dot-github/workflows/actions-minor-6bc1927ba3
Bump actions/setup-java from 5.6.0 to 5.7.0 in /.github/workflows in the actions-minor group across 1 directory
2026-08-06 11:25:11 +00:00
github-actions[bot] bdf39710a2 Rebuild 2026-08-05 17:59:47 +00:00
dependabot[bot] 74cfae9be6 Bump actions/setup-java
Bumps the actions-minor group with 1 update in the /.github/workflows directory: [actions/setup-java](https://github.com/actions/setup-java).


Updates `actions/setup-java` from 5.6.0 to 5.7.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/03ad4de0992f5dab5e18fcb136590ce7c4a0ac95...b6effb05e454b25005698d916606bdc6ffcbf961)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 17:56:46 +00:00
dependabot[bot] 47a0a833bb Bump globals in the npm-minor group across 1 directory
Bumps the npm-minor group with 1 update in the / directory: [globals](https://github.com/sindresorhus/globals).


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

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 17:54:39 +00:00
Michael B. Gale 6a90bf1f54 Merge pull request #4075 from github/dependabot/npm_and_yarn/brace-expansion-1.1.18
Bump brace-expansion from 1.1.16 to 1.1.18
2026-08-04 14:52:59 +00:00
github-actions[bot] c5995f544d Rebuild 2026-08-04 14:19:52 +00:00
dependabot[bot] 76c44396d3 Bump brace-expansion from 1.1.16 to 1.1.18
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.16 to 1.1.18.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.16...v1.1.18)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 14:18:04 +00:00
Michael B. Gale fad141fa6c Merge pull request #4073 from github/mergeback/v4.37.6-to-main-5595ccaf
Mergeback v4.37.6 refs/heads/releases/v4 into main
2026-08-04 14:03:40 +00:00
github-actions[bot] 7d82f1132f Rebuild 2026-08-04 13:34:54 +00:00
github-actions[bot] 37bdbde050 Update changelog and version after v4.37.6 2026-08-04 13:34:41 +00:00
Michael B. Gale 5595ccaf91 Merge pull request #4071 from github/update-v4.37.6-6a9359a1b
Merge main into releases/v4
2026-08-04 14:33:02 +01:00
Michael B. Gale ec9c75796a Add change note for PR 4070 2026-08-04 14:19:36 +01:00
github-actions[bot] 45c8742e17 Update changelog for v4.37.6 2026-08-04 13:15:26 +00:00
Michael B. Gale 6a9359a1bd Merge pull request #4070 from github/mbg/remote-address/change-file-default
Change `DEFAULT_CONFIG_FILE_NAME`
2026-08-03 14:18:48 +00:00
Michael B. Gale 065cdc0394 Change DEFAULT_CONFIG_FILE_NAME 2026-08-03 15:02:48 +01:00
Michael B. Gale f99dd5aeee Merge pull request #4066 from github/dependabot/npm_and_yarn/js-yaml-5.2.2
Bump js-yaml from 5.2.1 to 5.2.2
2026-08-03 13:26:42 +00:00
Henry Mercer 1804b211a3 Merge pull request #4068 from github/mergeback/v4.37.5-to-main-d1ba80a1
Mergeback v4.37.5 refs/heads/releases/v4 into main
2026-08-03 11:43:09 +00:00
github-actions[bot] 3020a2f462 Rebuild 2026-08-03 11:03:06 +00:00
github-actions[bot] 93c3a5a40b Update changelog and version after v4.37.5 2026-08-03 11:02:52 +00:00
Henry Mercer d1ba80a13d Merge pull request #4067 from github/update-v4.37.5-1cd4d01d5
Merge main into releases/v4
2026-08-03 12:01:20 +01:00
github-actions[bot] e74600b0d9 Update changelog for v4.37.5 2026-08-03 09:18:21 +00:00
github-actions[bot] 266c7bdbd2 Rebuild 2026-08-01 14:26:29 +00:00
dependabot[bot] daa7fe6fba Bump js-yaml from 5.2.1 to 5.2.2
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 5.2.1 to 5.2.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.2.1...5.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 14:24:54 +00:00
Michael B. Gale 1cd4d01d58 Merge pull request #4061 from github/henrymercer/turbo-system
Handle network errors when streaming the CodeQL bundle download
2026-07-31 16:04:48 +00:00
Michael B. Gale d2bfc30bc3 Merge pull request #4050 from github/mbg/status/registries
Include `registry_types` in more status reports
2026-07-31 15:32:50 +00:00
Sam Robson 68028fcb16 Merge pull request #4062 from github/sam-robson/migrate-enterprise-release-pat
ci: update Enterprise release checkout credential
2026-07-31 14:05:01 +00:00
Sam Robson c29563eeaa ci: use federated enterprise release PAT 2026-07-31 10:10:39 +01:00
sim 155e522997 Link the PR from the changelog entry
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-30 18:48:10 +01:00
sim 2d3b351ea6 Handle network errors when streaming the CodeQL bundle download
A network error such as `ECONNRESET` while streaming the download and
extraction of the CodeQL bundle terminated the `init` Action rather than
falling back to downloading the bundle before extracting it, since no
`error` listener was attached to the request returned by `https.get`.

Also pipe the response into `tar` using `stream.pipeline` so that errors
on the response itself are surfaced and `tar`'s standard input is closed,
and abort the request if it stalls.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-30 18:47:27 +01:00
Michael B. Gale 5d3eb98e4a Merge pull request #4055 from github/dependabot/npm_and_yarn/npm-minor-2032624187
Bump the npm-minor group across 1 directory with 2 updates
2026-07-30 16:18:33 +00:00
Michael B. Gale c5f739bd64 Merge branch 'main' into dependabot/npm_and_yarn/npm-minor-2032624187 2026-07-30 17:03:50 +01:00
Michael B. Gale 2e53f1cea2 Merge pull request #4056 from github/dependabot/github_actions/dot-github/workflows/actions-minor-6592b736e3
Bump ruby/setup-ruby from 1.319.0 to 1.321.0 in /.github/workflows in the actions-minor group across 1 directory
2026-07-30 10:14:47 +00:00
Óscar San José d0ee43ac62 Merge pull request #4057 from github/mergeback/v4.37.4-to-main-f205ea1c
Mergeback v4.37.4 refs/heads/releases/v4 into main
2026-07-30 09:37:10 +00:00
github-actions[bot] 06f1d4ffed Rebuild 2026-07-30 09:21:35 +00:00
github-actions[bot] 82f035a501 Update changelog and version after v4.37.4 2026-07-30 09:21:21 +00:00
Óscar San José f205ea1c33 Merge pull request #4053 from github/update-v4.37.4-9130ce0f7
Merge main into releases/v4
2026-07-30 11:19:36 +02:00
github-actions[bot] 60a57910be Rebuild 2026-07-29 17:59:24 +00:00
dependabot[bot] 3502f79575 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.319.0 to 1.321.0
- [Release notes](https://github.com/ruby/setup-ruby/releases)
- [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb)
- [Commits](https://github.com/ruby/setup-ruby/compare/003a5c4d8d6321bd302e38f6f0ec593f77f06600...95ef2b042f9d7a56d8268cba8559e2842e2ad01b)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 17:57:26 +00:00
dependabot[bot] 8f0a4f23c4 Bump the npm-minor group across 1 directory with 2 updates
Bumps the npm-minor group with 2 updates in the / directory: [sinon](https://github.com/sinonjs/sinon) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `sinon` from 22.0.0 to 22.1.0
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/CHANGES.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v22.0.0...v22.1.0)

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

---
updated-dependencies:
- dependency-name: sinon
  dependency-version: 22.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: typescript-eslint
  dependency-version: 8.65.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 17:54:52 +00:00
Michael B. Gale 780c00da3f Merge remote-tracking branch 'origin/main' into mbg/status/registries 2026-07-29 16:52:25 +01:00
Michael B. Gale f02afd42b0 Merge pull request #4049 from github/mbg/start-proxy/job-uuid
Generate job UUID in `start-proxy` action
2026-07-29 15:29:24 +00:00
Michael B. Gale d57c3ffcba Add tests for runInActions 2026-07-29 15:38:27 +01:00
github-actions[bot] e40d079dd9 Update changelog for v4.37.4 2026-07-29 14:37:39 +00:00
Michael B. Gale 8e6fdffc32 Use runInActions for start-proxy 2026-07-29 14:44:24 +01:00
Michael B. Gale ba46ff760e Add transformTelemetryError option to Action 2026-07-29 14:39:30 +01:00
Michael B. Gale b411bbcd4a Move getJobUUID call into runInActions for init and setup-codeql 2026-07-29 14:38:26 +01:00
Óscar San José 9130ce0f73 Merge pull request #4051 from github/update-bundle/codeql-bundle-v2.26.2
Update default bundle to 2.26.2
2026-07-29 12:45:04 +00:00
Michael B. Gale 0cebd1d28d Add hasEnv delayed assertion and use for getJobUUID test 2026-07-29 13:24:33 +01:00
Michael B. Gale d2f5cbbe91 Add get method to ReadOnlyEnv 2026-07-29 13:22:46 +01:00
github-actions[bot] c62d824686 Add changelog note 2026-07-29 12:22:31 +00:00
github-actions[bot] da0c190101 Update default bundle to codeql-bundle-v2.26.2 2026-07-29 12:22:25 +00:00
Michael B. Gale 36737508ec Add Env-backed ActionsEnv implementation for tests 2026-07-29 13:00:31 +01:00
Michael B. Gale 30c33c9286 Make results of function call available to delayed checks 2026-07-29 12:56:51 +01:00
Michael B. Gale 3ca82bb259 Change withActions to only allow mutations 2026-07-29 12:55:54 +01:00
Michael B. Gale 42a3b94790 Add CODEQL_ACTION_ prefix to JOB_RUN_UUID 2026-07-29 11:58:44 +01:00
Michael B. Gale 13d4882649 Validate JSON more 2026-07-29 11:35:46 +01:00
Michael B. Gale e55a57b808 Add RegistryBase schema and type 2026-07-29 11:21:19 +01:00
Michael B. Gale 51d51e8121 Add boolean Validator to json module 2026-07-29 11:19:57 +01:00
Michael B. Gale e893985e8b Fix makeValidator returning required: boolean 2026-07-29 11:19:21 +01:00
Michael B. Gale eb692f8b49 Add check to createStatusReportBase test 2026-07-29 10:57:52 +01:00
Michael B. Gale aac07d2a41 Include registry_types whenever CODEQL_PROXY_URLS is set 2026-07-29 10:55:46 +01:00
Michael B. Gale de57c4a441 Move registry_types to StatusReportBase 2026-07-29 10:38:08 +01:00
Michael B. Gale 2e251072b0 Use getEnv() 2026-07-29 10:20:04 +01:00
Michael B. Gale 94a12eb6f6 Add a test for invalid values 2026-07-28 19:14:57 +01:00
Michael B. Gale e28cbacfa1 Test that getJobUUID calls exportVariable 2026-07-28 19:13:35 +01:00
Michael B. Gale 60834a0cd9 Add exportVariable to ActionsEnv 2026-07-28 19:00:30 +01:00
Michael B. Gale e9831f72a2 Add getRequiredInput to ActionsEnv 2026-07-28 18:59:59 +01:00
Michael B. Gale 766928d055 Call getJobUUID in start-proxy
The `start-proxy` step precedes `init` in Default Setup
2026-07-28 18:50:56 +01:00
Michael B. Gale 049af32c59 Allow getJobUUID to retrieve the UUID from the environment 2026-07-28 18:47:23 +01:00
Michael B. Gale c7ae51bb2d Make ActionState available and add test 2026-07-28 18:41:41 +01:00
Michael B. Gale 1f9caf0118 Refactor jobRunUuid init into a function
Use in `init` and `setup-codeql` actions
2026-07-28 18:37:01 +01:00
Michael B. Gale 18420e3271 Merge pull request #4043 from github/mbg/ts/changelog
Convert last remaining Python CI scripts to TypeScript
2026-07-28 15:27:26 +00:00
Michael B. Gale 7e8d8970f0 Merge pull request #4046 from github/mbg/repo-prop/code-quality
Ignore configuration file repo property for unsupported analysis kinds
2026-07-28 11:16:26 +00:00
Michael B. Gale 2d4c474c2c Log !analysisKindSupported case 2026-07-28 12:02:41 +01:00
Michael B. Gale 98c05a17d3 Fix argument validation in rollback-changelog.ts
Co-authored-by: Mads Navntoft <navntoft@github.com>
2026-07-28 11:51:13 +01:00
Michael B. Gale 8289a49271 Ignore repository property for unsupported analysis kinds 2026-07-27 13:23:39 +01:00
Michael B. Gale 2a8731cc06 Move config-file computation after determining the analysisKinds 2026-07-27 13:10:34 +01:00
Michael B. Gale 3434fbbc53 Merge pull request #4044 from github/mbg/ff/promote-toolcache
Promote `AllowToolcacheInput` feature
2026-07-24 16:29:59 +00:00
Michael B. Gale 3013ac07bd Promote AllowToolcacheInput feature 2026-07-24 16:28:21 +01:00
Michael B. Gale 74b15aa2c6 Install JS deps if needed in post-release-mergeback workflow 2026-07-24 16:19:45 +01:00
Michael B. Gale f00f809405 Fix checking keys rather than values 2026-07-24 16:17:04 +01:00
Michael B. Gale 0953dc00da Add getErrorMessage to pr-checks-local util.ts to avoid pulling in src/util.ts dependencies 2026-07-24 16:08:18 +01:00
Michael B. Gale cbad145443 Remove Python-specific steps from workflows that no longer need them 2026-07-24 16:05:55 +01:00
Michael B. Gale ab44eb939d Remove python from CodeQL workflow
There is no more (non-test) Python code left to analyse, so CodeQL analysis would fail now
2026-07-24 16:05:10 +01:00
Michael B. Gale 961b583f9a Use rollback-changelog.ts and remove Python version 2026-07-24 16:05:10 +01:00
Michael B. Gale d71461774b Add rollback-changelog.ts with tests 2026-07-24 16:05:09 +01:00
Michael B. Gale 027ac05d3b Use bundle-changelog.ts and remove Python version 2026-07-24 16:05:09 +01:00
Michael B. Gale 66a6f42f0a Add bundle-changelog.ts with tests 2026-07-24 16:05:09 +01:00
Michael B. Gale c5d621238d Update extractChangelogSnippet to use parseChangelog 2026-07-24 16:05:09 +01:00
Michael B. Gale adba0868a4 Update processChangelogForBackports to use parseChangelog 2026-07-24 16:05:09 +01:00
Michael B. Gale 916098aa8d Add parseChangelog and renderChangelog 2026-07-24 16:05:08 +01:00
Michael B. Gale 093dce6cc2 Add extractChangelogSnippet test for the case where there is no first section 2026-07-24 16:05:08 +01:00
Michael B. Gale 57eb44123f Add constant for unreleased placeholder 2026-07-24 16:05:08 +01:00
Michael B. Gale 5901394530 Remove prepare_changelog.py 2026-07-24 16:05:08 +01:00
Michael B. Gale b69467ce8b Update workflows to use prepare-changelog.ts 2026-07-24 16:05:08 +01:00
Michael B. Gale 85d157095f Add prepare-changelog.ts with tests 2026-07-24 16:05:08 +01:00
Michael B. Gale 2d14f71964 Add NO_CHANGES_STR constant 2026-07-24 16:05:07 +01:00
58 changed files with 2656 additions and 1183 deletions
@@ -25,17 +25,6 @@ runs:
shell: bash
run: npm ci
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install PyGithub==2.3.0 requests
shell: bash
- name: Update git config
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin
+2 -2
View File
@@ -71,8 +71,8 @@ jobs:
run: |
cd "$RUNNER_TEMP/results"
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'."
if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
exit 1
else
echo "Found job run UUID '$actual'."
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Set up Ruby
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration
-1
View File
@@ -113,7 +113,6 @@ jobs:
matrix:
include:
- language: actions
- language: python
permissions:
contents: read
+4 -4
View File
@@ -51,9 +51,9 @@ jobs:
with:
node-version: 24
cache: 'npm'
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Install JavaScript dependencies
run: npm ci
- name: Update git config
run: |
@@ -127,7 +127,7 @@ jobs:
env:
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
run: |
python .github/workflows/script/prepare_changelog.py CHANGELOG.md > $PARTIAL_CHANGELOG
npx tsx pr-checks/prepare-changelog.ts --output="$PARTIAL_CHANGELOG"
echo "::group::Partial CHANGELOG"
cat $PARTIAL_CHANGELOG
+4 -2
View File
@@ -93,7 +93,7 @@ jobs:
LATEST_TAG: ${{ needs.prepare.outputs.latest_tag }}
VERSION: "${{ needs.prepare.outputs.version }}"
run: |
python .github/workflows/script/rollback_changelog.py \
npx tsx pr-checks/rollback-changelog.ts \
--target-version "${ROLLBACK_TAG:1}" \
--rollback-version "${LATEST_TAG:1}" \
--new-version "$VERSION" > $NEW_CHANGELOG
@@ -128,7 +128,9 @@ jobs:
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
run: |
python .github/workflows/script/prepare_changelog.py $NEW_CHANGELOG > $PARTIAL_CHANGELOG
npx tsx pr-checks/prepare-changelog.ts \
--changelog="$NEW_CHANGELOG" \
--output="$PARTIAL_CHANGELOG"
echo "::group::Partial CHANGELOG"
cat $PARTIAL_CHANGELOG
@@ -1,23 +0,0 @@
#!/usr/bin/env python3
import os
import re
cli_version = os.environ['CLI_VERSION']
# The GitHub Release for the new bundle version.
bundle_release_url = f"https://github.com/github/codeql-action/releases/tag/codeql-bundle-v{cli_version}"
# 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 [{cli_version}]({bundle_release_url}). [#{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)
@@ -1,35 +0,0 @@
#!/usr/bin/env python3
import os
import sys
EMPTY_CHANGELOG = 'No changes.\n\n'
# Prepare the changelog for the new release
# This function will extract the part of the changelog that
# we want to include in the new release.
def extract_changelog_snippet(changelog_file):
output = ''
if (not os.path.exists(changelog_file)):
output = EMPTY_CHANGELOG
else:
with open(changelog_file, 'r') as f:
lines = f.readlines()
# Include only the contents of the first section
found_first_section = False
for line in lines:
if line.startswith('## '):
if found_first_section:
break
found_first_section = True
elif found_first_section:
output += line
return output.strip()
if len(sys.argv) < 2:
raise Exception('Expecting argument: changelog_file')
changelog_file = sys.argv[1]
print(extract_changelog_snippet(changelog_file))
@@ -1,62 +0,0 @@
import datetime
import os
import argparse
EMPTY_CHANGELOG = """# CodeQL Action Changelog
"""
def get_today_string():
today = datetime.datetime.today()
return '{:%d %b %Y}'.format(today)
# Include everything up to and after the first heading,
# but not the first heading and body.
def drop_unreleased_section(lines: list[str]):
before_first_section = ''
after_first_section = ''
found_first_section = False
skipped_first_section = False
for i, line in enumerate(lines):
if line.startswith('## ') and not found_first_section:
found_first_section = True
elif line.startswith('## ') and found_first_section:
skipped_first_section = True
if not found_first_section:
before_first_section += line
if skipped_first_section:
after_first_section += line
return (before_first_section, after_first_section)
def update_changelog(target_version, rollback_version, new_version):
before_first_section = EMPTY_CHANGELOG
after_first_section = ''
if (os.path.exists('CHANGELOG.md')):
with open('CHANGELOG.md', 'r') as f:
(before_first_section, after_first_section) = drop_unreleased_section(f.readlines())
newHeader = f'## {new_version} - {get_today_string()}\n'
print(before_first_section, end="")
print(newHeader)
print(f"This release rolls back {rollback_version} due to issues with that release. It is identical to {target_version}.\n")
print(after_first_section)
# We expect three version strings as input:
#
# - target_version: the version that we are re-releasing as `new_version`
# - rollback_version: the version that we are rolling back, typically the one that followed `target_version`
# - new_version: the new version that we are releasing `target_version` as, typically the one that follows `rollback_version`
#
# Example: python3 .github/workflows/script/rollback_changelog.py --target-version "1.2.3" --rollback-version "1.2.4" --new-version "1.2.5"
parser = argparse.ArgumentParser(description="Update CHANGELOG.md for a rollback release.")
parser.add_argument("--target-version", "-t", required=True, help="Version to re-release as new_version.")
parser.add_argument("--rollback-version", "-r", required=True, help="Version being rolled back.")
parser.add_argument("--new-version", "-n", required=True, help="New version to publish for target_version.")
args = parser.parse_args()
update_changelog(args.target_version, args.rollback_version, args.new_version)
+1 -6
View File
@@ -40,11 +40,6 @@ 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@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -120,7 +115,7 @@ jobs:
- name: Create changelog note
run: |
python .github/workflows/script/bundle_changelog.py
npx tsx pr-checks/bundle-changelog.ts
- name: Push changelog note
run: |
@@ -22,11 +22,6 @@ jobs:
pull-requests: write # needed to create pull request
steps:
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Checkout CodeQL Action
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -43,7 +38,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: github/enterprise-releases
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
token: ${{ secrets.CODEQL_CI_ENTERPRISE_RELEASE_PAT }}
path: ${{ github.workspace }}/enterprise-releases/
sparse-checkout: releases.json
+13
View File
@@ -4,7 +4,20 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
No user facing changes.
## 4.37.6 - 04 Aug 2026
- Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to `.github/codeql-config.yml` to align it with the suggested path that is used elsewhere. [#4070](https://github.com/github/codeql-action/pull/4070)
## 4.37.5 - 03 Aug 2026
- Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the `init` Action instead of falling back to downloading the bundle before extracting it. [#4061](https://github.com/github/codeql-action/pull/4061)
## 4.37.4 - 29 Jul 2026
- This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037)
- Update default CodeQL bundle version to [2.26.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.2). [#4051](https://github.com/github/codeql-action/pull/4051)
## 4.37.3 - 22 Jul 2026
+4 -4
View File
@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.26.1",
"cliVersion": "2.26.1",
"priorBundleVersion": "codeql-bundle-v2.26.0",
"priorCliVersion": "2.26.0"
"bundleVersion": "codeql-bundle-v2.26.2",
"cliVersion": "2.26.2",
"priorBundleVersion": "codeql-bundle-v2.26.1",
"priorCliVersion": "2.26.1"
}
+979 -660
View File
File diff suppressed because it is too large Load Diff
+102 -102
View File
@@ -1,12 +1,12 @@
{
"name": "codeql",
"version": "4.37.4",
"version": "4.37.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeql",
"version": "4.37.4",
"version": "4.37.7",
"license": "MIT",
"workspaces": [
"pr-checks"
@@ -31,7 +31,7 @@
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.2.1",
"js-yaml": "^5.2.2",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
@@ -61,11 +61,11 @@
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.7.0",
"globals": "^17.8.0",
"nock": "^14.0.16",
"sinon": "^22.0.0",
"sinon": "^22.1.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.64.0"
"typescript-eslint": "^8.65.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -374,9 +374,9 @@
"license": "Apache-2.0"
},
"node_modules/@actions/artifact/node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -2591,17 +2591,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/type-utils": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/type-utils": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -2614,7 +2614,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.64.0",
"@typescript-eslint/parser": "^8.65.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -2630,16 +2630,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3"
},
"engines": {
@@ -2673,14 +2673,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.64.0",
"@typescript-eslint/types": "^8.64.0",
"@typescript-eslint/tsconfig-utils": "^8.65.0",
"@typescript-eslint/types": "^8.65.0",
"debug": "^4.4.3"
},
"engines": {
@@ -2713,14 +2713,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0"
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2731,9 +2731,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2748,15 +2748,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -2791,9 +2791,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2805,16 +2805,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.64.0",
"@typescript-eslint/tsconfig-utils": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/visitor-keys": "8.64.0",
"@typescript-eslint/project-service": "8.65.0",
"@typescript-eslint/tsconfig-utils": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/visitor-keys": "8.65.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2843,16 +2843,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
@@ -2874,13 +2874,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^5.0.8"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -2890,16 +2890,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.64.0",
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0"
"@typescript-eslint/scope-manager": "8.65.0",
"@typescript-eslint/types": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2914,13 +2914,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.64.0",
"@typescript-eslint/types": "8.65.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -3864,9 +3864,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -5115,16 +5115,16 @@
}
},
"node_modules/eslint-plugin-import-x/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/eslint-plugin-import-x/node_modules/minimatch": {
@@ -6111,15 +6111,15 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/glob/node_modules/minimatch": {
@@ -6138,9 +6138,9 @@
}
},
"node_modules/globals": {
"version": "17.7.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz",
"integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
"version": "17.8.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -6981,9 +6981,9 @@
}
},
"node_modules/js-yaml": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz",
"integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==",
"funding": [
{
"type": "github",
@@ -8090,15 +8090,15 @@
}
},
"node_modules/readdir-glob/node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"node": "20 || >=22"
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
@@ -8556,9 +8556,9 @@
}
},
"node_modules/sinon": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz",
"integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==",
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-22.1.0.tgz",
"integrity": "sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -9320,16 +9320,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.64.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
"integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.64.0",
"@typescript-eslint/parser": "8.64.0",
"@typescript-eslint/typescript-estree": "8.64.0",
"@typescript-eslint/utils": "8.64.0"
"@typescript-eslint/eslint-plugin": "8.65.0",
"@typescript-eslint/parser": "8.65.0",
"@typescript-eslint/typescript-estree": "8.65.0",
"@typescript-eslint/utils": "8.65.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.37.4",
"version": "4.37.7",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -39,7 +39,7 @@
"follow-redirects": "^1.16.0",
"get-folder-size": "^5.0.0",
"https-proxy-agent": "^7.0.6",
"js-yaml": "^5.2.1",
"js-yaml": "^5.2.2",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
@@ -69,11 +69,11 @@
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.7.0",
"globals": "^17.8.0",
"nock": "^14.0.16",
"sinon": "^22.0.0",
"sinon": "^22.1.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.64.0"
"typescript-eslint": "^8.65.0"
},
"overrides": {
"@actions/tool-cache": {
+142
View File
@@ -0,0 +1,142 @@
/**
* Tests for `bundle-changelog.ts`.
*/
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, it } from "node:test";
import {
CLI_VERSION_ENV_VAR,
getCLIVersion,
getPRNumber,
getPRUrl,
PR_URL_ENV_VAR,
updateChangelog,
} from "./bundle-changelog";
import {
EMPTY_CHANGELOG,
NO_CHANGES_STR,
UNRELEASED_PLACEHOLDER,
} from "./changelog";
let testDir: string;
beforeEach(() => {
// Set up a temporary directory for testing
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-changelog-test-"));
});
afterEach(() => {
/** Clean up temporary directories. */
fs.rmSync(testDir, { recursive: true, force: true });
});
describe("getCLIVersion", async () => {
await it("throws if the environment variable is not set", async () => {
delete process.env[CLI_VERSION_ENV_VAR];
assert.throws(() => getCLIVersion());
});
await it("throws if the environment variable is empty", async () => {
process.env[CLI_VERSION_ENV_VAR] = " ";
assert.throws(() => getCLIVersion());
});
await it("returns value of the environment variable if set", async () => {
const testValue = "1.2.3";
process.env[CLI_VERSION_ENV_VAR] = testValue;
assert.deepEqual(getCLIVersion(), testValue);
});
});
const testPrUrl = "https://github.com/github/codeql-action/pulls/42";
describe("getPRUrl", async () => {
await it("throws if the environment variable is not set", async () => {
delete process.env[PR_URL_ENV_VAR];
assert.throws(() => getPRUrl());
});
await it("throws if the environment variable is empty", async () => {
process.env[PR_URL_ENV_VAR] = " ";
assert.throws(() => getPRUrl());
});
await it("returns value of the environment variable if set", async () => {
process.env[PR_URL_ENV_VAR] = testPrUrl;
assert.deepEqual(getPRUrl(), testPrUrl);
});
});
describe("getPRNumber", async () => {
await it("throws if the last part of the input is not a number", async () => {
assert.throws(() => getPRNumber(`${testPrUrl}/foo`));
});
await it("throws if the last part of the input is not a positive number", async () => {
assert.throws(() => getPRNumber(`${testPrUrl}/-100`));
});
await it("returns the PR number from an URL", async () => {
assert.equal(getPRNumber(testPrUrl), 42);
});
});
const testChangelog = `${EMPTY_CHANGELOG.trimEnd()}
## 4.23.7
- Other change
## 4.23.6
${NO_CHANGES_STR}`;
const expectedChangelog = `# CodeQL Action Changelog
## ${UNRELEASED_PLACEHOLDER}
- Update default CodeQL bundle version to
## 4.23.7
- Other change
## 4.23.6
${NO_CHANGES_STR}`;
describe("updateChangelog", async () => {
await it("removes `NO_CHANGES_STR` if present in [UNRELEASED] section", async () => {
const result = updateChangelog(EMPTY_CHANGELOG, "");
assert.ok(!result.includes(NO_CHANGES_STR.trim()));
});
await it("doesn't remove `NO_CHANGES_STR` if present in versioned section", async () => {
const result = updateChangelog(
EMPTY_CHANGELOG.replace(UNRELEASED_PLACEHOLDER, "1.2.3"),
"",
);
assert.ok(result.includes(NO_CHANGES_STR.trim()));
});
await it("throws if there are no sections", async () => {
assert.throws(() => {
updateChangelog(
"# CodeQL Action Changelog",
"- Update default CodeQL bundle version to",
);
});
});
await it("adds note at the end of the first section", async () => {
const result = updateChangelog(
testChangelog,
"- Update default CodeQL bundle version to",
);
assert.deepEqual(result, expectedChangelog);
});
});
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env npx tsx
/**
* Updates the changelog with a change note for an updated CodeQL CLI bundle.
*/
import * as fs from "node:fs";
import {
parseChangelog,
renderChangelog,
UNRELEASED_PLACEHOLDER,
} from "./changelog";
import { CHANGELOG_FILE, CLI_BUNDLE_RELEASE_URL_PREFIX } from "./config";
import { getErrorMessage } from "./util";
export const CLI_VERSION_ENV_VAR = "CLI_VERSION";
export const PR_URL_ENV_VAR = "PR_URL";
/** Gets the CLI version from the environment. */
export function getCLIVersion() {
const cliVersion = process.env[CLI_VERSION_ENV_VAR];
if (cliVersion === undefined || cliVersion.trim() === "") {
throw new Error(`No CLI version was set in '${CLI_VERSION_ENV_VAR}'.`);
}
return cliVersion;
}
/** Gets the PR URL from the environment. */
export function getPRUrl() {
const prUrl = process.env[PR_URL_ENV_VAR];
if (prUrl === undefined || prUrl.trim() === "") {
throw new Error(`No PR URL was set in '${PR_URL_ENV_VAR}'.`);
}
return prUrl;
}
/**
* Gets the PR number from something like a PR URL.
*/
export function getPRNumber(prUrl: string) {
const prUrlParts = prUrl.split("/");
const prNumberStr = prUrlParts[prUrlParts.length - 1];
const prNumber = Number.parseInt(prNumberStr, 10);
if (!Number.isInteger(prNumber) || prNumber <= 0) {
throw new Error(
`Invalid PR URL '${prUrl}': last part is not a positive number`,
);
}
return prNumber;
}
/**
* Updates `changelog` by adding `changelogNote` to the first section.
*
* @param contents The existing changelog contents.
* @param changelogNote The note to add to the first section.
*/
export function updateChangelog(contents: string, changelogNote: string) {
// If the "[UNRELEASED]" section starts with "no user facing changes", remove that line.
contents = contents.replace(
`## ${UNRELEASED_PLACEHOLDER}\n\nNo user facing changes.`,
`## ${UNRELEASED_PLACEHOLDER}\n`,
);
const changelog = parseChangelog(contents);
if (changelog.sections.length === 0) {
throw new Error("The changelog contains no existing sections.");
}
// Add the changelog note to the bottom of the first section.
const firstSection = changelog.sections[0];
const lastLine = firstSection.bodyLines.pop();
if (lastLine !== undefined && lastLine.trim() !== "") {
// We expect the last line to be empty. If it isn't for some reason,
// add it back.
firstSection.bodyLines.push(lastLine);
}
firstSection.bodyLines.push(changelogNote);
// If the last line is empty as expected, then add it back in after the new note.
if (lastLine?.trim() === "") {
firstSection.bodyLines.push(lastLine);
}
return renderChangelog(changelog);
}
function main() {
try {
const cliVersion = getCLIVersion();
const prUrl = getPRUrl();
// The GitHub Release for the new bundle version.
const bundleReleaseUrl = `${CLI_BUNDLE_RELEASE_URL_PREFIX}${cliVersion}`;
// Get the PR number from the PR URL.
const prNumber = getPRNumber(prUrl);
const changelogNote = `- Update default CodeQL bundle version to [${cliVersion}](${bundleReleaseUrl}). [#${prNumber}](${prUrl})`;
let changelog = fs.readFileSync(CHANGELOG_FILE, "utf-8");
changelog = updateChangelog(changelog, changelogNote);
fs.writeFileSync(CHANGELOG_FILE, changelog);
return 0;
} catch (err) {
console.error(`Failed to bundle changelog: ${getErrorMessage(err)}`);
return -1;
}
}
// Only call `main` if this script was run directly.
if (require.main === module) {
process.exit(main());
}
+12
View File
@@ -5,14 +5,18 @@
*/
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import { describe, it } from "node:test";
import {
EMPTY_CHANGELOG,
getReleaseDateString,
parseChangelog,
processChangelogForBackports,
renderChangelog,
setVersionAndDate,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";
const testDate = new Date(2026, 7, 14);
@@ -37,6 +41,14 @@ describe("setVersionAndDate", async () => {
});
});
describe("parseChangelog + renderChangelog", async () => {
await it("renderChangelog(parseChangelog(c)) == c", async () => {
const actualChangelog = fs.readFileSync(CHANGELOG_FILE, "utf-8");
const roundtrip = renderChangelog(parseChangelog(actualChangelog));
assert.deepEqual(roundtrip.split("\n"), actualChangelog.split("\n"));
});
});
const testChangelog = `# CodeQL Action Changelog
## 4.12.3 - 14 Aug 2026
+125 -48
View File
@@ -2,14 +2,34 @@ import * as fs from "node:fs";
import { CHANGELOG_FILE, DryRunOption } from "./config";
/** The placeholder in the header for unreleased changes. */
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
/** The default contents for a section in the changelog. */
export const NO_CHANGES_STR = "No user facing changes.\n\n";
/** Placeholder changelog content for a new release. */
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
## [UNRELEASED]
## ${UNRELEASED_PLACEHOLDER}
No user facing changes.
${NO_CHANGES_STR}`;
`;
/**
* Represents sections in a changelog.
*/
export interface ChangelogSection {
headerLine: string;
bodyLines: string[];
}
/**
* Represents a changelog.
*/
export interface Changelog {
preamble: string[];
sections: ChangelogSection[];
}
/** Returns `date` formatted as `DD Mon YYYY`. */
export function getReleaseDateString(today: Date = new Date()): string {
@@ -53,7 +73,76 @@ export function setVersionAndDate(
date: Date = new Date(),
): string {
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
return content.replace("[UNRELEASED]", versionAndDate);
return content.replace(UNRELEASED_PLACEHOLDER, versionAndDate);
}
/**
* Parses `content` into a structured representation of a changelog.
*
* @param content The contents of the changelog file.
*/
export function parseChangelog(content: string): Changelog {
const lines = content.split("\n");
let i = 0;
const preamble: string[] = [];
const sections: ChangelogSection[] = [];
let currentSection: ChangelogSection | undefined = undefined;
// Process all lines of the input file.
while (i < lines.length) {
const line = lines[i];
// Sections of the changelog start with `## `.
if (line.startsWith("## ")) {
// We have discovered a new section. If `currentSection` is already defined,
// then this marks the end of that section. Push it to the array of sections
// in the changelog.
if (currentSection !== undefined) {
sections.push(currentSection);
}
// Initialise the new section.
currentSection = { headerLine: line, bodyLines: [] };
} else if (currentSection !== undefined) {
// Add lines between the section header and the next to the current section.
currentSection.bodyLines.push(line);
} else {
// This is neither a section header nor are we in a section already,
// so this line is part of the preamble.
preamble.push(line);
}
i++;
}
// Push the current section to the array of completed sections, if there is
// still one unfinished.
if (currentSection !== undefined) {
sections.push(currentSection);
}
return { preamble, sections };
}
/**
* Combines an array of lines into a single string by adding line breaks.
*/
export function unlines(lines: string[]): string {
return `${lines.join("\n")}`;
}
/**
* Renders a given changelog to a string.
*/
export function renderChangelog(changelog: Changelog): string {
let result = unlines(changelog.preamble);
for (const section of changelog.sections) {
result += `\n${section.headerLine}\n${unlines(section.bodyLines)}`;
}
return result;
}
/**
@@ -66,70 +155,58 @@ export function processChangelogForBackports(
targetBranchMajorVersion: string,
content: string,
): string {
const lines = content.split("\n");
// Changelog entries can use the following format to indicate
// that they only apply to newer versions
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
let output = "";
let i = 0;
// Parse the changelog.
const changelog = parseChangelog(content);
// Copy lines until we find the first section heading.
let foundFirstSection = false;
while (!foundFirstSection && i < lines.length) {
let line = lines[i];
if (line.startsWith("## ")) {
line = line.replace(
`## ${sourceBranchMajorVersion}`,
`## ${targetBranchMajorVersion}`,
);
foundFirstSection = true;
}
output += `${line}\n`;
i++;
}
if (!foundFirstSection) {
if (changelog.sections.length === 0) {
throw new Error("Could not find any change sections in CHANGELOG.md");
}
// Process remaining lines.
// `foundContent` tracks whether we hit two headings in a row
let foundContent = false;
output += "\n";
// Filter out changelog entries that only apply to newer versions and
// update the section headings with the backport major version for
// sections we keep.
for (const section of changelog.sections) {
// Update the section headings with the backport major version.
section.headerLine = section.headerLine.replace(
`## ${sourceBranchMajorVersion}`,
`## ${targetBranchMajorVersion}`,
);
while (i < lines.length) {
let line = lines[i];
i++;
const filteredEntries: string[] = [];
let foundContent = false;
// Filter out changelog entries that only apply to newer versions.
const match = someVersionsOnlyRegex.exec(line);
if (match) {
for (const line of section.bodyLines) {
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
// of the target branch is smaller than the required version.
const match = someVersionsOnlyRegex.exec(line);
if (
match &&
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
) {
continue;
}
}
if (line.startsWith("## ")) {
line = line.replace(
`## ${sourceBranchMajorVersion}`,
`## ${targetBranchMajorVersion}`,
);
if (!foundContent) {
output += "No user facing changes.\n";
}
foundContent = false;
output += `\n${line}\n\n`;
} else {
// Keep the line.
filteredEntries.push(line);
// Set `foundContent` to `true` if the line is not empty.
if (line.trim() !== "") {
foundContent = true;
output += `${line}\n`;
}
}
// Update the section with the retained entries.
section.bodyLines = filteredEntries;
// Add an entry if we didn't keep any.
if (!foundContent) {
section.bodyLines.push(NO_CHANGES_STR.trim());
}
}
return output;
return renderChangelog(changelog);
}
+2 -2
View File
@@ -21,8 +21,8 @@ steps:
run: |
cd "$RUNNER_TEMP/results"
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$JOB_RUN_UUID', but found '$actual'."
if [[ "$actual" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
exit 1
else
echo "Found job run UUID '$actual'."
+1 -1
View File
@@ -5,7 +5,7 @@ versions:
- default
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration
+4
View File
@@ -37,6 +37,10 @@ export const API_COMPATIBILITY_FILE = path.join(
"api-compatibility.json",
);
/** The prefix of CodeQL CLI bundle release URLs. */
export const CLI_BUNDLE_RELEASE_URL_PREFIX =
"https://github.com/github/codeql-action/releases/tag/codeql-bundle-v";
/** A common interface for operations that support dry runs. */
export interface DryRunOption {
/** A value indicating whether to perform operations with side effects. */
+54
View File
@@ -0,0 +1,54 @@
/**
* Tests for `prepare-changelog.ts`.
*/
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, it } from "node:test";
import { EMPTY_CHANGELOG, NO_CHANGES_STR } from "./changelog";
import { extractChangelogSnippet } from "./prepare-changelog";
let testDir: string;
beforeEach(() => {
// Set up a temporary directory for testing
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "prepare-changelog-test-"));
});
afterEach(() => {
/** Clean up temporary directories. */
fs.rmSync(testDir, { recursive: true, force: true });
});
const testBody = `- Test change`;
const testChangelog = `${EMPTY_CHANGELOG.replace(NO_CHANGES_STR, testBody)}
## Another section
- Other change`;
describe("extractChangelogSnippet", async () => {
await it("returns the default body if the input doesn't exist", async () => {
const result = extractChangelogSnippet(path.join(testDir, "not-here.md"));
assert.deepEqual(result, NO_CHANGES_STR);
});
await it("returns the first section if the input exists", async () => {
const changelogPath = path.join(testDir, "test-readme.md");
fs.writeFileSync(changelogPath, testChangelog);
const result = extractChangelogSnippet(changelogPath);
assert.deepEqual(result, testBody);
});
await it("returns an empty string if there is no first section", async () => {
const changelogPath = path.join(testDir, "test-readme.md");
fs.writeFileSync(changelogPath, "# CodeQL Action Changelog\n");
const result = extractChangelogSnippet(changelogPath);
assert.deepEqual(result, "");
});
});
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env npx tsx
/**
* Extracts the body of the first changelog section and outputs it to either
* stdout or a file.
*/
import * as fs from "node:fs";
import { parseArgs } from "node:util";
import { NO_CHANGES_STR, parseChangelog } from "./changelog";
import { CHANGELOG_FILE } from "./config";
import { getErrorMessage } from "./util";
/**
* Prepare the changelog for the new release
* This function will extract the part of the changelog that
* we want to include in the new release.
*
* @param changelogPath The path to the changelog file.
*/
export function extractChangelogSnippet(changelogPath: string) {
try {
const content = fs.readFileSync(changelogPath, "utf-8");
const changelog = parseChangelog(content);
// Return an empty string if we couldn't find the first section.
if (changelog.sections.length === 0) {
return "";
}
return changelog.sections[0].bodyLines.join("\n").trim();
} catch (err) {
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
console.error(`Changelog file at '${changelogPath}' does not exist.`);
return NO_CHANGES_STR;
} else {
throw Error(
`Failed to open changelog file at '${changelogPath}': ${getErrorMessage(err)}`,
);
}
}
}
function main() {
try {
const { values } = parseArgs({
options: {
changelog: {
type: "string",
short: "f",
default: CHANGELOG_FILE,
},
output: {
type: "string",
short: "o",
},
},
strict: true,
});
const body = extractChangelogSnippet(values.changelog);
// If no `output` argument was provided, output to stdout. Otherwise,
// write a file to the specified path.
if (values.output === undefined) {
console.info(body);
} else {
fs.writeFileSync(values.output, body);
}
return 0;
} catch (err) {
console.error(`Failed to prepare changelog: ${getErrorMessage(err)}`);
return -1;
}
}
// Only call `main` if this script was run directly.
if (require.main === module) {
process.exit(main());
}
+45
View File
@@ -0,0 +1,45 @@
/**
* Tests for `rollback-changelog.ts`.
*/
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import { describe, it } from "node:test";
import { getReleaseDateString, parseChangelog } from "./changelog";
import { CHANGELOG_FILE } from "./config";
import { updateChangelog } from "./rollback-changelog";
describe("updateChangelog", async () => {
await it("replaces the first section with one for the rollback release", async () => {
const actualChangelog = parseChangelog(
fs.readFileSync(CHANGELOG_FILE, "utf-8"),
);
const existingFirstSection = actualChangelog.sections[0];
const today = new Date();
updateChangelog(actualChangelog, {
"new-version": "Test.1.3",
"rollback-version": "Test.1.2",
"target-version": "Test.1.1",
today,
});
// Check that the old, first section is gone.
for (const section of actualChangelog.sections) {
assert.notDeepEqual(section, existingFirstSection);
}
// Check that the new, first section matches our expectations.
const newFirstSection = actualChangelog.sections[0];
assert.deepEqual(
newFirstSection.headerLine,
`## Test.1.3 - ${getReleaseDateString(today)}`,
);
assert.equal(newFirstSection.bodyLines.length, 3);
assert.deepEqual(
newFirstSection.bodyLines[1],
`This release rolls back Test.1.2 due to issues with that release. It is identical to Test.1.1.`,
);
});
});
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env npx tsx
/**
* Replaces the current, first section of the changelog with a new one for the rollback release.
*/
import * as fs from "node:fs";
import { parseArgs } from "node:util";
import {
Changelog,
ChangelogSection,
getReleaseDateString,
parseChangelog,
renderChangelog,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";
import { getErrorMessage } from "./util";
export interface RollbackChangelogInputs {
"target-version": string;
"rollback-version": string;
"new-version": string;
today?: Date;
}
/**
* Replaces the current, first section of the changelog with a new one for the rollback release.
*/
export function updateChangelog(
changelog: Changelog,
versions: RollbackChangelogInputs,
) {
// Drop the existing first section.
changelog.sections.shift();
// Construct the section for the rollback version.
const newSection: ChangelogSection = {
headerLine: `## ${versions["new-version"]} - ${getReleaseDateString(versions.today)}`,
bodyLines: [
"",
`This release rolls back ${versions["rollback-version"]} due to issues with that release. It is identical to ${versions["target-version"]}.`,
"",
],
};
// Add the new section at the top of the changelog.
changelog.sections.unshift(newSection);
}
function main() {
try {
const options = {
"target-version": { type: "string", short: "t" },
"rollback-version": { type: "string", short: "r" },
"new-version": { type: "string", short: "n" },
} as const;
const { values } = parseArgs({ options, strict: true });
for (const key of Object.keys(options)) {
const val = values[key as keyof typeof values];
if (val === undefined || val.trim() === "") {
throw new Error(`Argument '--${key}' is required.`);
}
}
const changelog = parseChangelog(fs.readFileSync(CHANGELOG_FILE, "utf-8"));
updateChangelog(changelog, values as RollbackChangelogInputs);
console.info(renderChangelog(changelog));
return 0;
} catch (err) {
console.error(
`Failed to prepare rollback changelog: ${getErrorMessage(err)}`,
);
return -1;
}
}
// Only call `main` if this script was run directly.
if (require.main === module) {
process.exit(main());
}
+2 -2
View File
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
name: "Install Java",
uses: pinnedUses(
"actions/setup-java",
"03ad4de0992f5dab5e18fcb136590ce7c4a0ac95",
"v5.6.0",
"b6effb05e454b25005698d916606bdc6ffcbf961",
"v5.7.0",
),
with: {
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
+9
View File
@@ -0,0 +1,9 @@
/**
* Returns an appropriate message for the error.
*
* If the error is an `Error` instance, this returns the error message without
* an `Error: ` prefix.
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
+123
View File
@@ -0,0 +1,123 @@
import * as core from "@actions/core";
import test from "ava";
import sinon from "sinon";
import * as common from "./action-common";
import * as actionsUtil from "./actions-util";
import * as environment from "./environment";
import * as logging from "./logging";
import { ActionName } from "./status-report";
import * as statusReport from "./status-report";
import {
getTestActionsEnv,
getTestEnv,
makeMacro,
RecordingLogger,
setupTests,
} from "./testing-utils";
import { getErrorMessage } from "./util";
setupTests(test);
interface RunInActionsTestOpts {
runFn?: () => Promise<any>;
expectedErrorMessage?: string;
expectedTelemetryError?: string;
}
const runInActionsMacro = makeMacro({
exec: async (t, opts: RunInActionsTestOpts) => {
const expectFailure = opts?.expectedErrorMessage !== undefined;
const logger = new RecordingLogger();
const getActionsLogger = sinon
.stub(logging, "getActionsLogger")
.returns(logger);
const env = getTestEnv();
const getEnv = sinon.stub(environment, "getEnv").returns(env);
const actionsEnv = getTestActionsEnv(env);
const getActionsEnv = sinon
.stub(actionsUtil, "getActionsEnv")
.returns(actionsEnv);
const getJobUUID = sinon
.stub(statusReport, "getJobUUID")
.returns("test-job-uuid");
const setFailed = sinon.stub(core, "setFailed");
const sendUnhandledErrorStatusReport = sinon.stub(
statusReport,
"sendUnhandledErrorStatusReport",
);
const name = ActionName.Init;
const run = sinon.stub();
if (opts?.runFn) {
run.callsFake(opts.runFn);
}
const transformTelemetryError = sinon
.stub()
.callsFake((err) => opts?.expectedTelemetryError ?? getErrorMessage(err));
const testAction: common.Action = {
name,
run,
transformTelemetryError,
};
await common.runInActions(testAction);
// These always should have been called once.
t.true(getActionsLogger.calledOnce);
t.true(getEnv.calledOnce);
t.true(getActionsEnv.calledOnce);
const expectedActionState = {
actions: actionsEnv,
env,
logger,
name: ActionName.Init,
};
t.true(getJobUUID.calledOnceWithExactly(sinon.match(expectedActionState)));
t.true(run.calledOnceWithExactly(sinon.match(expectedActionState)));
t.is(setFailed.calledOnce, expectFailure ?? false);
t.is(sendUnhandledErrorStatusReport.calledOnce, expectFailure ?? false);
if (expectFailure) {
t.true(
setFailed.calledOnceWithExactly(
`${statusReport.getDisplayActionName(name)} action failed: ${opts?.expectedErrorMessage}`,
),
);
t.true(
sendUnhandledErrorStatusReport.calledOnceWithExactly(
name,
sinon.match.any,
opts?.expectedTelemetryError ?? opts?.expectedErrorMessage,
logger,
),
);
}
},
title: (providedTitle) => `runInActions - ${providedTitle}`,
});
runInActionsMacro.serial("calls run", {});
runInActionsMacro.serial("handles run exceptions", {
runFn: () => {
throw new Error("Test failure");
},
expectedErrorMessage: "Test failure",
});
runInActionsMacro.serial("transforms run exceptions", {
runFn: () => {
throw new Error("Test failure");
},
expectedErrorMessage: "Test failure",
expectedTelemetryError: "Transformed failure message",
});
+26 -4
View File
@@ -8,9 +8,10 @@ import { getActionsLogger, Logger } from "./logging";
import {
ActionName,
getDisplayActionName,
getJobUUID,
sendUnhandledErrorStatusReport,
} from "./status-report";
import { getEnv, getErrorMessage } from "./util";
import { getEnv, getErrorMessage, wrapError } from "./util";
/** Base state that is available to an Action on startup. */
export interface BaseState {
@@ -78,6 +79,12 @@ export interface Action {
name: ActionName;
/** The entry point for the Action. */
run: ActionMain;
/**
* An optional function that transforms a caught error into a message suitable for
* inclusion in a status report. This is primarily intended for the `start-proxy`
* action to replace the thrown `Error`'s message with a safe one.
*/
transformTelemetryError?: (error: Error) => string;
}
/** A generic entry point that sets up the basic environment for the `action` and runs it. */
@@ -88,17 +95,32 @@ export async function runInActions(action: Action) {
const actionsEnv = getActionsEnv();
try {
await action.run({
const actionState = {
name: action.name,
startedAt,
logger,
env,
actions: actionsEnv,
});
};
// Create a unique identifier for this run.
getJobUUID(actionState);
await action.run(actionState);
} catch (error) {
core.setFailed(
`${getDisplayActionName(action.name)} action failed: ${getErrorMessage(error)}`,
);
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
const statusReportError =
action.transformTelemetryError !== undefined
? action.transformTelemetryError(wrapError(error))
: error;
await sendUnhandledErrorStatusReport(
action.name,
startedAt,
statusReportError,
logger,
);
}
}
+7 -1
View File
@@ -27,14 +27,20 @@ declare const __CODEQL_ACTION_VERSION__: string;
* global functions in tests.
*/
export interface ActionsEnv {
getRequiredInput: (name: string) => string;
getOptionalInput: (name: string) => string | undefined;
exportVariable: (name: string, value: string) => void;
}
/**
* Gets the real `ActionsEnv` used by production code.
*/
export function getActionsEnv(): ActionsEnv {
return { getOptionalInput };
return {
getRequiredInput,
getOptionalInput,
exportVariable: core.exportVariable,
};
}
/**
+98 -24
View File
@@ -1295,33 +1295,12 @@ checkOverlayEnablementMacro.serial(
);
checkOverlayEnablementMacro.serial(
"No overlay-base database on default branch if runner disk space is below v2 limit and v2 resource checks enabled",
"Overlay-base database on default branch if runner disk space is above the default limit",
{
languages: [BuiltInLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
Feature.OverlayAnalysisResourceChecksV2,
],
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: 5_000_000_000,
numTotalBytes: 100_000_000_000,
},
},
{
disabledReason: OverlayDisabledReason.InsufficientDiskSpace,
},
);
checkOverlayEnablementMacro.serial(
"Overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks enabled",
{
languages: [BuiltInLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
Feature.OverlayAnalysisResourceChecksV2,
],
isDefaultBranch: true,
diskUsage: {
@@ -1336,7 +1315,7 @@ checkOverlayEnablementMacro.serial(
);
checkOverlayEnablementMacro.serial(
"No overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks not enabled",
"No overlay-base database on default branch if runner disk space is below the default limit",
{
languages: [BuiltInLanguage.javascript],
features: [
@@ -1345,7 +1324,102 @@ checkOverlayEnablementMacro.serial(
],
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: 15_000_000_000,
numAvailableBytes: 10_000_000_000,
numTotalBytes: 100_000_000_000,
},
},
{
disabledReason: OverlayDisabledReason.InsufficientDiskSpace,
},
);
// Check that each feature flag lowers the limit to the threshold that its name
// declares. Both sides of the boundary are needed to pin the threshold down: a
// mapping to a lower value would still pass the case at the limit, and one to a
// higher value would still fail the case below it.
for (const [feature, thresholdGb] of [
[Feature.OverlayAnalysisMinDisk8Gb, 8],
[Feature.OverlayAnalysisMinDisk9Gb, 9],
[Feature.OverlayAnalysisMinDisk10Gb, 10],
[Feature.OverlayAnalysisMinDisk11Gb, 11],
[Feature.OverlayAnalysisMinDisk12Gb, 12],
[Feature.OverlayAnalysisMinDisk13Gb, 13],
] as Array<[Feature, number]>) {
const features = [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
feature,
];
checkOverlayEnablementMacro.serial(
`Overlay-base database on default branch if ${feature} is enabled and runner disk space is at its limit`,
{
languages: [BuiltInLanguage.javascript],
features,
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: thresholdGb * 1_000_000_000,
numTotalBytes: 100_000_000_000,
},
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
checkOverlayEnablementMacro.serial(
`No overlay-base database on default branch if ${feature} is enabled and runner disk space is below its limit`,
{
languages: [BuiltInLanguage.javascript],
features,
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: thresholdGb * 1_000_000_000 - 1_000_000,
numTotalBytes: 100_000_000_000,
},
},
{
disabledReason: OverlayDisabledReason.InsufficientDiskSpace,
},
);
}
checkOverlayEnablementMacro.serial(
"Overlay-base database on default branch if runner disk space is exactly at the lowest limit enabled by a feature flag",
{
languages: [BuiltInLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
Feature.OverlayAnalysisMinDisk9Gb,
Feature.OverlayAnalysisMinDisk12Gb,
],
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: 9_000_000_000,
numTotalBytes: 100_000_000_000,
},
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
checkOverlayEnablementMacro.serial(
"No overlay-base database on default branch if runner disk space is below the lowest limit enabled by a feature flag",
{
languages: [BuiltInLanguage.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
Feature.OverlayAnalysisMinDisk9Gb,
Feature.OverlayAnalysisMinDisk12Gb,
],
isDefaultBranch: true,
diskUsage: {
numAvailableBytes: 8_500_000_000,
numTotalBytes: 100_000_000_000,
},
},
+49 -24
View File
@@ -48,7 +48,7 @@ import {
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import * as errorMessages from "./error-messages";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
import {
RepositoryProperties,
RepositoryPropertyName,
@@ -101,19 +101,28 @@ export { type Config } from "./config/action-config";
* whether to perform overlay analysis, then the action will not perform overlay
* analysis unless overlay analysis has been explicitly enabled via environment
* variable.
*
* This threshold can be lowered by the feature flags in
* `OVERLAY_MINIMUM_DISK_SPACE_FEATURES`.
*/
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 20000;
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES =
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB * 1_000_000;
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 14000;
/**
* The v2 minimum available disk space (in MB) required to perform overlay
* analysis. This is a lower threshold than the v1 limit, allowing overlay
* analysis to run on runners with less available disk space.
* Feature flags that lower the minimum available disk space required to perform
* overlay analysis, paired with the threshold (in MB) that each one enables.
*
* If several of these are enabled, the lowest threshold takes effect.
*/
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB = 14000;
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES =
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB * 1_000_000;
const OVERLAY_MINIMUM_DISK_SPACE_FEATURES: ReadonlyArray<
[FeatureWithoutCLI, number]
> = [
[Feature.OverlayAnalysisMinDisk8Gb, 8000],
[Feature.OverlayAnalysisMinDisk9Gb, 9000],
[Feature.OverlayAnalysisMinDisk10Gb, 10000],
[Feature.OverlayAnalysisMinDisk11Gb, 11000],
[Feature.OverlayAnalysisMinDisk12Gb, 12000],
[Feature.OverlayAnalysisMinDisk13Gb, 13000],
];
/**
* The minimum memory (in MB) that must be available for CodeQL to perform overlay analysis. If
@@ -588,24 +597,42 @@ async function checkOverlayAnalysisFeatureEnabled(
return new Success(undefined);
}
/**
* Returns the minimum available disk space (in MB) required to perform overlay
* analysis, which is the lowest threshold enabled by a feature flag, or the
* default threshold if no such feature flag is enabled.
*/
async function getMinimumDiskSpaceMb(
features: FeatureEnablement,
): Promise<number> {
let minimumMb = OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB;
for (const [feature, thresholdMb] of OVERLAY_MINIMUM_DISK_SPACE_FEATURES) {
if (await features.getValue(feature)) {
minimumMb = Math.min(minimumMb, thresholdMb);
}
}
return minimumMb;
}
/** Checks if the runner has enough disk space for overlay analysis. */
function runnerHasSufficientDiskSpace(
diskUsage: DiskUsage,
logger: Logger,
useV2ResourceChecks: boolean,
minimumDiskSpaceMb: number,
): boolean {
const minimumDiskSpaceBytes = useV2ResourceChecks
? OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES
: OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES;
if (diskUsage.numAvailableBytes < minimumDiskSpaceBytes) {
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
const minimumDiskSpaceMb = Math.round(minimumDiskSpaceBytes / 1_000_000);
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
if (diskUsage.numAvailableBytes < minimumDiskSpaceMb * 1_000_000) {
logger.info(
`Setting overlay database mode to ${OverlayDatabaseMode.None} ` +
`due to insufficient disk space (${diskSpaceMb} MB, needed ${minimumDiskSpaceMb} MB).`,
);
return false;
}
logger.debug(
`Disk space available for CodeQL analysis is ${diskSpaceMb} MB, which is at or above the ` +
`minimum of ${minimumDiskSpaceMb} MB.`,
);
return true;
}
@@ -637,7 +664,7 @@ async function runnerHasSufficientMemory(
}
logger.debug(
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is at or above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
);
return true;
}
@@ -648,12 +675,13 @@ async function runnerHasSufficientMemory(
*/
async function checkRunnerResources(
codeql: CodeQL,
features: FeatureEnablement,
diskUsage: DiskUsage,
ramInput: string | undefined,
logger: Logger,
useV2ResourceChecks: boolean,
): Promise<Result<void, OverlayDisabledReason>> {
if (!runnerHasSufficientDiskSpace(diskUsage, logger, useV2ResourceChecks)) {
const minimumDiskSpaceMb = await getMinimumDiskSpaceMb(features);
if (!runnerHasSufficientDiskSpace(diskUsage, logger, minimumDiskSpaceMb)) {
return new Failure(OverlayDisabledReason.InsufficientDiskSpace);
}
if (!(await runnerHasSufficientMemory(codeql, ramInput, logger))) {
@@ -752,9 +780,6 @@ export async function checkOverlayEnablement(
Feature.OverlayAnalysisSkipResourceChecks,
codeql,
));
const useV2ResourceChecks = await features.getValue(
Feature.OverlayAnalysisResourceChecksV2,
);
const checkOverlayStatus = await features.getValue(
Feature.OverlayAnalysisStatusCheck,
);
@@ -770,10 +795,10 @@ export async function checkOverlayEnablement(
performResourceChecks && diskUsage !== undefined
? await checkRunnerResources(
codeql,
features,
diskUsage,
ramInput,
logger,
useV2ResourceChecks,
)
: new Success<void>(undefined);
if (resourceResult.isFailure()) {
+38 -5
View File
@@ -2,6 +2,7 @@ import * as github from "@actions/github";
import test from "ava";
import sinon from "sinon";
import { AnalysisKind } from "../analyses";
import * as api from "../api-client";
import { RegistryProxyVars } from "../environment";
import { Feature } from "../feature-flags";
@@ -18,7 +19,7 @@ setupTests(test);
test("getConfigFileInput returns undefined by default", async (t) => {
await callee(getConfigFileInput)
.withArgs({})
.withArgs({}, undefined)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.passes(t.is, undefined);
});
@@ -40,7 +41,7 @@ test("getConfigFileInput returns input value", async (t) => {
.withArgs("config-file")
.returns(testInput);
})
.withArgs(repositoryProperties)
.withArgs(repositoryProperties, undefined)
.logs(t, "Using configuration file input from workflow")
.passes(t.is, testInput);
});
@@ -49,16 +50,48 @@ test("getConfigFileInput returns repository property value", async (t) => {
// Since there is no direct input, we should use the repository property.
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs(repositoryProperties)
.withArgs(repositoryProperties, undefined)
.logs(t, "Using configuration file input from repository property")
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
});
test("getConfigFileInput returns repository property value for Code Scanning", async (t) => {
// Since there is no direct input, we should use the repository property.
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs(repositoryProperties, [AnalysisKind.CodeScanning])
.logs(t, "Using configuration file input from repository property")
.passes(t.is, repositoryProperties[RepositoryPropertyName.CONFIG_FILE]);
});
test("getConfigFileInput ignores repository property for other analysis kinds", async (t) => {
const unsupportedCases = [
[AnalysisKind.CodeQuality],
[AnalysisKind.RiskAssessment],
[AnalysisKind.CodeScanning, AnalysisKind.CodeQuality],
];
const target = callee(getConfigFileInput).withFeatures([
Feature.ConfigFileRepositoryProperty,
]);
for (const unsupportedCase of unsupportedCases) {
// Since the analysis kind is unsupported, we should ignore the repository property.
await target
.withArgs(repositoryProperties, unsupportedCase)
.logs(
t,
"Ignoring configuration file input from repository property, because it is unsupported for the current analysis kind.",
)
.passes(t.is, undefined);
}
});
test("getConfigFileInput ignores empty repository property value", async (t) => {
// Since the repository property value is an empty/whitespace string, we should ignore it.
await callee(getConfigFileInput)
.withFeatures([Feature.ConfigFileRepositoryProperty])
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " })
.withArgs({ [RepositoryPropertyName.CONFIG_FILE]: " " }, undefined)
.passes(t.is, undefined);
});
@@ -66,7 +99,7 @@ test("getConfigFileInput ignores repository property value when FF is off", asyn
// Since the FF is off, we should ignore the repository property value.
await callee(getConfigFileInput)
.withFeatures([])
.withArgs(repositoryProperties)
.withArgs(repositoryProperties, undefined)
.notLogs(t, "Using configuration file input from repository property")
.logs(
t,
+15 -1
View File
@@ -1,4 +1,5 @@
import { ActionState } from "../action-common";
import { AnalysisKind } from "../analyses";
import * as api from "../api-client";
import * as errorMessages from "../error-messages";
import { Feature } from "../feature-flags";
@@ -34,6 +35,7 @@ export async function getConfigFileInput(
features,
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
repositoryProperties: Partial<RepositoryProperties>,
analysisKinds: AnalysisKind[] | undefined,
): Promise<string | undefined> {
const input = actions.getOptionalInput("config-file");
@@ -45,17 +47,29 @@ export async function getConfigFileInput(
const propertyValue =
repositoryProperties[RepositoryPropertyName.CONFIG_FILE];
// Only allow the repository property to be used for standard Code Scanning analyses,
// since we don't currently support some customisation options for Code Quality.
// We don't expect customisations for Risk Assessments either.
const analysisKindSupported =
analysisKinds === undefined ||
(analysisKinds.includes(AnalysisKind.CodeScanning) &&
analysisKinds.length === 1);
if (propertyValue !== undefined && propertyValue.trim().length > 0) {
// Only use the repository property value if the FF is enabled.
const useRepositoryProperty = await features.getValue(
Feature.ConfigFileRepositoryProperty,
);
if (useRepositoryProperty) {
if (analysisKindSupported && useRepositoryProperty) {
logger.info(
`Using configuration file input from repository property: ${propertyValue}`,
);
return propertyValue;
} else if (!analysisKindSupported) {
logger.info(
"Ignoring configuration file input from repository property, because it is unsupported for the current analysis kind.",
);
} else {
logger.info(
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
+5 -13
View File
@@ -1,7 +1,7 @@
import test from "ava";
import sinon from "sinon";
import { getActionsEnv } from "../actions-util";
import { ActionsEnv } from "../actions-util";
import { Feature } from "../feature-flags";
import { RepositoryPropertyName } from "../feature-flags/properties";
import { callee } from "../testing-utils";
@@ -22,32 +22,26 @@ const expectedRepositoryPropertyResult: ComputedInput = {
value: "repo-property-input-value",
};
function stubGetToolsInput() {
const actions = getActionsEnv();
function stubGetToolsInput(actions: ActionsEnv) {
sinon
.stub(actions, "getOptionalInput")
.withArgs(InputName.Tools)
.returns(expectedWorkflowResult.value);
return actions;
}
const workflowLogMessage = `Using ${InputName.Tools} input from workflow:`;
test("getToolsInput - returns workflow input if available", async (t) => {
const actions = stubGetToolsInput();
await callee(getToolsInput)
.withActions(actions)
.withActions(stubGetToolsInput)
.withArgs({})
.logs(t, workflowLogMessage)
.passes(t.deepEqual, expectedWorkflowResult);
});
test("getToolsInput - returns repository property value if enforced", async (t) => {
const actions = stubGetToolsInput();
const target = callee(getToolsInput)
.withActions(actions)
.withActions(stubGetToolsInput)
.withArgs({
[RepositoryPropertyName.TOOLS]: `!${expectedRepositoryPropertyResult.value}`,
});
@@ -65,10 +59,8 @@ test("getToolsInput - returns repository property value if enforced", async (t)
});
test("getToolsInput - prefers workflow input", async (t) => {
const actions = stubGetToolsInput();
const target = callee(getToolsInput)
.withActions(actions)
.withActions(stubGetToolsInput)
.withArgs({
[RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value,
});
+1 -1
View File
@@ -16,7 +16,7 @@ export interface RemoteFileAddress {
}
/** The default file path to use in configuration file shorthands. */
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-config.yml";
/** The default ref to use in configuration file shorthands. */
export const DEFAULT_CONFIG_FILE_REF = "main";
+4 -4
View File
@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.26.1",
"cliVersion": "2.26.1",
"priorBundleVersion": "codeql-bundle-v2.26.0",
"priorCliVersion": "2.26.0"
"bundleVersion": "codeql-bundle-v2.26.2",
"cliVersion": "2.26.2",
"priorBundleVersion": "codeql-bundle-v2.26.1",
"priorCliVersion": "2.26.1"
}
+6 -1
View File
@@ -88,7 +88,7 @@ export enum EnvVar {
LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION",
/** UUID representing the current job run. */
JOB_RUN_UUID = "JOB_RUN_UUID",
JOB_RUN_UUID = "CODEQL_ACTION_JOB_RUN_UUID",
/** Status for the entire job, submitted to the status report in `init-post` */
JOB_STATUS = "CODEQL_ACTION_JOB_STATUS",
@@ -270,6 +270,11 @@ export class ReadOnlyEnv<T extends string | undefined = string | undefined> {
return Object.create(this, { vars: { value: { ...this.vars } } }) as this;
}
/** Gets a copy of the underlying environment. */
public get(): Record<string, T> {
return { ...this.vars };
}
/** Tries to get the value for `name` and throws if there isn't one. */
public getRequired(name: string): string {
return getRequiredEnvVar(this.vars, name);
+38 -12
View File
@@ -74,7 +74,6 @@ export enum Feature {
AllowMergeConfigFiles = "allow_merge_config_files",
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
AllowToolcacheInput = "allow_toolcache_input",
CleanupTrapCaches = "cleanup_trap_caches",
/** Whether to allow the `config-file` input to be specified via a repository property. */
ConfigFileRepositoryProperty = "config_file_repository_property",
@@ -122,12 +121,19 @@ export enum Feature {
* `OverlayAnalysisMatchCodeqlVersion` overrides this flag.
*/
OverlayAnalysisMatchCodeqlVersionDryRun = "overlay_analysis_match_codeql_version_dry_run",
OverlayAnalysisPython = "overlay_analysis_python",
/**
* Controls whether lower disk space requirements are used for overlay hardware checks.
* Has no effect if `OverlayAnalysisSkipResourceChecks` is enabled.
* Feature flags that lower the amount of available disk space that the overlay hardware check
* requires. The lowest threshold that is enabled takes effect; if none are enabled, the default
* threshold applies. These flags have no effect if `OverlayAnalysisSkipResourceChecks` is
* enabled.
*/
OverlayAnalysisResourceChecksV2 = "overlay_analysis_resource_checks_v2",
OverlayAnalysisMinDisk8Gb = "overlay_analysis_min_disk_8_gb",
OverlayAnalysisMinDisk9Gb = "overlay_analysis_min_disk_9_gb",
OverlayAnalysisMinDisk10Gb = "overlay_analysis_min_disk_10_gb",
OverlayAnalysisMinDisk11Gb = "overlay_analysis_min_disk_11_gb",
OverlayAnalysisMinDisk12Gb = "overlay_analysis_min_disk_12_gb",
OverlayAnalysisMinDisk13Gb = "overlay_analysis_min_disk_13_gb",
OverlayAnalysisPython = "overlay_analysis_python",
OverlayAnalysisRuby = "overlay_analysis_ruby",
/** Controls whether hardware checks are skipped for overlay analysis. */
OverlayAnalysisSkipResourceChecks = "overlay_analysis_skip_resource_checks",
@@ -187,11 +193,6 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
minimumVersion: undefined,
},
[Feature.AllowToolcacheInput]: {
defaultValue: false,
envVar: "CODEQL_ACTION_ALLOW_TOOLCACHE_INPUT",
minimumVersion: undefined,
},
[Feature.CleanupTrapCaches]: {
defaultValue: false,
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
@@ -360,9 +361,34 @@ export const featureConfig = {
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MATCH_CODEQL_VERSION_DRY_RUN",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisResourceChecksV2]: {
[Feature.OverlayAnalysisMinDisk8Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RESOURCE_CHECKS_V2",
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_8_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk9Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_9_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk10Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_10_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk11Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_11_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk12Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_12_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisMinDisk13Gb]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_13_GB",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisStatusCheck]: {
+8 -12
View File
@@ -4,7 +4,6 @@ import * as path from "path";
import * as core from "@actions/core";
import * as io from "@actions/io";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import {
@@ -255,19 +254,8 @@ async function run(
);
const repositoryProperties = repositoryPropertiesResult.orElse({});
// Create a unique identifier for this run.
const jobRunUuid = uuidV4();
logger.info(`Job run UUID is ${jobRunUuid}.`);
core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
core.exportVariable(EnvVar.INIT_ACTION_HAS_RUN, "true");
const actionStateWithFeatures = { ...actionState, features };
configFile = await getConfigFileInput(
actionStateWithFeatures,
repositoryProperties,
);
// path.resolve() respects the intended semantics of source-root. If
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
// source-root is absolute, it is used as given.
@@ -290,6 +278,14 @@ async function run(
);
}
// Compute the value of the `config-file` input.
const actionStateWithFeatures = { ...actionState, features };
configFile = await getConfigFileInput(
actionStateWithFeatures,
repositoryProperties,
analysisKinds,
);
// Send a status report indicating that an analysis is starting.
await sendStartingStatusReport(startedAt, { analysisKinds }, logger);
+27 -5
View File
@@ -35,6 +35,11 @@ export function isNumber(value: unknown): value is number {
return typeof value === "number";
}
/** Asserts that `value` is a boolean. */
export function isBoolean(value: unknown): value is boolean {
return typeof value === "boolean";
}
/** Asserts that `value` is either a string or undefined. */
export function isStringOrUndefined(
value: unknown,
@@ -62,14 +67,11 @@ function defaultCheck(
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
}
function makeValidator<T>(
validate: (arg: unknown) => arg is T,
required: boolean = true,
) {
function makeValidator<T>(validate: (arg: unknown) => arg is T) {
return {
validate,
check: defaultCheck(validate),
required,
required: true,
} as const satisfies Validator<T>;
}
@@ -82,6 +84,9 @@ export const string = makeValidator(isString);
/** A validator for number fields in schemas. */
export const number = makeValidator(isNumber);
/** A validator for boolean fields in schemas. */
export const boolean = makeValidator(isBoolean);
/** A validator for arrays. */
export function array<T>(validator: Validator<T>) {
const validate = (val: unknown) => {
@@ -221,6 +226,23 @@ export function validateSchema<
return result.valid;
}
/**
* Validates that `arr` is an array whose elements satisfy at least `elementSchema`.
* Additional keys are accepted in each element.
*
* @param elementSchema The schema to validate the elements against.
* @param arr The array to validate.
* @returns Asserts that `arr` has elements of `schema`'s type if validation is successful.
*/
export function validateArray<
S extends Schema,
T extends UnvalidatedArray = Array<FromSchema<S>>,
>(elementSchema: S, arr: UnvalidatedArray): arr is T {
const elementValidator = object(elementSchema);
return array(elementValidator).validate(arr);
}
export interface CheckSchemaOptions {
/** Whether to stop validation after the first error. */
failFast?: boolean;
+1 -6
View File
@@ -1,5 +1,4 @@
import * as core from "@actions/core";
import { v4 as uuidV4 } from "uuid";
import { Action, ActionState, runInActions } from "./action-common";
import {
@@ -95,7 +94,7 @@ async function sendCompletedStatusReport(
/** The main behaviour of this action. */
async function run(
actionState: ActionState<["Base", "Logger", "Actions"]>,
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
): Promise<void> {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -140,10 +139,6 @@ async function run(
const actionStateWithFeatures = { ...actionState, features };
const jobRunUuid = uuidV4();
logger.info(`Job run UUID is ${jobRunUuid}.`);
core.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
const statusReportBase = await createStatusReportBase(
ActionName.SetupCodeQL,
"starting",
+3 -11
View File
@@ -451,7 +451,7 @@ test.serial(
async (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const features = createFeatures([Feature.AllowToolcacheInput]);
const features = createFeatures([]);
const latestToolcacheVersion = "3.2.1";
const latestVersionPath = "/path/to/latest";
@@ -580,7 +580,7 @@ const toolcacheInputFallbackMacro = makeMacro({
toolcacheInputFallbackMacro.serial(
"the toolcache doesn't have a CodeQL CLI when tools == toolcache",
[Feature.AllowToolcacheInput],
[],
{ GITHUB_EVENT_NAME: "dynamic" },
[],
[
@@ -591,7 +591,7 @@ toolcacheInputFallbackMacro.serial(
toolcacheInputFallbackMacro.serial(
"the workflow trigger is not `dynamic`",
[Feature.AllowToolcacheInput],
[],
{ GITHUB_EVENT_NAME: "pull_request" },
[],
[
@@ -599,14 +599,6 @@ toolcacheInputFallbackMacro.serial(
],
);
toolcacheInputFallbackMacro.serial(
"the feature flag is not enabled",
[],
{ GITHUB_EVENT_NAME: "dynamic" },
[],
[`Ignoring 'tools: toolcache' because the feature is not enabled.`],
);
test.serial(
'tryGetTagNameFromUrl extracts the right tag name for a repo name containing "codeql-bundle"',
(t) => {
+4 -14
View File
@@ -533,11 +533,7 @@ export async function getCodeQLSource(
// We only allow `toolsInput === "toolcache"` for `dynamic` events. In general, using `toolsInput === "toolcache"`
// can lead to alert wobble and so it shouldn't be used for an analysis where results are intended to be uploaded.
// We also allow this in test mode.
const allowToolcacheValueFF = await features.getValue(
Feature.AllowToolcacheInput,
);
const allowToolcacheValue =
allowToolcacheValueFF && (isDynamicWorkflow() || util.isInTestMode());
const allowToolcacheValue = isDynamicWorkflow() || util.isInTestMode();
if (allowToolcacheValue) {
// If `toolsInput === "toolcache"`, try to find the latest version of the CLI that's available in the toolcache
// and use that. We perform this check here since we can set `cliVersion` directly and don't want to default to
@@ -558,15 +554,9 @@ export async function getCodeQLSource(
`Found no CodeQL CLI in the toolcache, ignoring 'tools: ${toolsInput}'...`,
);
} else {
if (allowToolcacheValueFF) {
logger.warning(
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`,
);
} else {
logger.info(
`Ignoring 'tools: ${toolsInput}' because the feature is not enabled.`,
);
}
logger.warning(
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`,
);
}
const version = await resolveDefaultCliVersion(
+14 -19
View File
@@ -3,11 +3,12 @@ import * as path from "path";
import * as core from "@actions/core";
import { Action, ActionState, runInActions } from "./action-common";
import * as actionsUtil from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { FeatureEnablement, initFeatures } from "./feature-flags";
import { BuiltInLanguage, parseBuiltInLanguage } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { Logger } from "./logging";
import { getRepositoryNwo } from "./repository";
import {
credentialToStr,
@@ -23,14 +24,14 @@ import {
import { generateCertificateAuthority } from "./start-proxy/ca";
import { checkProxyEnvironment } from "./start-proxy/environment";
import { checkConnections } from "./start-proxy/reachability";
import { ActionName, sendUnhandledErrorStatusReport } from "./status-report";
import { ActionName } from "./status-report";
import * as util from "./util";
async function run(startedAt: Date) {
async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
const logger = getActionsLogger();
const startedAt = action.startedAt;
const logger = action.logger;
let features: FeatureEnablement | undefined;
let language: BuiltInLanguage | undefined;
@@ -122,21 +123,15 @@ async function run(startedAt: Date) {
}
}
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
/** Defines the `start-proxy` Action. */
const startProxyAction: Action = {
name: ActionName.StartProxy,
run,
transformTelemetryError: getSafeErrorMessage,
};
try {
await run(startedAt);
} catch (error) {
core.setFailed(`start-proxy action failed: ${util.getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
ActionName.StartProxy,
startedAt,
getSafeErrorMessage(util.wrapError(error)),
logger,
);
}
export async function runWrapper() {
await runInActions(startProxyAction);
}
async function startProxy(
+1 -7
View File
@@ -83,12 +83,6 @@ export class StartProxyError extends Error {
}
}
interface StartProxyStatus extends StatusReportBase {
// A comma-separated list of registry types which are configured for CodeQL.
// This only includes registry types we support, not all that are configured.
registry_types: string;
}
/**
* Sends a status report for the `start-proxy` action indicating a successful outcome.
*
@@ -112,7 +106,7 @@ export async function sendSuccessStatusReport(
logger,
);
if (statusReportBase !== undefined) {
const statusReport: StartProxyStatus = {
const statusReport: StatusReportBase = {
...statusReportBase,
registry_types: registry_types.join(","),
};
+11 -5
View File
@@ -254,13 +254,19 @@ export function credentialToStr(credential: Credential): string {
return result;
}
/** A package registry is identified by its type and address. */
export type Registry = {
/** The schema for `RegistryBase` objects. */
export const registryBaseSchema = {
/** The type of the package registry. */
type: string;
type: json.string,
/** Whether the registry replaces the base registry for the ecosystem. */
"replaces-base"?: boolean;
} & Address;
"replaces-base": json.optional(json.boolean),
} as const satisfies json.Schema;
/** Information about a registry, other than its address. */
export type RegistryBase = json.FromSchema<typeof registryBaseSchema>;
/** A package registry is identified by its type and address. */
export type Registry = RegistryBase & Address;
// If a registry has an `url`, then that takes precedence over the `host` which may or may
// not be defined.
+104 -1
View File
@@ -1,17 +1,21 @@
import test from "ava";
import * as sinon from "sinon";
import * as uuid from "uuid";
import * as actionsUtil from "./actions-util";
import { Config } from "./config-utils";
import { EnvVar } from "./environment";
import { EnvVar, RegistryProxyVars } from "./environment";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";
import { ToolsSource } from "./setup-codeql";
import type { Registry } from "./start-proxy";
import {
ActionName,
createInitWithConfigStatusReport,
createStatusReportBase,
getActionsStatus,
getRegistryTypesFromEnv,
getJobUUID,
InitStatusReport,
InitWithConfigStatusReport,
} from "./status-report";
@@ -20,11 +24,106 @@ import {
setupActionsVars,
createTestConfig,
makeMacro,
getTestEnv,
RecordingLogger,
callee,
} from "./testing-utils";
import { BuildMode, ConfigurationError, withTmpDir, wrapError } from "./util";
setupTests(test);
test("getRegistryTypesFromEnv - gets unique registry types from environment", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([
{ type: "git_source", url: "https://example.com" },
{ type: "git_source", url: "https://github.com" },
{ type: "docker_registry", url: "https://registry.example.com" },
] satisfies Array<Partial<Registry>>),
});
const result = getRegistryTypesFromEnv(logger, env);
t.deepEqual(result, ["git_source", "docker_registry"].sort().join(","));
});
test("getRegistryTypesFromEnv - returns undefined if the env var is not set", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({});
const result = getRegistryTypesFromEnv(logger, env);
t.is(result, undefined);
});
test("getRegistryTypesFromEnv - returns undefined if the env var is not valid JSON", async (t) => {
const logger = new RecordingLogger(true);
const env = getTestEnv({ [RegistryProxyVars.PROXY_URLS]: "[" });
const result = getRegistryTypesFromEnv(logger, env);
t.is(result, undefined);
});
test("getRegistryTypesFromEnv - returns undefined if the env var is unexpected JSON", async (t) => {
const logger = new RecordingLogger(true);
t.is(
getRegistryTypesFromEnv(
logger,
getTestEnv({
// Top-level object rather than an array of objects.
[RegistryProxyVars.PROXY_URLS]: JSON.stringify({ type: "git_source" }),
}),
),
undefined,
);
t.is(
getRegistryTypesFromEnv(
logger,
getTestEnv({
// Object has no "type" key.
[RegistryProxyVars.PROXY_URLS]: JSON.stringify([{}]),
}),
),
undefined,
);
});
test("getJobUUID - generates valid UUIDs", async (t) => {
await callee(getJobUUID)
.withArgs()
.logs(t, "Job run UUID is ")
.hasEnv(t, (val) => {
return {
[EnvVar.JOB_RUN_UUID]: val,
};
})
.passes((val) => {
t.true(uuid.validate(val));
});
});
test("getJobUUID - retrieves existing job UUIDs", async (t) => {
const existingJobUuid = uuid.v4();
await callee(getJobUUID)
.withArgs()
.withEnv((env) => {
env.set(EnvVar.JOB_RUN_UUID, existingJobUuid);
})
.logs(t, `Existing job run UUID is ${existingJobUuid}.`)
.passes(t.deepEqual, existingJobUuid);
});
test("getJobUUID - doesn't retrieve invalid UUIDs", async (t) => {
const existingJobUuid = "not-a-uuid";
await callee(getJobUUID)
.withArgs()
.withEnv((env) => {
env.set(EnvVar.JOB_RUN_UUID, existingJobUuid);
})
.logs(t, `Job run UUID is `)
.notLogs(t, `Existing job run UUID is ${existingJobUuid}.`)
.passes(t.notDeepEqual, existingJobUuid);
});
function setupEnvironmentAndStub(tmpDir: string) {
setupActionsVars(tmpDir, tmpDir, {
GITHUB_EVENT_NAME: "dynamic",
@@ -34,6 +133,9 @@ function setupEnvironmentAndStub(tmpDir: string) {
process.env[EnvVar.ANALYSIS_KEY] = "analysis-key";
process.env["ImageVersion"] = "2023.05.19.1";
process.env[RegistryProxyVars.PROXY_URLS] = JSON.stringify([
{ type: "maven_repository" },
] satisfies Array<Partial<Registry>>);
const getRequiredInput = sinon.stub(actionsUtil, "getRequiredInput");
getRequiredInput.withArgs("matrix").resolves("input/matrix");
@@ -77,6 +179,7 @@ test.serial("createStatusReportBase", async (t) => {
t.is(typeof statusReport.job_run_uuid, "string");
t.is(statusReport.languages, "java,swift");
t.is(statusReport.ref, process.env["GITHUB_REF"]!);
t.is(statusReport.registry_types, "maven_repository");
t.is(statusReport.runner_available_disk_space_bytes, 100);
t.is(statusReport.runner_image_version, process.env["ImageVersion"]);
t.is(statusReport.runner_os, process.env["RUNNER_OS"]!);
+80 -1
View File
@@ -1,7 +1,9 @@
import * as os from "os";
import * as core from "@actions/core";
import * as uuid from "uuid";
import type { ActionState } from "./action-common";
import {
getWorkflowEventName,
getOptionalInput,
@@ -17,12 +19,14 @@ import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
import { DocUrl } from "./doc-url";
import { EnvVar } from "./environment";
import { EnvVar, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
import { getRef } from "./git-utils";
import * as json from "./json";
import type { Logger } from "./logging";
import type { OverlayBaseDatabaseDownloadStats } from "./overlay/caching";
import { getRepositoryNwo } from "./repository";
import type { ToolsSource } from "./setup-codeql";
import { registryBaseSchema } from "./start-proxy/types";
import {
ConfigurationError,
getRequiredEnvParam,
@@ -59,6 +63,30 @@ export function getDisplayActionName(actionName: ActionName): string {
return actionName;
}
/**
* Either creates a UUIDv4 for the analysis or retrieves an existing one from the
* environment and returns it.
* If a new UUID is generated, it is also exported as an environment variable.
*/
export function getJobUUID(
action: ActionState<["Logger", "ReadOnlyEnv", "Actions"]>,
) {
// Check if we already have a UUID for the analysis and return it if so.
const existingJobRunUuid = action.env.getOptional(EnvVar.JOB_RUN_UUID);
if (existingJobRunUuid !== undefined && uuid.validate(existingJobRunUuid)) {
action.logger.info(`Existing job run UUID is ${existingJobRunUuid}.`);
return existingJobRunUuid;
}
// Otherwise generate a new UUID.
const jobRunUuid = uuid.v4();
action.logger.info(`Job run UUID is ${jobRunUuid}.`);
action.actions.exportVariable(EnvVar.JOB_RUN_UUID, jobRunUuid);
return jobRunUuid;
}
/**
* @returns a boolean indicating whether the analysis is considered to be first party.
*
@@ -159,6 +187,12 @@ export interface StatusReportBase {
ml_powered_javascript_queries?: string;
/** Ref that the workflow was triggered on. */
ref: string;
/**
* A comma-separated list of private registry types which are configured for CodeQL.
* This only includes registry types we support (as determined by the `start-proxy` action),
* not all that are configured.
*/
registry_types?: string;
/** Action runner hardware architecture (context runner.arch). */
runner_arch?: string;
/** Available disk space on the runner, in bytes. */
@@ -262,6 +296,50 @@ export interface EventReport {
started_at: string;
}
/**
* Attempts to retrieve a list of private registry types from the `CODEQL_PROXY_URLS` environment
* variable and returns it as a comma-separated string if successful. Returns `undefined` otherwise.
*/
export function getRegistryTypesFromEnv(
logger: Logger,
env: ReadOnlyEnv = getEnv(),
): string | undefined {
// Try to get the value of the environment variable.
const value = env.getOptional(RegistryProxyVars.PROXY_URLS);
if (value === undefined) {
return undefined;
}
// Try to parse the JSON we expect to find in it and return the comma-separated list of
// (unique) registry types.
try {
const data = JSON.parse(value) as unknown;
// Check that the parsed JSON meets our expectations.
if (!json.isArray(data)) {
logger.debug(
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array, but got '${typeof data}'.`,
);
return undefined;
}
if (!json.validateArray(registryBaseSchema, data)) {
logger.debug(
`Expected '${RegistryProxyVars.PROXY_URLS}' to contain a JSON array of registry objects, but got something else.`,
);
return undefined;
}
const types = new Set(data.map((r) => r.type));
return Array.from(types).sort().join(",");
} catch (err) {
logger.debug(
`Failed to parse '${RegistryProxyVars.PROXY_URLS}': ${getErrorMessage(err)}.`,
);
return undefined;
}
}
/**
* Compose a StatusReport.
*
@@ -324,6 +402,7 @@ export async function createStatusReportBase(
job_name: jobName,
job_run_uuid: jobRunUUID,
ref,
registry_types: getRegistryTypesFromEnv(logger),
runner_os: runnerOs,
started_at: workflowStartedAt,
status,
+33
View File
@@ -0,0 +1,33 @@
import * as path from "path";
import * as stream from "stream";
import test from "ava";
import { getRunnerLogger } from "./logging";
import { extractTarZst } from "./tar";
import { setupTests } from "./testing-utils";
import { withTmpDir } from "./util";
setupTests(test);
test("extractTarZst rejects if the input stream errors", async (t) => {
await withTmpDir(async (tmpDir) => {
const archive = new stream.PassThrough();
const promise = extractTarZst(
archive,
path.join(tmpDir, "dest"),
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
);
archive.destroy(
Object.assign(new Error("socket hang up"), {
code: "ECONNRESET",
}),
);
await t.throwsAsync(promise, {
message: /Error while downloading and extracting tar/,
});
});
});
+9 -4
View File
@@ -194,10 +194,15 @@ export async function extractTarZst(
});
if (tar instanceof stream.Readable) {
tar.pipe(tarProcess.stdin).on("error", (err) => {
reject(
new Error(`Error while downloading and extracting tar: ${err}`),
);
// Use `pipeline` rather than `pipe` so that an error on either stream is reported here
// rather than being emitted as an unhandled `error` event, and so that `tar`'s standard
// input is closed if the download fails partway through.
stream.pipeline(tar, tarProcess.stdin, (err) => {
if (err) {
reject(
new Error(`Error while downloading and extracting tar: ${err}`),
);
}
});
}
+83 -25
View File
@@ -34,11 +34,14 @@ import { ActionName } from "./status-report";
import {
DEFAULT_DEBUG_ARTIFACT_NAME,
DEFAULT_DEBUG_DATABASE_NAME,
Failure,
getEnv,
GitHubVariant,
GitHubVersion,
HTTPError,
resetCachedCodeQlVersion,
Result,
Success,
} from "./util";
export const SAMPLE_DOTCOM_API_DETAILS = {
@@ -182,13 +185,32 @@ export function getTestEnv(testEnv: NodeJS.ProcessEnv = {}): Env {
return getEnv(testEnv);
}
/** An implementation of `ActionsEnv` for use in tests. */
class TestActionsEnv implements ActionsEnv {
constructor(private readonly env: Env) {}
public clone(env: Env): this {
return Object.create(this, { env: { value: env } }) as this;
}
public getRequiredInput(name: string): string {
throw new Error(`Input required and not supplied: ${name}`);
}
public getOptionalInput(_name: string): string | undefined {
return undefined;
}
public exportVariable(name: string, value: string): void {
this.env.set(name, value);
}
}
/**
* Gets an `ActionsEnv` instance for use in tests.
*/
export function getTestActionsEnv(): ActionsEnv {
return {
getOptionalInput: () => undefined,
};
export function getTestActionsEnv(env: Env): TestActionsEnv {
return new TestActionsEnv(env);
}
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
@@ -206,12 +228,13 @@ type AllState = [
export function initAllState(
overrides?: Partial<ActionState<AllState>>,
): ActionState<AllState> {
const env = getTestEnv();
return {
name: ActionName.Init,
startedAt: new Date(),
logger: new RecordingLogger(),
env: getTestEnv(),
actions: getTestActionsEnv(),
env,
actions: getTestActionsEnv(env),
apiClient: github.getOctokit("123"),
features: createFeatures([]),
...overrides,
@@ -222,9 +245,13 @@ type DelayedCheck<
Args extends readonly any[],
R,
Fs extends ReadonlyArray<AllState[number]>,
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
> = (
env: Readonly<BaseEnvBuilder<Args, R, Fs>>,
result: Result<Awaited<R>, ThrownError<ErrorConstructor | Error>>,
) => Promise<any>;
export type ValueOrMutation<T> = T | ((val: T) => void);
export type Mutation<T> = (val: T) => void;
export type ValueOrMutation<T> = T | Mutation<T>;
/**
* Wraps a function that accepts an `ActionState` for testing in different environments.
@@ -236,6 +263,7 @@ abstract class BaseEnvBuilder<
> {
protected readonly fn: (state: ActionState<Fs>, ...args: Args) => R;
private logger: RecordingLogger;
private actions: TestActionsEnv;
protected state: ActionState<AllState>;
protected checks: Array<DelayedCheck<Args, R, Fs>>;
@@ -245,15 +273,26 @@ abstract class BaseEnvBuilder<
) {
this.fn = fn;
this.logger = new RecordingLogger();
this.state =
cloneFrom !== undefined
? ({
...cloneFrom.state,
env: cloneFrom.state.env.clone(),
actions: Object.create(cloneFrom.state.actions),
logger: this.logger,
} satisfies ActionState<AllState>)
: initAllState({ logger: this.logger });
if (cloneFrom !== undefined) {
const env = cloneFrom.state.env.clone();
this.actions = cloneFrom.actions.clone(env);
this.state = {
...cloneFrom.state,
env,
actions: this.actions,
logger: this.logger,
} satisfies ActionState<AllState>;
} else {
const env = getTestEnv();
this.actions = getTestActionsEnv(env);
this.state = initAllState({
logger: this.logger,
env,
actions: this.actions,
});
}
this.checks = [...(cloneFrom?.checks ?? [])];
}
@@ -320,13 +359,10 @@ abstract class BaseEnvBuilder<
return result;
}
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
/** Applies `fn` to the `ActionsEnv`. */
public withActions(fn: Mutation<ActionsEnv>): this {
const result = this.clone();
if (typeof arg === "function") {
arg(result.state.actions);
} else {
result.state.actions = arg;
}
fn(result.state.actions);
return result;
}
@@ -342,6 +378,28 @@ abstract class BaseEnvBuilder<
return result;
}
/**
* Adds a delayed check that the environment variables returned by `fn`
* are present in the environment after the main assertion passes.
*/
public hasEnv(
t: ExecutionContext<unknown>,
fn: (
value: Awaited<R> | undefined,
error: ThrownError<ErrorConstructor | Error> | undefined,
) => Record<string, string | undefined>,
): this {
const result = this.clone();
result.checks.push(async (env, r) => {
const value = r.orElse(undefined);
const error = r.isFailure() ? r.value : undefined;
const expected = fn(value, error);
t.like(env.getState().env.get(), expected);
});
return result;
}
/**
* Adds a delayed check that `messages` are not logged. The check will be
* performed after the main assertion passes.
@@ -439,7 +497,7 @@ class CallableEnvBuilder<
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
await delayedCheck(this, new Success(result));
}
// Return the results of the function call and the main assertion.
@@ -465,7 +523,7 @@ class CallableEnvBuilder<
// Run other delayed checks.
for (const delayedCheck of this.checks) {
await delayedCheck(this);
await delayedCheck(this, new Failure(error));
}
// Return the error.
+37
View File
@@ -38,6 +38,43 @@ test.serial(
},
);
test.serial(
"downloadAndExtract falls back to downloading before extracting if streaming fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
sinon.stub(process, "platform").value("linux");
const archivePath = path.join(tmpDir, "codeql-bundle.tar.zst");
const destination = path.join(tmpDir, "codeql");
const downloadTool = sinon
.stub(toolcache, "downloadTool")
.resolves(archivePath);
const extract = sinon.stub(tar, "extract").resolves(destination);
const extractTarZst = sinon.stub(tar, "extractTarZst").resolves();
const request = nock("https://example.com")
.get("/codeql-bundle.tar.zst")
.replyWithError(
Object.assign(new Error("socket hang up"), { code: "ECONNRESET" }),
);
const statusReport = await downloadAndExtract(
"https://example.com/codeql-bundle.tar.zst",
"zstd",
destination,
undefined,
{},
{ type: "gnu", version: "1.34" },
getRunnerLogger(true),
);
t.assert(Number.isInteger(statusReport.downloadDurationMs));
t.true(request.isDone());
t.false(extractTarZst.called);
t.true(downloadTool.calledOnce);
t.true(extract.calledOnce);
});
},
);
test.serial(
"downloadAndExtract omits the download duration when streaming extraction",
async (t) => {
+24 -4
View File
@@ -19,6 +19,12 @@ import { cleanUpPath, getErrorMessage, getRequiredEnvParam } from "./util";
*/
const STREAMING_HIGH_WATERMARK_BYTES = 4 * 1024 * 1024; // 4 MiB
/**
* How long the streaming download of the CodeQL tools may stall for before we abort it. This
* applies both to establishing the connection and to gaps between chunks of the response body.
*/
const STREAMING_STALL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
/**
* The name of the tool cache directory for the CodeQL tools.
*/
@@ -137,8 +143,8 @@ async function downloadAndExtractZstdWithStreaming(
authorization ? { authorization } : {},
headers,
);
const response = await new Promise<IncomingMessage>((resolve) =>
https.get(
const response = await new Promise<IncomingMessage>((resolve, reject) => {
const request = https.get(
codeqlURL,
{
headers,
@@ -148,10 +154,24 @@ async function downloadAndExtractZstdWithStreaming(
agent,
} as unknown as RequestOptions,
(r) => resolve(r),
),
);
);
// Without this listener, connection failures such as `ECONNRESET` are emitted as unhandled
// `error` events, which terminate the process instead of letting us fall back to downloading
// the bundle before extracting it. This listener stays attached after the response arrives, so
// it also handles errors that occur while the response is being streamed.
request.on("error", reject);
request.setTimeout(STREAMING_STALL_TIMEOUT_MS, () => {
request.destroy(
new Error(
`No data received for ${formatDuration(STREAMING_STALL_TIMEOUT_MS)}.`,
),
);
});
});
if (response.statusCode !== 200) {
// Discard the response body so that the connection can be released.
response.resume();
throw new Error(
`Failed to download CodeQL bundle from ${codeqlURL}. HTTP status code: ${response.statusCode}.`,
);