mirror of
https://github.com/github/codeql-action.git
synced 2026-08-05 13:02:04 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e9e5714ab | |||
| 91fbc53f6a |
@@ -25,6 +25,17 @@ 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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.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@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.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" != "$CODEQL_ACTION_JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$CODEQL_ACTION_JOB_RUN_UUID', but found '$actual'."
|
||||
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$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@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -113,6 +113,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
- language: python
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -51,9 +51,9 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install JavaScript dependencies
|
||||
run: npm ci
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Update git config
|
||||
run: |
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
env:
|
||||
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
|
||||
run: |
|
||||
npx tsx pr-checks/prepare-changelog.ts --output="$PARTIAL_CHANGELOG"
|
||||
python .github/workflows/script/prepare_changelog.py CHANGELOG.md > $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: |
|
||||
npx tsx pr-checks/rollback-changelog.ts \
|
||||
python .github/workflows/script/rollback_changelog.py \
|
||||
--target-version "${ROLLBACK_TAG:1}" \
|
||||
--rollback-version "${LATEST_TAG:1}" \
|
||||
--new-version "$VERSION" > $NEW_CHANGELOG
|
||||
@@ -128,9 +128,7 @@ jobs:
|
||||
NEW_CHANGELOG: "${{ runner.temp }}/new_changelog.md"
|
||||
PARTIAL_CHANGELOG: "${{ runner.temp }}/partial_changelog.md"
|
||||
run: |
|
||||
npx tsx pr-checks/prepare-changelog.ts \
|
||||
--changelog="$NEW_CHANGELOG" \
|
||||
--output="$PARTIAL_CHANGELOG"
|
||||
python .github/workflows/script/prepare_changelog.py $NEW_CHANGELOG > $PARTIAL_CHANGELOG
|
||||
|
||||
echo "::group::Partial CHANGELOG"
|
||||
cat $PARTIAL_CHANGELOG
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/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)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
#!/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))
|
||||
@@ -0,0 +1,62 @@
|
||||
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,6 +40,11 @@ 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:
|
||||
@@ -115,7 +120,7 @@ jobs:
|
||||
|
||||
- name: Create changelog note
|
||||
run: |
|
||||
npx tsx pr-checks/bundle-changelog.ts
|
||||
python .github/workflows/script/bundle_changelog.py
|
||||
|
||||
- name: Push changelog note
|
||||
run: |
|
||||
|
||||
@@ -22,6 +22,11 @@ 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
|
||||
|
||||
@@ -38,7 +43,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: github/enterprise-releases
|
||||
token: ${{ secrets.CODEQL_CI_ENTERPRISE_RELEASE_PAT }}
|
||||
token: ${{ secrets.ENTERPRISE_RELEASE_TOKEN }}
|
||||
path: ${{ github.workspace }}/enterprise-releases/
|
||||
sparse-checkout: releases.json
|
||||
|
||||
|
||||
@@ -4,20 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
|
||||
|
||||
## [UNRELEASED]
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 4.37.6 - 04 Aug 2026
|
||||
|
||||
- Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to `.github/codeql-config.yml` to align it with the suggested path that is used elsewhere. [#4070](https://github.com/github/codeql-action/pull/4070)
|
||||
|
||||
## 4.37.5 - 03 Aug 2026
|
||||
|
||||
- Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the `init` Action instead of falling back to downloading the bundle before extracting it. [#4061](https://github.com/github/codeql-action/pull/4061)
|
||||
|
||||
## 4.37.4 - 29 Jul 2026
|
||||
|
||||
- This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037)
|
||||
- Update default CodeQL bundle version to [2.26.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.2). [#4051](https://github.com/github/codeql-action/pull/4051)
|
||||
|
||||
## 4.37.3 - 22 Jul 2026
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.2",
|
||||
"cliVersion": "2.26.2",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.1",
|
||||
"priorCliVersion": "2.26.1"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
Generated
+659
-920
File diff suppressed because it is too large
Load Diff
Generated
+98
-98
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.7",
|
||||
"version": "4.37.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codeql",
|
||||
"version": "4.37.7",
|
||||
"version": "4.37.4",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks"
|
||||
@@ -31,7 +31,7 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.2.2",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -63,9 +63,9 @@
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.1.0",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"typescript-eslint": "^8.64.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -374,9 +374,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@actions/artifact/node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -2591,17 +2591,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
|
||||
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/type-utils": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -2614,7 +2614,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -2630,16 +2630,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
|
||||
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2673,14 +2673,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2713,14 +2713,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2731,9 +2731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2748,15 +2748,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -2791,9 +2791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2805,16 +2805,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -2843,16 +2843,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
|
||||
@@ -2874,13 +2874,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.8"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
@@ -2890,16 +2890,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2914,13 +2914,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3864,9 +3864,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -5115,16 +5115,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import-x/node_modules/minimatch": {
|
||||
@@ -6111,15 +6111,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
@@ -6981,9 +6981,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.2.tgz",
|
||||
"integrity": "sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
|
||||
"integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8090,15 +8090,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob/node_modules/minimatch": {
|
||||
@@ -8556,9 +8556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sinon": {
|
||||
"version": "22.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-22.1.0.tgz",
|
||||
"integrity": "sha512-n1ajF2rBWMTtEwbKcw4UdFg4nCnDdq/U6RDoxtOd7oapOlRoJ5ynwFx60owROyhDpA9QhMZi0pCO/xtmwFjG7w==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz",
|
||||
"integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -9320,16 +9320,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
|
||||
"integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
|
||||
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "4.37.7",
|
||||
"version": "4.37.4",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -39,7 +39,7 @@
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.2.2",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
@@ -71,9 +71,9 @@
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.1.0",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"typescript-eslint": "^8.64.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/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,18 +5,14 @@
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
@@ -41,14 +37,6 @@ 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
|
||||
|
||||
+50
-127
@@ -2,34 +2,14 @@ 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_PLACEHOLDER}
|
||||
## [UNRELEASED]
|
||||
|
||||
${NO_CHANGES_STR}`;
|
||||
No user facing changes.
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -73,76 +53,7 @@ export function setVersionAndDate(
|
||||
date: Date = new Date(),
|
||||
): string {
|
||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||
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;
|
||||
return content.replace("[UNRELEASED]", versionAndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,58 +66,70 @@ 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\]/;
|
||||
|
||||
// Parse the changelog.
|
||||
const changelog = parseChangelog(content);
|
||||
let output = "";
|
||||
let i = 0;
|
||||
|
||||
if (changelog.sections.length === 0) {
|
||||
// Copy lines until we find the first section heading.
|
||||
let foundFirstSection = false;
|
||||
while (!foundFirstSection && i < lines.length) {
|
||||
let line = lines[i];
|
||||
if (line.startsWith("## ")) {
|
||||
line = line.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
foundFirstSection = true;
|
||||
}
|
||||
output += `${line}\n`;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!foundFirstSection) {
|
||||
throw new Error("Could not find any change sections in CHANGELOG.md");
|
||||
}
|
||||
|
||||
// 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}`,
|
||||
);
|
||||
// Process remaining lines.
|
||||
// `foundContent` tracks whether we hit two headings in a row
|
||||
let foundContent = false;
|
||||
output += "\n";
|
||||
|
||||
const filteredEntries: string[] = [];
|
||||
let foundContent = false;
|
||||
while (i < lines.length) {
|
||||
let line = lines[i];
|
||||
i++;
|
||||
|
||||
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);
|
||||
// Filter out changelog entries that only apply to newer versions.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (match) {
|
||||
if (
|
||||
match &&
|
||||
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep the line.
|
||||
filteredEntries.push(line);
|
||||
|
||||
// Set `foundContent` to `true` if the line is not empty.
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
if (line.startsWith("## ")) {
|
||||
line = line.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
if (!foundContent) {
|
||||
output += "No user facing changes.\n";
|
||||
}
|
||||
foundContent = false;
|
||||
output += `\n${line}\n\n`;
|
||||
} else {
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
output += `${line}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderChangelog(changelog);
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ steps:
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/results"
|
||||
actual=$(jq -r '.runs[0].properties.jobRunUuid' javascript.sarif)
|
||||
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'."
|
||||
if [[ "$actual" != "$JOB_RUN_UUID" ]]; then
|
||||
echo "Expected SARIF output to contain job run UUID '$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@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
||||
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
|
||||
with:
|
||||
ruby-version: 2.6
|
||||
- name: Install Code Scanning integration
|
||||
|
||||
@@ -37,10 +37,6 @@ 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. */
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* 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, "");
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/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());
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/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",
|
||||
"b6effb05e454b25005698d916606bdc6ffcbf961",
|
||||
"v5.7.0",
|
||||
"03ad4de0992f5dab5e18fcb136590ce7c4a0ac95",
|
||||
"v5.6.0",
|
||||
),
|
||||
with: {
|
||||
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
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",
|
||||
});
|
||||
+4
-26
@@ -8,10 +8,9 @@ import { getActionsLogger, Logger } from "./logging";
|
||||
import {
|
||||
ActionName,
|
||||
getDisplayActionName,
|
||||
getJobUUID,
|
||||
sendUnhandledErrorStatusReport,
|
||||
} from "./status-report";
|
||||
import { getEnv, getErrorMessage, wrapError } from "./util";
|
||||
import { getEnv, getErrorMessage } from "./util";
|
||||
|
||||
/** Base state that is available to an Action on startup. */
|
||||
export interface BaseState {
|
||||
@@ -79,12 +78,6 @@ 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. */
|
||||
@@ -95,32 +88,17 @@ export async function runInActions(action: Action) {
|
||||
const actionsEnv = getActionsEnv();
|
||||
|
||||
try {
|
||||
const actionState = {
|
||||
await action.run({
|
||||
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)}`,
|
||||
);
|
||||
|
||||
const statusReportError =
|
||||
action.transformTelemetryError !== undefined
|
||||
? action.transformTelemetryError(wrapError(error))
|
||||
: error;
|
||||
await sendUnhandledErrorStatusReport(
|
||||
action.name,
|
||||
startedAt,
|
||||
statusReportError,
|
||||
logger,
|
||||
);
|
||||
await sendUnhandledErrorStatusReport(action.name, startedAt, error, logger);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-7
@@ -27,20 +27,14 @@ 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 {
|
||||
getRequiredInput,
|
||||
getOptionalInput,
|
||||
exportVariable: core.exportVariable,
|
||||
};
|
||||
return { getOptionalInput };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2544,6 +2544,7 @@ test("loadUserConfig - loads local configuration files", async (t) => {
|
||||
) =>
|
||||
configUtils.loadUserConfig(
|
||||
actionState,
|
||||
[AnalysisKind.CodeScanning],
|
||||
filePath,
|
||||
workspaceDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
@@ -2587,12 +2588,19 @@ test.serial("loadUserConfig - loads remote configuration files", async (t) => {
|
||||
|
||||
const remoteAddress = "owner/repo/file@ref";
|
||||
await callee(configUtils.loadUserConfig)
|
||||
.withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir)
|
||||
.withArgs(
|
||||
[AnalysisKind.CodeScanning],
|
||||
remoteAddress,
|
||||
tmpDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
)
|
||||
.passes(t.deepEqual, {});
|
||||
|
||||
t.true(
|
||||
getRemoteConfig.calledOnceWithExactly(
|
||||
sinon.match.any,
|
||||
[AnalysisKind.CodeScanning],
|
||||
remoteAddress,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
),
|
||||
@@ -2626,9 +2634,9 @@ test.serial(
|
||||
// match our expectations. We break it down like this to get
|
||||
// more useful test output.
|
||||
const args = getRemoteConfig.getCalls()[0].args;
|
||||
t.is(args.length, 3);
|
||||
t.deepEqual(args[1], address);
|
||||
t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS);
|
||||
t.is(args.length, 4);
|
||||
t.deepEqual(args[2], address);
|
||||
t.deepEqual(args[3], SAMPLE_DOTCOM_API_DETAILS);
|
||||
};
|
||||
|
||||
// Utility function to assert that `targetWithArgs` has not identified
|
||||
@@ -2665,6 +2673,7 @@ test.serial(
|
||||
|
||||
// Prepare the test call to `loadUserConfig`.
|
||||
const targetWithArgs = target.withArgs(
|
||||
[AnalysisKind.CodeScanning],
|
||||
address,
|
||||
tmpDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
|
||||
+9
-1
@@ -484,6 +484,7 @@ async function downloadCacheWithTime(
|
||||
*/
|
||||
export async function loadUserConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
analysisKinds: AnalysisKind[],
|
||||
configFile: string,
|
||||
workspacePath: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
@@ -511,7 +512,12 @@ export async function loadUserConfig(
|
||||
if (isExplicitRemotePath(configFile)) {
|
||||
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
|
||||
}
|
||||
return await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
return await getRemoteConfig(
|
||||
actionState,
|
||||
analysisKinds,
|
||||
configFile,
|
||||
apiDetails,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,6 +1077,7 @@ export async function determineUserConfig(
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
action,
|
||||
inputs.analysisKinds,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
@@ -1118,6 +1125,7 @@ export async function determineUserConfig(
|
||||
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
|
||||
return await loadUserConfig(
|
||||
action,
|
||||
inputs.analysisKinds,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
|
||||
+71
-1
@@ -13,6 +13,7 @@ import {
|
||||
setupTests,
|
||||
} from "../testing-utils";
|
||||
|
||||
import type { UserConfig } from "./db-config";
|
||||
import { getConfigFileInput, getRemoteConfig } from "./file";
|
||||
|
||||
setupTests(test);
|
||||
@@ -137,7 +138,11 @@ test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => {
|
||||
|
||||
const target = callee(getRemoteConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs("file.yml", SAMPLE_DOTCOM_API_DETAILS);
|
||||
.withArgs(
|
||||
[AnalysisKind.CodeScanning],
|
||||
"file.yml",
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
// Should use it when the FF is enabled and the environment variables are set.
|
||||
await target
|
||||
@@ -164,3 +169,68 @@ test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => {
|
||||
.notLogs(t, "Using private registry proxy at 'http://localhost:1234'")
|
||||
.throws(t, { message: errorMessage });
|
||||
});
|
||||
|
||||
test.serial("getRemoteConfig replaces meta variables", async (t) => {
|
||||
const client = github.getOctokit("123");
|
||||
const response = {
|
||||
data: {
|
||||
content: Buffer.from("disable-default-queries: false").toString("base64"),
|
||||
},
|
||||
};
|
||||
sinon.stub(client.rest.repos, "getContent").callsFake((params) => {
|
||||
if (params?.path.endsWith("$kind.yml")) {
|
||||
throw new Error(`Unexpected request path: ${params.path}`);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response as any;
|
||||
});
|
||||
|
||||
sinon
|
||||
.stub(api, "getApiClientWithExternalAuth")
|
||||
.callsFake((_details, _proxy) => {
|
||||
return client;
|
||||
});
|
||||
|
||||
const target = callee(getRemoteConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(
|
||||
[AnalysisKind.CodeScanning],
|
||||
"owner/repo:file-$kind.yml",
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
);
|
||||
|
||||
// Should replace the meta variable if the FF is enabled.
|
||||
await target
|
||||
.withFeatures([Feature.RemoteAddressAnalysisMetaVar])
|
||||
.logs(
|
||||
t,
|
||||
"Remote file address after replacing meta variables: owner/repo:file-code-scanning.yml",
|
||||
)
|
||||
.passes(t.deepEqual, {
|
||||
"disable-default-queries": false,
|
||||
} satisfies UserConfig);
|
||||
|
||||
// But not if the FF is off.
|
||||
await target.throws(t, {
|
||||
instanceOf: Error,
|
||||
message: "Unexpected request path: file-$kind.yml",
|
||||
});
|
||||
|
||||
// Or if there are multiple analysis kinds.
|
||||
await callee(getRemoteConfig)
|
||||
.withDefaultActionsEnv()
|
||||
.withArgs(
|
||||
[AnalysisKind.CodeScanning, AnalysisKind.CodeQuality],
|
||||
"owner/repo:file-$kind.yml",
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
)
|
||||
.withFeatures([Feature.RemoteAddressAnalysisMetaVar])
|
||||
.logs(
|
||||
t,
|
||||
`Ignoring '${Feature.RemoteAddressAnalysisMetaVar}' feature, because multiple analysis kinds are enabled.`,
|
||||
)
|
||||
.throws(t, {
|
||||
instanceOf: Error,
|
||||
message: "Unexpected request path: file-$kind.yml",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,14 @@ export async function getConfigFileInput(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Replaces supported meta variables in `configFileAddress`. */
|
||||
export function replaceMetaVars(
|
||||
configFileAddress: string,
|
||||
analysisKind: AnalysisKind,
|
||||
): string {
|
||||
return configFileAddress.replaceAll("$kind", analysisKind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to fetch a `UserConfig` from a remote `address`.
|
||||
*
|
||||
@@ -91,9 +99,25 @@ export async function getConfigFileInput(
|
||||
*/
|
||||
export async function getRemoteConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
analysisKinds: AnalysisKind[],
|
||||
configFile: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
): Promise<UserConfig> {
|
||||
const supportMetaVar = await actionState.features.getValue(
|
||||
Feature.RemoteAddressAnalysisMetaVar,
|
||||
);
|
||||
|
||||
if (supportMetaVar && analysisKinds.length === 1) {
|
||||
configFile = replaceMetaVars(configFile, analysisKinds[0]);
|
||||
actionState.logger.debug(
|
||||
`Remote file address after replacing meta variables: ${configFile}`,
|
||||
);
|
||||
} else if (supportMetaVar) {
|
||||
actionState.logger.warning(
|
||||
`Ignoring '${Feature.RemoteAddressAnalysisMetaVar}' feature, because multiple analysis kinds are enabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
const address = await parseRemoteFileAddress(actionState, configFile);
|
||||
|
||||
const shouldProxyRequest = await actionState.features.getValue(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from "ava";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { ActionsEnv } from "../actions-util";
|
||||
import { getActionsEnv } from "../actions-util";
|
||||
import { Feature } from "../feature-flags";
|
||||
import { RepositoryPropertyName } from "../feature-flags/properties";
|
||||
import { callee } from "../testing-utils";
|
||||
@@ -22,26 +22,32 @@ const expectedRepositoryPropertyResult: ComputedInput = {
|
||||
value: "repo-property-input-value",
|
||||
};
|
||||
|
||||
function stubGetToolsInput(actions: ActionsEnv) {
|
||||
function stubGetToolsInput() {
|
||||
const actions = getActionsEnv();
|
||||
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(stubGetToolsInput)
|
||||
.withActions(actions)
|
||||
.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(stubGetToolsInput)
|
||||
.withActions(actions)
|
||||
.withArgs({
|
||||
[RepositoryPropertyName.TOOLS]: `!${expectedRepositoryPropertyResult.value}`,
|
||||
});
|
||||
@@ -59,8 +65,10 @@ 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(stubGetToolsInput)
|
||||
.withActions(actions)
|
||||
.withArgs({
|
||||
[RepositoryPropertyName.TOOLS]: expectedRepositoryPropertyResult.value,
|
||||
});
|
||||
|
||||
@@ -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-config.yml";
|
||||
export const DEFAULT_CONFIG_FILE_NAME = ".github/codeql-action.yaml";
|
||||
|
||||
/** The default ref to use in configuration file shorthands. */
|
||||
export const DEFAULT_CONFIG_FILE_REF = "main";
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.26.2",
|
||||
"cliVersion": "2.26.2",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.1",
|
||||
"priorCliVersion": "2.26.1"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
+1
-6
@@ -88,7 +88,7 @@ export enum EnvVar {
|
||||
LOG_VERSION_DEPRECATION = "CODEQL_ACTION_DID_LOG_VERSION_DEPRECATION",
|
||||
|
||||
/** UUID representing the current job run. */
|
||||
JOB_RUN_UUID = "CODEQL_ACTION_JOB_RUN_UUID",
|
||||
JOB_RUN_UUID = "JOB_RUN_UUID",
|
||||
|
||||
/** Status for the entire job, submitted to the status report in `init-post` */
|
||||
JOB_STATUS = "CODEQL_ACTION_JOB_STATUS",
|
||||
@@ -270,11 +270,6 @@ 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);
|
||||
|
||||
@@ -137,6 +137,8 @@ export enum Feature {
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
/** Adds support for an analysis kind meta variable in remote addresses. */
|
||||
RemoteAddressAnalysisMetaVar = "remote_address_analysis_meta_var",
|
||||
/** Note that this currently only disables baseline file coverage information. */
|
||||
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
|
||||
StartProxyUseFeaturesRelease = "start_proxy_use_features_release",
|
||||
@@ -385,6 +387,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_PROXY_API_REQUESTS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.RemoteAddressAnalysisMetaVar]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_REMOTE_ADDRESS_ANALYSIS_META_VAR",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.SkipFileCoverageOnPrs]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 {
|
||||
@@ -254,6 +255,11 @@ 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");
|
||||
|
||||
// path.resolve() respects the intended semantics of source-root. If
|
||||
|
||||
+5
-27
@@ -35,11 +35,6 @@ 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,
|
||||
@@ -67,11 +62,14 @@ function defaultCheck(
|
||||
return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) });
|
||||
}
|
||||
|
||||
function makeValidator<T>(validate: (arg: unknown) => arg is T) {
|
||||
function makeValidator<T>(
|
||||
validate: (arg: unknown) => arg is T,
|
||||
required: boolean = true,
|
||||
) {
|
||||
return {
|
||||
validate,
|
||||
check: defaultCheck(validate),
|
||||
required: true,
|
||||
required,
|
||||
} as const satisfies Validator<T>;
|
||||
}
|
||||
|
||||
@@ -84,9 +82,6 @@ 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) => {
|
||||
@@ -226,23 +221,6 @@ export function validateSchema<
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that `arr` is an array whose elements satisfy at least `elementSchema`.
|
||||
* Additional keys are accepted in each element.
|
||||
*
|
||||
* @param elementSchema The schema to validate the elements against.
|
||||
* @param arr The array to validate.
|
||||
* @returns Asserts that `arr` has elements of `schema`'s type if validation is successful.
|
||||
*/
|
||||
export function validateArray<
|
||||
S extends Schema,
|
||||
T extends UnvalidatedArray = Array<FromSchema<S>>,
|
||||
>(elementSchema: S, arr: UnvalidatedArray): arr is T {
|
||||
const elementValidator = object(elementSchema);
|
||||
|
||||
return array(elementValidator).validate(arr);
|
||||
}
|
||||
|
||||
export interface CheckSchemaOptions {
|
||||
/** Whether to stop validation after the first error. */
|
||||
failFast?: boolean;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
@@ -94,7 +95,7 @@ async function sendCompletedStatusReport(
|
||||
|
||||
/** The main behaviour of this action. */
|
||||
async function run(
|
||||
actionState: ActionState<["Base", "Logger", "Env", "Actions"]>,
|
||||
actionState: ActionState<["Base", "Logger", "Actions"]>,
|
||||
): Promise<void> {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
@@ -139,6 +140,10 @@ 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",
|
||||
|
||||
+19
-14
@@ -3,12 +3,11 @@ 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 { Logger } from "./logging";
|
||||
import { getActionsLogger, Logger } from "./logging";
|
||||
import { getRepositoryNwo } from "./repository";
|
||||
import {
|
||||
credentialToStr,
|
||||
@@ -24,14 +23,14 @@ import {
|
||||
import { generateCertificateAuthority } from "./start-proxy/ca";
|
||||
import { checkProxyEnvironment } from "./start-proxy/environment";
|
||||
import { checkConnections } from "./start-proxy/reachability";
|
||||
import { ActionName } from "./status-report";
|
||||
import { ActionName, sendUnhandledErrorStatusReport } from "./status-report";
|
||||
import * as util from "./util";
|
||||
|
||||
async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
async function run(startedAt: Date) {
|
||||
// To capture errors appropriately, keep as much code within the try-catch as
|
||||
// possible, and only use safe functions outside.
|
||||
const startedAt = action.startedAt;
|
||||
const logger = action.logger;
|
||||
|
||||
const logger = getActionsLogger();
|
||||
let features: FeatureEnablement | undefined;
|
||||
let language: BuiltInLanguage | undefined;
|
||||
|
||||
@@ -123,15 +122,21 @@ async function run(action: ActionState<["Base", "Logger", "Env", "Actions"]>) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Defines the `start-proxy` Action. */
|
||||
const startProxyAction: Action = {
|
||||
name: ActionName.StartProxy,
|
||||
run,
|
||||
transformTelemetryError: getSafeErrorMessage,
|
||||
};
|
||||
|
||||
export async function runWrapper() {
|
||||
await runInActions(startProxyAction);
|
||||
const startedAt = new Date();
|
||||
const logger = getActionsLogger();
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function startProxy(
|
||||
|
||||
+7
-1
@@ -83,6 +83,12 @@ 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.
|
||||
*
|
||||
@@ -106,7 +112,7 @@ export async function sendSuccessStatusReport(
|
||||
logger,
|
||||
);
|
||||
if (statusReportBase !== undefined) {
|
||||
const statusReport: StatusReportBase = {
|
||||
const statusReport: StartProxyStatus = {
|
||||
...statusReportBase,
|
||||
registry_types: registry_types.join(","),
|
||||
};
|
||||
|
||||
@@ -254,19 +254,13 @@ export function credentialToStr(credential: Credential): string {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The schema for `RegistryBase` objects. */
|
||||
export const registryBaseSchema = {
|
||||
/** The type of the package registry. */
|
||||
type: json.string,
|
||||
/** Whether the registry replaces the base registry for the ecosystem. */
|
||||
"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;
|
||||
export type Registry = {
|
||||
/** The type of the package registry. */
|
||||
type: string;
|
||||
/** Whether the registry replaces the base registry for the ecosystem. */
|
||||
"replaces-base"?: boolean;
|
||||
} & Address;
|
||||
|
||||
// If a registry has an `url`, then that takes precedence over the `host` which may or may
|
||||
// not be defined.
|
||||
|
||||
+1
-104
@@ -1,21 +1,17 @@
|
||||
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, RegistryProxyVars } from "./environment";
|
||||
import { EnvVar } 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";
|
||||
@@ -24,106 +20,11 @@ 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",
|
||||
@@ -133,9 +34,6 @@ 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");
|
||||
@@ -179,7 +77,6 @@ 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"]!);
|
||||
|
||||
+1
-80
@@ -1,9 +1,7 @@
|
||||
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,
|
||||
@@ -19,14 +17,12 @@ 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, getEnv, ReadOnlyEnv, RegistryProxyVars } from "./environment";
|
||||
import { EnvVar } 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,
|
||||
@@ -63,30 +59,6 @@ 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.
|
||||
*
|
||||
@@ -187,12 +159,6 @@ 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. */
|
||||
@@ -296,50 +262,6 @@ 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.
|
||||
*
|
||||
@@ -402,7 +324,6 @@ export async function createStatusReportBase(
|
||||
job_name: jobName,
|
||||
job_run_uuid: jobRunUUID,
|
||||
ref,
|
||||
registry_types: getRegistryTypesFromEnv(logger),
|
||||
runner_os: runnerOs,
|
||||
started_at: workflowStartedAt,
|
||||
status,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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/,
|
||||
});
|
||||
});
|
||||
});
|
||||
+4
-9
@@ -194,15 +194,10 @@ export async function extractTarZst(
|
||||
});
|
||||
|
||||
if (tar instanceof stream.Readable) {
|
||||
// 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}`),
|
||||
);
|
||||
}
|
||||
tar.pipe(tarProcess.stdin).on("error", (err) => {
|
||||
reject(
|
||||
new Error(`Error while downloading and extracting tar: ${err}`),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+25
-83
@@ -34,14 +34,11 @@ 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 = {
|
||||
@@ -185,32 +182,13 @@ 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(env: Env): TestActionsEnv {
|
||||
return new TestActionsEnv(env);
|
||||
export function getTestActionsEnv(): ActionsEnv {
|
||||
return {
|
||||
getOptionalInput: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/** For testing purposes, we make all available state features accessible in `TestEnv`. */
|
||||
@@ -228,13 +206,12 @@ 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,
|
||||
actions: getTestActionsEnv(env),
|
||||
env: getTestEnv(),
|
||||
actions: getTestActionsEnv(),
|
||||
apiClient: github.getOctokit("123"),
|
||||
features: createFeatures([]),
|
||||
...overrides,
|
||||
@@ -245,13 +222,9 @@ type DelayedCheck<
|
||||
Args extends readonly any[],
|
||||
R,
|
||||
Fs extends ReadonlyArray<AllState[number]>,
|
||||
> = (
|
||||
env: Readonly<BaseEnvBuilder<Args, R, Fs>>,
|
||||
result: Result<Awaited<R>, ThrownError<ErrorConstructor | Error>>,
|
||||
) => Promise<any>;
|
||||
> = (env: Readonly<BaseEnvBuilder<Args, R, Fs>>) => Promise<any>;
|
||||
|
||||
export type Mutation<T> = (val: T) => void;
|
||||
export type ValueOrMutation<T> = T | Mutation<T>;
|
||||
export type ValueOrMutation<T> = T | ((val: T) => void);
|
||||
|
||||
/**
|
||||
* Wraps a function that accepts an `ActionState` for testing in different environments.
|
||||
@@ -263,7 +236,6 @@ 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>>;
|
||||
|
||||
@@ -273,26 +245,15 @@ abstract class BaseEnvBuilder<
|
||||
) {
|
||||
this.fn = fn;
|
||||
this.logger = new RecordingLogger();
|
||||
|
||||
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.state =
|
||||
cloneFrom !== undefined
|
||||
? ({
|
||||
...cloneFrom.state,
|
||||
env: cloneFrom.state.env.clone(),
|
||||
actions: Object.create(cloneFrom.state.actions),
|
||||
logger: this.logger,
|
||||
} satisfies ActionState<AllState>)
|
||||
: initAllState({ logger: this.logger });
|
||||
this.checks = [...(cloneFrom?.checks ?? [])];
|
||||
}
|
||||
|
||||
@@ -359,10 +320,13 @@ abstract class BaseEnvBuilder<
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Applies `fn` to the `ActionsEnv`. */
|
||||
public withActions(fn: Mutation<ActionsEnv>): this {
|
||||
public withActions(arg: ValueOrMutation<ActionsEnv>): this {
|
||||
const result = this.clone();
|
||||
fn(result.state.actions);
|
||||
if (typeof arg === "function") {
|
||||
arg(result.state.actions);
|
||||
} else {
|
||||
result.state.actions = arg;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -378,28 +342,6 @@ 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.
|
||||
@@ -497,7 +439,7 @@ class CallableEnvBuilder<
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this, new Success(result));
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the results of the function call and the main assertion.
|
||||
@@ -523,7 +465,7 @@ class CallableEnvBuilder<
|
||||
|
||||
// Run other delayed checks.
|
||||
for (const delayedCheck of this.checks) {
|
||||
await delayedCheck(this, new Failure(error));
|
||||
await delayedCheck(this);
|
||||
}
|
||||
|
||||
// Return the error.
|
||||
|
||||
@@ -38,43 +38,6 @@ 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) => {
|
||||
|
||||
+4
-24
@@ -19,12 +19,6 @@ 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.
|
||||
*/
|
||||
@@ -143,8 +137,8 @@ async function downloadAndExtractZstdWithStreaming(
|
||||
authorization ? { authorization } : {},
|
||||
headers,
|
||||
);
|
||||
const response = await new Promise<IncomingMessage>((resolve, reject) => {
|
||||
const request = https.get(
|
||||
const response = await new Promise<IncomingMessage>((resolve) =>
|
||||
https.get(
|
||||
codeqlURL,
|
||||
{
|
||||
headers,
|
||||
@@ -154,24 +148,10 @@ 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}.`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user