mirror of
https://github.com/github/codeql-action
synced 2026-10-13 15:02:07 +03:00
Compare commits
165
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486fec2a3e | ||
|
|
134624c67b | ||
|
|
ff43db8f98 | ||
|
|
4605e03a74 | ||
|
|
099c869cad | ||
|
|
db488ddef3 | ||
|
|
1845f5ba8b | ||
|
|
79a73408b4 | ||
|
|
f9d9f07d37 | ||
|
|
9ee088e136 | ||
|
|
1aef003397 | ||
|
|
508b83bc41 | ||
|
|
d97b3428e8 | ||
|
|
47fa622223 | ||
|
|
45693cc688 | ||
|
|
c2fd8f54d1 | ||
|
|
c56f48e9bd | ||
|
|
aa0eadc572 | ||
|
|
43250d671a | ||
|
|
5008effa71 | ||
|
|
053d41e61e | ||
|
|
1158e1c92a | ||
|
|
ff2f1c621b | ||
|
|
2d49edbac6 | ||
|
|
951a133f96 | ||
|
|
be7a3dbb81 | ||
|
|
9310334b11 | ||
|
|
6dc633238e | ||
|
|
b4d8a54218 | ||
|
|
ab5db2519c | ||
|
|
6c0d9018d4 | ||
|
|
bf96b0df93 | ||
|
|
337136ab8a | ||
|
|
a9baab8dee | ||
|
|
33d70867d5 | ||
|
|
38055a3c3c | ||
|
|
1f87aed5e6 | ||
|
|
dc1b98ad1c | ||
|
|
6f0220ee37 | ||
|
|
ca1c97228c | ||
|
|
0e8a5d99f8 | ||
|
|
54a084632e | ||
|
|
40f80a8df0 | ||
|
|
b222c3aaea | ||
|
|
11569df0a1 | ||
|
|
0a99875ae5 | ||
|
|
246018e041 | ||
|
|
1332611f51 | ||
|
|
4dc327a942 | ||
|
|
bb19330c5e | ||
|
|
0e85c0e99c | ||
|
|
bfcd769ba1 | ||
|
|
c16c0f3f28 | ||
|
|
208a88adc7 | ||
|
|
f47bb7b9aa | ||
|
|
c205ff6f09 | ||
|
|
b672c70acd | ||
|
|
7131139037 | ||
|
|
b5225f21c5 | ||
|
|
acb38565c9 | ||
|
|
9183a7b6e1 | ||
|
|
5f8c44ba62 | ||
|
|
794f5bc385 | ||
|
|
54109818e0 | ||
|
|
99caaa8b90 | ||
|
|
6117bb503a | ||
|
|
af767ec1f6 | ||
|
|
7d9249f5a5 | ||
|
|
8ebf1091b0 | ||
|
|
bdf39710a2 | ||
|
|
74cfae9be6 | ||
|
|
47a0a833bb | ||
|
|
6a90bf1f54 | ||
|
|
c5995f544d | ||
|
|
76c44396d3 | ||
|
|
fad141fa6c | ||
|
|
7d82f1132f | ||
|
|
37bdbde050 | ||
|
|
5595ccaf91 | ||
|
|
ec9c75796a | ||
|
|
45c8742e17 | ||
|
|
6a9359a1bd | ||
|
|
065cdc0394 | ||
|
|
f99dd5aeee | ||
|
|
1804b211a3 | ||
|
|
3020a2f462 | ||
|
|
93c3a5a40b | ||
|
|
d1ba80a13d | ||
|
|
e74600b0d9 | ||
|
|
266c7bdbd2 | ||
|
|
daa7fe6fba | ||
|
|
1cd4d01d58 | ||
|
|
d2bfc30bc3 | ||
|
|
68028fcb16 | ||
|
|
c29563eeaa | ||
|
|
155e522997 | ||
|
|
2d3b351ea6 | ||
|
|
5d3eb98e4a | ||
|
|
c5f739bd64 | ||
|
|
2e53f1cea2 | ||
|
|
d0ee43ac62 | ||
|
|
06f1d4ffed | ||
|
|
82f035a501 | ||
|
|
f205ea1c33 | ||
|
|
60a57910be | ||
|
|
3502f79575 | ||
|
|
8f0a4f23c4 | ||
|
|
780c00da3f | ||
|
|
f02afd42b0 | ||
|
|
d57c3ffcba | ||
|
|
e40d079dd9 | ||
|
|
8e6fdffc32 | ||
|
|
ba46ff760e | ||
|
|
b411bbcd4a | ||
|
|
9130ce0f73 | ||
|
|
0cebd1d28d | ||
|
|
d2f5cbbe91 | ||
|
|
c62d824686 | ||
|
|
da0c190101 | ||
|
|
36737508ec | ||
|
|
30c33c9286 | ||
|
|
3ca82bb259 | ||
|
|
42a3b94790 | ||
|
|
13d4882649 | ||
|
|
e55a57b808 | ||
|
|
51d51e8121 | ||
|
|
e893985e8b | ||
|
|
eb692f8b49 | ||
|
|
aac07d2a41 | ||
|
|
de57c4a441 | ||
|
|
2e251072b0 | ||
|
|
94a12eb6f6 | ||
|
|
e28cbacfa1 | ||
|
|
60834a0cd9 | ||
|
|
e9831f72a2 | ||
|
|
766928d055 | ||
|
|
049af32c59 | ||
|
|
c7ae51bb2d | ||
|
|
1f9caf0118 | ||
|
|
18420e3271 | ||
|
|
7e8d8970f0 | ||
|
|
2d4c474c2c | ||
|
|
98c05a17d3 | ||
|
|
8289a49271 | ||
|
|
2a8731cc06 | ||
|
|
3434fbbc53 | ||
|
|
3013ac07bd | ||
|
|
74b15aa2c6 | ||
|
|
f00f809405 | ||
|
|
0953dc00da | ||
|
|
cbad145443 | ||
|
|
ab44eb939d | ||
|
|
961b583f9a | ||
|
|
d71461774b | ||
|
|
027ac05d3b | ||
|
|
66a6f42f0a | ||
|
|
c5d621238d | ||
|
|
adba0868a4 | ||
|
|
916098aa8d | ||
|
|
093dce6cc2 | ||
|
|
57eb44123f | ||
|
|
5901394530 | ||
|
|
b69467ce8b | ||
|
|
85d157095f | ||
|
|
2d14f71964 |
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -113,7 +113,6 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
- language: python
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -4,7 +4,28 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.8 - 21 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.7 - 13 Aug 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3). [#4085](https://github.com/github/codeql-action/pull/4085)
|
||||
|
||||
## 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
@@ -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.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
}
|
||||
Generated
+2653
-1818
File diff suppressed because it is too large.
Load diff
Generated
+387
-351
File diff suppressed because it is too large.
Load diff
+13
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.4",
|
||||
"version": "4.37.9",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -30,22 +30,22 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.2.1",
|
||||
"js-yaml": "^5.2.3",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.1",
|
||||
"undici": "^6.24.0"
|
||||
"undici": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
@@ -58,22 +58,22 @@
|
||||
"@types/node": "^20.19.43",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.1",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"globals": "^17.11.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.64.0"
|
||||
"typescript-eslint": "^8.67.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
@@ -95,6 +95,6 @@
|
||||
"semver": ">=6.3.1"
|
||||
},
|
||||
"glob": "^13.0.6",
|
||||
"undici": "^6.24.0"
|
||||
"undici": "^6.28.0"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Executable
+127
@@ -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());
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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'."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.5",
|
||||
@@ -12,6 +12,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.23.1"
|
||||
"tsx": "^4.23.12"
|
||||
}
|
||||
}
|
||||
@@ -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, "");
|
||||
});
|
||||
});
|
||||
Executable
+82
@@ -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());
|
||||
}
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Executable
+84
@@ -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
@@ -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}' }}`,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function runWrapper() {
|
||||
logger,
|
||||
);
|
||||
if (config !== undefined) {
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const version = await codeql.getVersion();
|
||||
await debugArtifacts.uploadCombinedSarifArtifacts(
|
||||
logger,
|
||||
|
||||
@@ -255,7 +255,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
if (hasBadExpectErrorInput()) {
|
||||
throw new util.ConfigurationError(
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
|
||||
{"maximumVersion":"3.23","minimumVersion":"3.17"}
|
||||
@@ -99,7 +99,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
languages = await determineAutobuildLanguages(codeql, config, logger);
|
||||
if (languages !== undefined) {
|
||||
|
||||
+1
-1
@@ -155,7 +155,7 @@ export async function runAutobuild(
|
||||
logger: Logger,
|
||||
) {
|
||||
logger.startGroup(`Attempting to automatically build ${language} code`);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd);
|
||||
const codeQL = await getCodeQL(logger, config.codeQLCmd);
|
||||
if (language === BuiltInLanguage.cpp) {
|
||||
await setupCppAutobuild(codeQL, logger);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { EnvVar } from "../environment";
|
||||
import { getRunnerLogger } from "../logging";
|
||||
import { getTestEnv, setupTests } from "../testing-utils";
|
||||
import * as util from "../util";
|
||||
|
||||
import * as outputCache from "./output-cache";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "codeql-action-command-cache.json");
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.deepEqual(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
{
|
||||
version: "2.20.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
fs.writeFileSync(
|
||||
cacheFile,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
fs.writeFileSync(cacheFile, "not valid json", "utf8");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFile = path.join(tmpDir, "version.json");
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
|
||||
const testValues = [
|
||||
{ cmd: "/path/to/codeql" },
|
||||
{ entries: { version: { version: "2.20.0" } } },
|
||||
{ cmd: "/path/to/codeql", entries: {} },
|
||||
{ cmd: "/path/to/codeql", entries: null },
|
||||
{ cmd: "/path/to/codeql", entries: { version: {} } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: null } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: "2.20.0" } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: null } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2.2 } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2 } } },
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", overlayVersion: "1" } },
|
||||
},
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", features: "nope" } },
|
||||
},
|
||||
].map((v) => JSON.stringify(v));
|
||||
|
||||
for (const value of testValues) {
|
||||
fs.writeFileSync(cacheFile, value, "utf8");
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
value,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
|
||||
t.notThrows(() => {
|
||||
t.is(
|
||||
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
import type { VersionInfo } from "./types";
|
||||
|
||||
/**
|
||||
* The keys of the command cache. Each key corresponds to a command whose output we cache.
|
||||
*/
|
||||
export type CommandCacheKey = string;
|
||||
|
||||
/**
|
||||
* The type of the command cache that is persisted to disk.
|
||||
*/
|
||||
export interface OutputCache {
|
||||
cmd: string;
|
||||
entries: Record<CommandCacheKey, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the temporary file that backs the on-disk cache of
|
||||
* CLI responses between workflow steps.
|
||||
*/
|
||||
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
||||
|
||||
/**
|
||||
* The module-global variable that caches the CodeQL CLI version in-memory.
|
||||
*/
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the temporary file that backs the
|
||||
* on-disk cache of CLI responses between workflow steps.
|
||||
*/
|
||||
function getCommandCacheFilePath(env: Env): string {
|
||||
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the CodeQL CLI version both in-memory and on disk.
|
||||
* @param env The environment variables to use.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
* @param version The version information to cache.
|
||||
*/
|
||||
export function cacheCodeQlVersion(
|
||||
env: Env,
|
||||
cmd: string,
|
||||
version: VersionInfo,
|
||||
): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
const outputCache = {
|
||||
cmd,
|
||||
entries: { version },
|
||||
} satisfies OutputCache;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
fs.writeFileSync(
|
||||
getCommandCacheFilePath(env),
|
||||
JSON.stringify(outputCache),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached CodeQL CLI version, if any.
|
||||
* @param logger The logger to use for logging messages.
|
||||
* @param env The environment variables to use.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
*/
|
||||
export function getCachedCodeQlVersion(
|
||||
logger: Logger,
|
||||
env: Env,
|
||||
cmd?: string,
|
||||
): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
let serialized: string;
|
||||
try {
|
||||
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
|
||||
} catch (e) {
|
||||
logger.debug(
|
||||
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch (e) {
|
||||
logger.debug(`Cannot parse CLI-cache data as JSON: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isOutputCache(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.entries.version as VersionInfo;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `VersionInfo` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const candidate = x as Partial<VersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.version === "string" &&
|
||||
(candidate.features === undefined ||
|
||||
(typeof candidate.features === "object" &&
|
||||
candidate.features !== null)) &&
|
||||
(candidate.overlayVersion === undefined ||
|
||||
typeof candidate.overlayVersion === "number")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `OutputCache` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isOutputCache(x: unknown): x is OutputCache {
|
||||
const candidate = x as Partial<OutputCache> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
candidate.entries !== undefined &&
|
||||
isVersionInfo(candidate.entries.version)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
+25
-4
@@ -51,6 +51,31 @@ test.beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - true for expected errors", async (t) => {
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(new Error("ENOSPC: Out of disk space")),
|
||||
);
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(
|
||||
new Error(
|
||||
"EACCES: permission denied, mkdir /opt/hostedtoolcache/CodeQL/",
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - false for other errors", async (t) => {
|
||||
t.false(codeql.isDiskConfigurationError("Not an Error instance"));
|
||||
|
||||
const otherMessages = [
|
||||
"Does not contain an error code we test for",
|
||||
"ENOSP: Not quite the full error code",
|
||||
];
|
||||
for (const otherMessage of otherMessages) {
|
||||
t.false(codeql.isDiskConfigurationError(new Error(otherMessage)));
|
||||
}
|
||||
});
|
||||
|
||||
async function installIntoToolcache({
|
||||
apiDetails = SAMPLE_DOTCOM_API_DETAILS,
|
||||
cliVersion,
|
||||
@@ -580,7 +605,6 @@ const injectedConfigMacro = makeMacro({
|
||||
"",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -856,7 +880,6 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
"/path/to/qlconfig.yml",
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -887,7 +910,6 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
undefined, // undefined qlconfigFile
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as any[];
|
||||
@@ -1066,7 +1088,6 @@ test.serial(
|
||||
"sourceRoot",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(false),
|
||||
);
|
||||
|
||||
t.true(runnerConstructorStub.calledOnce);
|
||||
|
||||
+33
-26
@@ -12,10 +12,12 @@ import {
|
||||
runTool,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import * as outputCache from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import { CliError, wrapCliConfigurationError } from "./cli-errors";
|
||||
import { appendExtraQueryExclusions, type Config } from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar } from "./environment";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -23,7 +25,7 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { isAnalyzingDefaultBranch } from "./git-utils";
|
||||
import { Language } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "./overlay";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import * as setupCodeql from "./setup-codeql";
|
||||
@@ -91,7 +93,6 @@ export interface CodeQL {
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Runs the autobuilder for the given language.
|
||||
@@ -215,20 +216,6 @@ export interface CodeQL {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
|
||||
export interface ResolveDatabaseOutput {
|
||||
overlayBaseSpecifier?: string;
|
||||
}
|
||||
@@ -286,6 +273,26 @@ const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
|
||||
/**
|
||||
* Decides whether `e` is a disk-related error outside of our control
|
||||
* that should be classified as a `ConfigurationError`.
|
||||
*
|
||||
* @param e The error to check.
|
||||
* @returns True if the error should be treated as a `ConfigurationError` or false if not.
|
||||
*/
|
||||
export function isDiskConfigurationError(e: unknown): boolean {
|
||||
if (!(e instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
// out of disk space
|
||||
e.message.includes("ENOSPC") ||
|
||||
// access denied
|
||||
e.message.includes("EACCES")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up CodeQL CLI access.
|
||||
*
|
||||
@@ -346,7 +353,7 @@ export async function setupCodeQL(
|
||||
);
|
||||
}
|
||||
|
||||
cachedCodeQL = await getCodeQLForCmd(codeqlCmd, checkVersion);
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, codeqlCmd, checkVersion);
|
||||
return {
|
||||
codeql: cachedCodeQL,
|
||||
toolsDownloadStatusReport,
|
||||
@@ -356,8 +363,7 @@ export async function setupCodeQL(
|
||||
} catch (rawError) {
|
||||
const e = api.wrapApiConfigurationError(rawError);
|
||||
const ErrorClass =
|
||||
e instanceof util.ConfigurationError ||
|
||||
(e instanceof Error && e.message.includes("ENOSPC")) // out of disk space
|
||||
e instanceof util.ConfigurationError || isDiskConfigurationError(e)
|
||||
? util.ConfigurationError
|
||||
: Error;
|
||||
|
||||
@@ -372,9 +378,9 @@ export async function setupCodeQL(
|
||||
/**
|
||||
* Use the CodeQL executable located at the given path.
|
||||
*/
|
||||
export async function getCodeQL(cmd: string): Promise<CodeQL> {
|
||||
export async function getCodeQL(logger: Logger, cmd: string): Promise<CodeQL> {
|
||||
if (cachedCodeQL === undefined) {
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true);
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, cmd, true);
|
||||
}
|
||||
return cachedCodeQL;
|
||||
}
|
||||
@@ -481,8 +487,9 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||
*/
|
||||
export async function getCodeQLForTesting(
|
||||
cmd = "codeql-for-testing",
|
||||
logger: Logger = getRunnerLogger(true),
|
||||
): Promise<CodeQL> {
|
||||
return getCodeQLForCmd(cmd, false);
|
||||
return getCodeQLForCmd(logger, cmd, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -494,6 +501,7 @@ export async function getCodeQLForTesting(
|
||||
* @returns A new CodeQL object
|
||||
*/
|
||||
async function getCodeQLForCmd(
|
||||
logger: Logger,
|
||||
cmd: string,
|
||||
checkVersion: boolean,
|
||||
): Promise<CodeQL> {
|
||||
@@ -502,7 +510,7 @@ async function getCodeQLForCmd(
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
let result = util.getCachedCodeQlVersion(cmd);
|
||||
let result = outputCache.getCachedCodeQlVersion(logger, getEnv(), cmd);
|
||||
if (result === undefined) {
|
||||
result = await runCliJson<VersionInfo>(
|
||||
cmd,
|
||||
@@ -511,7 +519,7 @@ async function getCodeQLForCmd(
|
||||
noStreamStdout: true,
|
||||
},
|
||||
);
|
||||
util.cacheCodeQlVersion(cmd, result);
|
||||
outputCache.cacheCodeQlVersion(getEnv(), cmd, result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -539,7 +547,6 @@ async function getCodeQLForCmd(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
) {
|
||||
const extraArgs = config.languages.map(
|
||||
(language) => `--language=${language}`,
|
||||
|
||||
@@ -1295,13 +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",
|
||||
"No overlay-base database on default branch if runner disk space is below minimum",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1315,13 +1314,12 @@ checkOverlayEnablementMacro.serial(
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"Overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks enabled",
|
||||
"Overlay-base database on default branch if runner disk space is above minimum",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1335,17 +1333,93 @@ checkOverlayEnablementMacro.serial(
|
||||
},
|
||||
);
|
||||
|
||||
// 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(
|
||||
"No overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks not enabled",
|
||||
"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: 15_000_000_000,
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
+46
-24
@@ -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,23 @@ 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_MB_BY_FEATURE`.
|
||||
*/
|
||||
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.
|
||||
* Minimum available disk space (in MB) enabled by each overlay feature flag.
|
||||
*/
|
||||
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_MB_BY_FEATURE = {
|
||||
[Feature.OverlayAnalysisMinDisk8Gb]: 8000,
|
||||
[Feature.OverlayAnalysisMinDisk9Gb]: 9000,
|
||||
[Feature.OverlayAnalysisMinDisk10Gb]: 10000,
|
||||
[Feature.OverlayAnalysisMinDisk11Gb]: 11000,
|
||||
[Feature.OverlayAnalysisMinDisk12Gb]: 12000,
|
||||
[Feature.OverlayAnalysisMinDisk13Gb]: 13000,
|
||||
} satisfies Partial<Record<FeatureWithoutCLI, number>>;
|
||||
|
||||
/**
|
||||
* The minimum memory (in MB) that must be available for CodeQL to perform overlay analysis. If
|
||||
@@ -588,24 +592,44 @@ 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 Object.entries(
|
||||
OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE,
|
||||
)) {
|
||||
if (await features.getValue(feature as FeatureWithoutCLI)) {
|
||||
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 +661,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 +672,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 +777,6 @@ export async function checkOverlayEnablement(
|
||||
Feature.OverlayAnalysisSkipResourceChecks,
|
||||
codeql,
|
||||
));
|
||||
const useV2ResourceChecks = await features.getValue(
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
);
|
||||
const checkOverlayStatus = await features.getValue(
|
||||
Feature.OverlayAnalysisStatusCheck,
|
||||
);
|
||||
@@ -770,10 +792,10 @@ export async function checkOverlayEnablement(
|
||||
performResourceChecks && diskUsage !== undefined
|
||||
? await checkRunnerResources(
|
||||
codeql,
|
||||
features,
|
||||
diskUsage,
|
||||
ramInput,
|
||||
logger,
|
||||
useV2ResourceChecks,
|
||||
)
|
||||
: new Success<void>(undefined);
|
||||
if (resourceResult.isFailure()) {
|
||||
|
||||
+51
-14
@@ -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";
|
||||
@@ -13,13 +14,12 @@ import {
|
||||
} from "../testing-utils";
|
||||
|
||||
import { getConfigFileInput, getRemoteConfig } from "./file";
|
||||
import { InputSource } from "./inputs";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
test("getConfigFileInput returns undefined by default", async (t) => {
|
||||
await callee(getConfigFileInput)
|
||||
.withArgs({})
|
||||
.withArgs({}, undefined)
|
||||
.withFeatures([Feature.ConfigFileRepositoryProperty])
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
@@ -41,32 +41,69 @@ test("getConfigFileInput returns input value", async (t) => {
|
||||
.withArgs("config-file")
|
||||
.returns(testInput);
|
||||
})
|
||||
.withArgs(repositoryProperties)
|
||||
.logs(t, "Using config-file input from workflow")
|
||||
.passes(t.deepEqual, { value: testInput, source: InputSource.Workflow });
|
||||
.withArgs(repositoryProperties, undefined)
|
||||
.logs(t, "Using configuration file input from workflow")
|
||||
.passes(t.is, testInput);
|
||||
});
|
||||
|
||||
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)
|
||||
.logs(t, "Using config-file input from repository property")
|
||||
.passes(t.deepEqual, {
|
||||
value: repositoryProperties[RepositoryPropertyName.CONFIG_FILE],
|
||||
source: InputSource.RepositoryProperty,
|
||||
});
|
||||
.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]: " " }, undefined)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
test("getConfigFileInput ignores repository property value when FF is off", async (t) => {
|
||||
// Since the FF is off, we should ignore the repository property value.
|
||||
await callee(getConfigFileInput)
|
||||
.withFeatures([])
|
||||
.withArgs(repositoryProperties)
|
||||
.notLogs(t, "Using config-file input from repository property")
|
||||
.withArgs(repositoryProperties, undefined)
|
||||
.notLogs(t, "Using configuration file input from repository property")
|
||||
.logs(
|
||||
t,
|
||||
"Ignoring config-file input from repository property, because the corresponding feature flag is disabled.",
|
||||
"Ignoring configuration file input from repository property, because the corresponding feature flag is disabled.",
|
||||
)
|
||||
.passes(t.is, undefined);
|
||||
});
|
||||
|
||||
+48
-12
@@ -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";
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
import { ConfigurationError } from "../util";
|
||||
|
||||
import { parseUserConfig, UserConfig } from "./db-config";
|
||||
import { getComputedInput, InputName, type ComputedInput } from "./inputs";
|
||||
import { parseRemoteFileAddress } from "./remote-file";
|
||||
|
||||
/**
|
||||
@@ -29,19 +29,55 @@ export const REMOTE_PATH_PREFIX = "remote=";
|
||||
* Gets the value that is configured for the configuration file, if any.
|
||||
*/
|
||||
export async function getConfigFileInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
{
|
||||
logger,
|
||||
actions,
|
||||
features,
|
||||
}: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
// Only use the repository property value if the FF is enabled.
|
||||
const useRepositoryProperty = await action.features.getValue(
|
||||
Feature.ConfigFileRepositoryProperty,
|
||||
);
|
||||
analysisKinds: AnalysisKind[] | undefined,
|
||||
): Promise<string | undefined> {
|
||||
const input = actions.getOptionalInput("config-file");
|
||||
|
||||
return getComputedInput(action, repositoryProperties, InputName.ConfigFile, {
|
||||
repositoryPropertyFeatureEnabled: useRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: false,
|
||||
repositoryPropertyName: RepositoryPropertyName.CONFIG_FILE,
|
||||
});
|
||||
if (input !== undefined) {
|
||||
logger.info(`Using configuration file input from workflow: ${input}`);
|
||||
return input;
|
||||
}
|
||||
|
||||
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 (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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+10
-62
@@ -3,12 +3,10 @@ import { Feature } from "../feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
StringRepositoryPropertyNames,
|
||||
} from "../feature-flags/properties";
|
||||
|
||||
/** Enumerates input names. */
|
||||
export enum InputName {
|
||||
ConfigFile = "config-file",
|
||||
Tools = "tools",
|
||||
}
|
||||
|
||||
@@ -30,50 +28,26 @@ export type ComputedInput = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents options for how to compute an input.
|
||||
*/
|
||||
export interface ComputedInputOptions {
|
||||
/**
|
||||
* Whether the FF for the repository property (if any) is enabled.
|
||||
*/
|
||||
repositoryPropertyFeatureEnabled: boolean;
|
||||
/**
|
||||
* The name of the repository property to try and get the input value from.
|
||||
*/
|
||||
repositoryPropertyName: StringRepositoryPropertyNames;
|
||||
/**
|
||||
* Whether the repository property value may start with `!` to take precedence
|
||||
* over any input value provided in the workflow file.
|
||||
*/
|
||||
allowForcedRepositoryPropertyValue: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed input for `name`. This comes from either the workflow or
|
||||
* Gets the computed `tools` input. This comes from either the workflow or
|
||||
* the repository property.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param repositoryProperties The values of known repository properties.
|
||||
* @param name The name of the input to compute.
|
||||
* @param options Options for how to compute the input value.
|
||||
*
|
||||
* @returns The computed input or `undefined` if there is no input.
|
||||
*/
|
||||
export async function getComputedInput(
|
||||
export async function getToolsInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: RepositoryProperties,
|
||||
name: InputName,
|
||||
options: ComputedInputOptions,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
const name = InputName.Tools;
|
||||
const input = action.actions.getOptionalInput(name);
|
||||
const propertyValue = repositoryProperties[options.repositoryPropertyName];
|
||||
const propertyValue = repositoryProperties[RepositoryPropertyName.TOOLS];
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
Feature.ToolsRepositoryProperty,
|
||||
);
|
||||
|
||||
// The repository property takes precedence if it starts with an '!'.
|
||||
if (
|
||||
options.repositoryPropertyFeatureEnabled &&
|
||||
options.allowForcedRepositoryPropertyValue &&
|
||||
propertyValue?.startsWith("!")
|
||||
) {
|
||||
if (allowRepositoryProperty && propertyValue?.startsWith("!")) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property (enforced): ${propertyValue}`,
|
||||
);
|
||||
@@ -91,7 +65,7 @@ export async function getComputedInput(
|
||||
}
|
||||
|
||||
// Use the repository property if there's no workflow input.
|
||||
if (options.repositoryPropertyFeatureEnabled && propertyValue !== undefined) {
|
||||
if (allowRepositoryProperty && propertyValue !== undefined) {
|
||||
action.logger.info(
|
||||
`Using ${name} input from repository property: ${propertyValue}`,
|
||||
);
|
||||
@@ -99,34 +73,8 @@ export async function getComputedInput(
|
||||
value: propertyValue,
|
||||
source: InputSource.RepositoryProperty,
|
||||
};
|
||||
} else if (propertyValue !== undefined) {
|
||||
action.logger.info(
|
||||
`Ignoring ${name} input from repository property, because the corresponding feature flag is disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
// There's no input.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the computed `tools` input. This comes from either the workflow or
|
||||
* the repository property.
|
||||
*
|
||||
* @param action The Action state.
|
||||
* @param repositoryProperties The values of known repository properties.
|
||||
* @returns The computed input or `undefined` if there is no input.
|
||||
*/
|
||||
export async function getToolsInput(
|
||||
action: ActionState<["Logger", "Actions", "FeatureFlags"]>,
|
||||
repositoryProperties: Partial<RepositoryProperties>,
|
||||
): Promise<ComputedInput | undefined> {
|
||||
const allowRepositoryProperty = await action.features.getValue(
|
||||
Feature.ToolsRepositoryProperty,
|
||||
);
|
||||
return getComputedInput(action, repositoryProperties, InputName.Tools, {
|
||||
repositoryPropertyFeatureEnabled: allowRepositoryProperty,
|
||||
allowForcedRepositoryPropertyValue: true,
|
||||
repositoryPropertyName: RepositoryPropertyName.TOOLS,
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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.3",
|
||||
"cliVersion": "2.26.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.2",
|
||||
"priorCliVersion": "2.26.2"
|
||||
}
|
||||
+6
-7
@@ -39,12 +39,6 @@ export enum EnvVar {
|
||||
*/
|
||||
CODE_SCANNING_REF = "CODE_SCANNING_REF",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
*/
|
||||
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
|
||||
|
||||
/** Whether the CodeQL Action has invoked the Go autobuilder. */
|
||||
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
|
||||
|
||||
@@ -88,7 +82,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 +264,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);
|
||||
|
||||
+56
-12
@@ -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,37 @@ 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.
|
||||
* Lowers the overlay minimum available disk space to 8 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisResourceChecksV2 = "overlay_analysis_resource_checks_v2",
|
||||
OverlayAnalysisMinDisk8Gb = "overlay_analysis_min_disk_8_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 9 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk9Gb = "overlay_analysis_min_disk_9_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 10 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk10Gb = "overlay_analysis_min_disk_10_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 11 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk11Gb = "overlay_analysis_min_disk_11_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 12 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk12Gb = "overlay_analysis_min_disk_12_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 13 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
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 +211,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 +379,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]: {
|
||||
|
||||
@@ -176,30 +176,6 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"loadPropertiesFromApi returns undefined if non-empty string property is empty",
|
||||
async (t) => {
|
||||
sinon.stub(api, "getRepositoryProperties").resolves({
|
||||
headers: {},
|
||||
status: 200,
|
||||
url: "",
|
||||
data: [{ property_name: "github-codeql-config-file", value: "" }],
|
||||
});
|
||||
const logger = getRunnerLogger(true);
|
||||
const mockRepositoryNwo = parseRepositoryNwo("owner/repo");
|
||||
|
||||
// `github-codeql-config-file` is a `nonEmptyStringProperty`, so we expect to
|
||||
// get `undefined` instead of the empty string
|
||||
const response = await properties.loadPropertiesFromApi(
|
||||
logger,
|
||||
mockRepositoryNwo,
|
||||
);
|
||||
t.deepEqual(response, {
|
||||
"github-codeql-config-file": undefined,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"loadPropertiesFromApi warns if boolean property has unexpected value",
|
||||
async (t) => {
|
||||
|
||||
@@ -22,25 +22,13 @@ export enum RepositoryPropertyName {
|
||||
|
||||
/** Parsed types of the known repository properties. */
|
||||
export type AllRepositoryProperties = {
|
||||
[RepositoryPropertyName.CONFIG_FILE]: string | undefined;
|
||||
[RepositoryPropertyName.CONFIG_FILE]: string;
|
||||
[RepositoryPropertyName.DISABLE_OVERLAY]: boolean;
|
||||
[RepositoryPropertyName.EXTRA_QUERIES]: string;
|
||||
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: boolean;
|
||||
[RepositoryPropertyName.TOOLS]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The subset of known repository properties which are of type `string`.
|
||||
* We tolerate `undefined` for `string`-typed properties that are empty.
|
||||
*/
|
||||
export type StringRepositoryPropertyNames = keyof {
|
||||
[K in keyof AllRepositoryProperties as AllRepositoryProperties[K] extends
|
||||
| string
|
||||
| undefined
|
||||
? K
|
||||
: never]: AllRepositoryProperties[K];
|
||||
};
|
||||
|
||||
/** Parsed repository properties. */
|
||||
export type RepositoryProperties = Partial<AllRepositoryProperties>;
|
||||
|
||||
@@ -84,12 +72,6 @@ const stringProperty = {
|
||||
parse: parseStringRepositoryProperty,
|
||||
};
|
||||
|
||||
/** A repository property that we expect to contain a non-empty string value. */
|
||||
const nonEmptyStringProperty = {
|
||||
...stringProperty,
|
||||
parse: parseNonEmptyStringRepositoryProperty,
|
||||
};
|
||||
|
||||
/** A repository property that we expect to contain a boolean value. */
|
||||
const booleanProperty = {
|
||||
// The value from the API should come as a string, which we then parse into a boolean.
|
||||
@@ -101,7 +83,7 @@ const booleanProperty = {
|
||||
const repositoryPropertyParsers: {
|
||||
[K in RepositoryPropertyName]: PropertyInfo<K>;
|
||||
} = {
|
||||
[RepositoryPropertyName.CONFIG_FILE]: nonEmptyStringProperty,
|
||||
[RepositoryPropertyName.CONFIG_FILE]: stringProperty,
|
||||
[RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty,
|
||||
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
|
||||
[RepositoryPropertyName.FILE_COVERAGE_ON_PRS]: booleanProperty,
|
||||
@@ -246,17 +228,6 @@ function parseStringRepositoryProperty(_name: string, value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Parse a non-empty string repository property. */
|
||||
function parseNonEmptyStringRepositoryProperty(
|
||||
_name: string,
|
||||
value: string,
|
||||
): string | undefined {
|
||||
if (value.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Set of known repository property names, for fast lookups. */
|
||||
const KNOWN_REPOSITORY_PROPERTY_NAMES = new Set<string>(
|
||||
Object.values(RepositoryPropertyName),
|
||||
|
||||
@@ -123,6 +123,7 @@ async function prepareFailedSarif(
|
||||
const category = `/language:${language}`;
|
||||
const checkoutPath = ".";
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -146,6 +147,7 @@ async function prepareFailedSarif(
|
||||
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
|
||||
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -156,6 +158,7 @@ async function prepareFailedSarif(
|
||||
}
|
||||
|
||||
async function generateFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: Config,
|
||||
category: string | undefined,
|
||||
@@ -163,7 +166,7 @@ async function generateFailedSarif(
|
||||
sarifFile?: string,
|
||||
) {
|
||||
const databasePath = config.dbLocation;
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
// Set the filename for the SARIF file if not already set.
|
||||
if (sarifFile === undefined) {
|
||||
|
||||
@@ -75,7 +75,7 @@ async function run(startedAt: Date) {
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
|
||||
);
|
||||
} else {
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
|
||||
uploadFailedSarifResult = await initActionPostHelper.uploadFailureInfo(
|
||||
debugArtifacts.tryUploadAllAvailableDebugArtifacts,
|
||||
|
||||
+13
-22
@@ -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 {
|
||||
@@ -129,7 +128,7 @@ async function sendStartingStatusReport(
|
||||
async function sendCompletedStatusReport(
|
||||
startedAt: Date,
|
||||
config: configUtils.Config | undefined,
|
||||
configFileInput: ComputedInput | undefined,
|
||||
configFile: string | undefined,
|
||||
toolsInput: ComputedInput | undefined,
|
||||
toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined,
|
||||
toolsFeatureFlagsValid: boolean | undefined,
|
||||
@@ -168,9 +167,6 @@ async function sendCompletedStatusReport(
|
||||
if (toolsInput !== undefined) {
|
||||
initStatusReport.computed_inputs.tools = toolsInput;
|
||||
}
|
||||
if (configFileInput !== undefined) {
|
||||
initStatusReport.computed_inputs["config-file"] = configFileInput;
|
||||
}
|
||||
|
||||
const initToolsDownloadFields: InitToolsDownloadFields = {};
|
||||
|
||||
@@ -188,7 +184,7 @@ async function sendCompletedStatusReport(
|
||||
await createInitWithConfigStatusReport(
|
||||
config,
|
||||
initStatusReport,
|
||||
configFileInput,
|
||||
configFile,
|
||||
Math.round(
|
||||
await getTotalCacheSize(Object.values(config.trapCaches), logger),
|
||||
),
|
||||
@@ -215,7 +211,7 @@ async function run(
|
||||
|
||||
let apiDetails: GitHubApiCombinedDetails;
|
||||
let config: configUtils.Config | undefined;
|
||||
let configFileInput: ComputedInput | undefined;
|
||||
let configFile: string | undefined;
|
||||
let codeql: CodeQL;
|
||||
let features: FeatureEnablement;
|
||||
let sourceRoot: string;
|
||||
@@ -258,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 };
|
||||
configFileInput = 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.
|
||||
@@ -293,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);
|
||||
|
||||
@@ -379,7 +372,7 @@ async function run(
|
||||
packsInput: getOptionalInput("packs"),
|
||||
buildModeInput: getOptionalInput("build-mode"),
|
||||
ramInput: getOptionalInput("ram"),
|
||||
configFile: configFileInput?.value,
|
||||
configFile,
|
||||
dbLocation: getOptionalInput("db-location"),
|
||||
configInput: getOptionalInput("config"),
|
||||
dependencyCachingEnabled: getDependencyCachingEnabled(),
|
||||
@@ -696,7 +689,6 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
|
||||
// To check custom query packs for compatibility with overlay analysis, we
|
||||
@@ -725,7 +717,6 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -785,7 +776,7 @@ async function run(
|
||||
await sendCompletedStatusReport(
|
||||
startedAt,
|
||||
config,
|
||||
configFileInput,
|
||||
configFile,
|
||||
toolsInput,
|
||||
toolsDownloadStatusReport,
|
||||
toolsFeatureFlagsValid,
|
||||
|
||||
@@ -89,7 +89,6 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
fs.mkdirSync(config.dbLocation, { recursive: true });
|
||||
await configUtils.wrapEnvironment(
|
||||
@@ -100,7 +99,6 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot,
|
||||
processName,
|
||||
qlconfigFile,
|
||||
logger,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+27
-5
@@ -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;
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function runResolveBuildEnvironment(
|
||||
) {
|
||||
logger.startGroup(`Attempting to resolve build environment for ${language}`);
|
||||
|
||||
const codeql = await getCodeQL(cmd);
|
||||
const codeql = await getCodeQL(logger, cmd);
|
||||
|
||||
if (workingDir !== undefined) {
|
||||
logger.info(`Using ${workingDir} as the working directory.`);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(","),
|
||||
};
|
||||
|
||||
@@ -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
@@ -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"]!);
|
||||
|
||||
+85
-6
@@ -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,
|
||||
@@ -12,21 +14,23 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import { getCachedCodeQlVersion } from "./cli/output-cache";
|
||||
import type { Config } from "./config/action-config";
|
||||
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,
|
||||
getCachedCodeQlVersion,
|
||||
isInTestMode,
|
||||
GITHUB_DOTCOM_URL,
|
||||
DiskUsage,
|
||||
@@ -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.
|
||||
*
|
||||
@@ -298,7 +376,7 @@ export async function createStatusReportBase(
|
||||
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
|
||||
}
|
||||
const runnerOs = getRequiredEnvParam("RUNNER_OS");
|
||||
const codeQlCliVersion = getCachedCodeQlVersion();
|
||||
const codeQlCliVersion = getCachedCodeQlVersion(logger, getEnv());
|
||||
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
|
||||
const testingEnvironment = getTestingEnvironment();
|
||||
// re-export the testing environment variable so that it is available to subsequent steps,
|
||||
@@ -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,
|
||||
@@ -554,7 +633,7 @@ export interface InitToolsDownloadFields {
|
||||
*
|
||||
* @param config The CodeQL Action configuration whose values should be added to the base status report.
|
||||
* @param initStatusReport The base status report.
|
||||
* @param configFileInput Optionally, the filename of the configuration file that was read.
|
||||
* @param configFile Optionally, the filename of the configuration file that was read.
|
||||
* @param totalCacheSize The computed total TRAP cache size.
|
||||
* @param overlayBaseDatabaseStats Statistics about the overlay database, if any.
|
||||
* @returns
|
||||
@@ -562,7 +641,7 @@ export interface InitToolsDownloadFields {
|
||||
export async function createInitWithConfigStatusReport(
|
||||
config: Config,
|
||||
initStatusReport: InitStatusReport,
|
||||
configFileInput: ComputedInput | undefined,
|
||||
configFile: string | undefined,
|
||||
totalCacheSize: number,
|
||||
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
|
||||
dependencyCachingResults: DependencyCacheRestoreStatusReport | undefined,
|
||||
@@ -601,7 +680,7 @@ export async function createInitWithConfigStatusReport(
|
||||
|
||||
return {
|
||||
...initStatusReport,
|
||||
config_file: configFileInput?.value ?? "",
|
||||
config_file: configFile ?? "",
|
||||
disable_default_queries: disableDefaultQueries,
|
||||
paths,
|
||||
paths_ignore: pathsIgnore,
|
||||
|
||||
@@ -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
@@ -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}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+86
-27
@@ -18,6 +18,8 @@ import { AnalysisKind } from "./analyses";
|
||||
import * as apiClient from "./api-client";
|
||||
import { GitHubApiDetails } from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { resetCachedCodeQlVersion } from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import * as codeql from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
@@ -34,11 +36,13 @@ 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 +186,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 +229,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 +246,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 +264,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 +274,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 +360,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 +379,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 +498,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 +524,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.
|
||||
@@ -814,7 +873,7 @@ export const makeVersionInfo = (
|
||||
version: string,
|
||||
features?: { [name: string]: boolean },
|
||||
overlayVersion?: number,
|
||||
): codeql.VersionInfo => ({
|
||||
): VersionInfo => ({
|
||||
version,
|
||||
features,
|
||||
overlayVersion,
|
||||
|
||||
@@ -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
@@ -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}.`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as semver from "semver";
|
||||
|
||||
import type { VersionInfo } from "./codeql";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
|
||||
export enum ToolsFeature {
|
||||
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ async function combineSarifFilesUsingCLI(
|
||||
|
||||
const config = await getConfig(tempDir, logger);
|
||||
if (config !== undefined) {
|
||||
codeQL = await getCodeQL(config.codeQLCmd);
|
||||
codeQL = await getCodeQL(logger, config.codeQLCmd);
|
||||
tempDir = config.tempDir;
|
||||
} else {
|
||||
logger.info(
|
||||
|
||||
@@ -532,58 +532,3 @@ test("Failure.orElse returns the default value for a failure result", (t) => {
|
||||
const result = new util.Failure(new Error("test error"));
|
||||
t.is(result.orElse("default value"), "default value");
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql"), {
|
||||
version: "2.20.0",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
version: { version: "2.20.0" },
|
||||
});
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
(t) => {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = "not valid json";
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
(t) => {
|
||||
for (const value of [
|
||||
JSON.stringify({ cmd: "/path/to/codeql" }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: {} }),
|
||||
JSON.stringify({ cmd: "/path/to/codeql", version: { version: 2 } }),
|
||||
JSON.stringify({ version: { version: "2.20.0" } }),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", overlayVersion: "1" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
version: { version: "2.20.0", features: "nope" },
|
||||
}),
|
||||
]) {
|
||||
process.env[EnvVar.CODEQL_VERSION_INFO] = value;
|
||||
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined, value);
|
||||
}
|
||||
},
|
||||
);
|
||||
+1
-85
@@ -10,7 +10,7 @@ import * as yaml from "js-yaml";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as apiCompatibility from "./api-compatibility.json";
|
||||
import type { CodeQL, VersionInfo } from "./codeql";
|
||||
import type { CodeQL } from "./codeql";
|
||||
import type { Pack } from "./config/db-config";
|
||||
import type { Config } from "./config-utils";
|
||||
import { EnvVar, getRequiredEnvParam } from "./environment";
|
||||
@@ -598,90 +598,6 @@ export function asHTTPError(arg: any): HTTPError | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/** The persisted version together with the CLI path it was obtained from. */
|
||||
interface PersistedVersionInfo {
|
||||
cmd: string;
|
||||
version: VersionInfo;
|
||||
}
|
||||
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
const candidate = x as Partial<VersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.version === "string" &&
|
||||
(candidate.features === undefined ||
|
||||
(typeof candidate.features === "object" &&
|
||||
candidate.features !== null)) &&
|
||||
(candidate.overlayVersion === undefined ||
|
||||
typeof candidate.overlayVersion === "number")
|
||||
);
|
||||
}
|
||||
|
||||
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
|
||||
const candidate = x as Partial<PersistedVersionInfo> | null;
|
||||
return (
|
||||
typeof candidate === "object" &&
|
||||
candidate !== null &&
|
||||
typeof candidate.cmd === "string" &&
|
||||
isVersionInfo(candidate.version)
|
||||
);
|
||||
}
|
||||
|
||||
export function cacheCodeQlVersion(cmd: string, version: VersionInfo): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
core.exportVariable(
|
||||
EnvVar.CODEQL_VERSION_INFO,
|
||||
JSON.stringify({ cmd, version }),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
const serialized = process.env[EnvVar.CODEQL_VERSION_INFO];
|
||||
if (!serialized) {
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isPersistedVersionInfo(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.version;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
export async function codeQlVersionAtLeast(
|
||||
codeql: CodeQL,
|
||||
requiredVersion: string,
|
||||
|
||||
Reference in new issue
Block a user