mirror of
https://github.com/github/codeql-action.git
synced 2026-11-04 20:20:23 +00:00
Compare commits
4 Commits
v3.38.0
...
mbg/config
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af8f50bf94 | ||
|
|
f638e8b5fb | ||
|
|
391a05d414 | ||
|
|
fdf46cd70c |
2
.github/actions/check-sarif/action.yml
vendored
2
.github/actions/check-sarif/action.yml
vendored
@@ -16,5 +16,5 @@ inputs:
|
||||
Comma separated list of query ids that should NOT be included in this SARIF file.
|
||||
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: index.js
|
||||
|
||||
11
.github/actions/release-initialise/action.yml
vendored
11
.github/actions/release-initialise/action.yml
vendored
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Verify that the best-effort debug artifact scan completed
|
||||
description: Verifies that the best-effort debug artifact scan completed successfully during tests
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: index.js
|
||||
post: post.js
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
applyTo: "CHANGELOG.md,src/defaults.json,lib/defaults.json,src/api-compatibility.json"
|
||||
---
|
||||
|
||||
# Merging release, mergeback, and backport PRs
|
||||
|
||||
The release process creates a cascade of PRs (`main` → `releases/vN`, then
|
||||
`releases/vN` → `main` mergeback, then `releases/vN` → `releases/v(N-1)`
|
||||
backport). These PRs reliably touch `CHANGELOG.md`, `src/defaults.json` /
|
||||
`lib/defaults.json` (bundle/CLI version bump), and `src/api-compatibility.json`.
|
||||
|
||||
Such PRs **must be merged with a merge commit**. Never squash or rebase, as
|
||||
that breaks the branch linkage the release automation relies on.
|
||||
|
||||
When arming auto-merge on these PRs, use `--merge` (e.g. `gh pr merge --merge`),
|
||||
not `--squash` or `--rebase`.
|
||||
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
2
.github/workflows/__all-platform-bundle.yml
generated
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
version: nightly-latest
|
||||
- os: macos-latest-xlarge
|
||||
- os: macos-latest
|
||||
version: nightly-latest
|
||||
- os: windows-latest
|
||||
version: nightly-latest
|
||||
|
||||
2
.github/workflows/__autobuild-direct-tracing-with-working-dir.yml
generated
vendored
2
.github/workflows/__autobuild-direct-tracing-with-working-dir.yml
generated
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
2
.github/workflows/__build-mode-autobuild.yml
generated
vendored
2
.github/workflows/__build-mode-autobuild.yml
generated
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install Java
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||
with:
|
||||
java-version: ${{ inputs.java-version || '17' }}
|
||||
distribution: temurin
|
||||
|
||||
4
.github/workflows/__job-run-uuid-sarif.yml
generated
vendored
4
.github/workflows/__job-run-uuid-sarif.yml
generated
vendored
@@ -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'."
|
||||
|
||||
106
.github/workflows/__linux-arm64.yml
generated
vendored
106
.github/workflows/__linux-arm64.yml
generated
vendored
@@ -1,106 +0,0 @@
|
||||
# Warning: This file is generated automatically, and should not be modified.
|
||||
# Instead, please modify the template in the pr-checks directory and run:
|
||||
# pr-checks/sync.sh
|
||||
# to regenerate this file.
|
||||
|
||||
name: PR Check - Linux Arm64
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GO111MODULE: auto
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- releases/v*
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types:
|
||||
- checks_requested
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dotnet-version:
|
||||
type: string
|
||||
description: The version of .NET to install
|
||||
required: false
|
||||
default: 9.x
|
||||
go-version:
|
||||
type: string
|
||||
description: The version of Go to install
|
||||
required: false
|
||||
default: '>=1.21.0'
|
||||
workflow_call:
|
||||
inputs:
|
||||
dotnet-version:
|
||||
type: string
|
||||
description: The version of .NET to install
|
||||
required: false
|
||||
default: 9.x
|
||||
go-version:
|
||||
type: string
|
||||
description: The version of Go to install
|
||||
required: false
|
||||
default: '>=1.21.0'
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
|
||||
jobs:
|
||||
linux-arm64:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-24.04-arm
|
||||
version: nightly-latest
|
||||
name: Linux Arm64
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: read
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
|
||||
with:
|
||||
dotnet-version: ${{ inputs.dotnet-version || '9.x' }}
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: ${{ inputs.go-version || '>=1.21.0' }}
|
||||
cache: false
|
||||
- name: Prepare test
|
||||
id: prepare-test
|
||||
uses: ./.github/actions/prepare-test
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
use-all-platform-bundle: 'false'
|
||||
setup-kotlin: 'true'
|
||||
- uses: ./../action/init
|
||||
with:
|
||||
languages: ${{ env.LANGUAGES }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Assert databases exist
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/codeql_databases"
|
||||
for lang in ${LANGUAGES//,/ }; do
|
||||
if [[ ! -d "$lang" ]]; then
|
||||
echo "Did not find a database for $lang"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found database for $lang"
|
||||
done
|
||||
env:
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
1
.github/workflows/__multi-language-autodetect.yml
generated
vendored
@@ -191,6 +191,5 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
|
||||
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
2
.github/workflows/__rubocop-multi-language.yml
generated
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/__swift-custom-build.yml
generated
vendored
6
.github/workflows/__swift-custom-build.yml
generated
vendored
@@ -54,11 +54,11 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest-xlarge
|
||||
- os: macos-latest
|
||||
version: linked
|
||||
- os: macos-latest-xlarge
|
||||
- os: macos-latest
|
||||
version: default
|
||||
- os: macos-latest-xlarge
|
||||
- os: macos-latest
|
||||
version: nightly-latest
|
||||
name: Swift analysis using a custom build command
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -113,6 +113,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
- language: python
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
8
.github/workflows/post-release-mergeback.yml
vendored
8
.github/workflows/post-release-mergeback.yml
vendored
@@ -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
|
||||
|
||||
34
.github/workflows/pr-checks.yml
vendored
34
.github/workflows/pr-checks.yml
vendored
@@ -67,43 +67,11 @@ jobs:
|
||||
|
||||
- name: Upload sarif
|
||||
uses: ./upload-sarif
|
||||
# The merge queue deletes its `gh-readonly-queue` ref as soon as the queue entry resolves,
|
||||
# so uploading against it races with that deletion. Both the `merge_group` run and the
|
||||
# paired `push` run that the queue branch creates use that ref, so gate on the ref itself
|
||||
# rather than the event. The same results are uploaded by the `pull_request` run and again
|
||||
# by the `push` run on `main`.
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24 && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/')
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
|
||||
with:
|
||||
sarif_file: eslint.sarif
|
||||
category: eslint
|
||||
|
||||
changetool-tests:
|
||||
name: changetool unit tests
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: pr-checks-changetool-tests-${{ github.ref }}-${{ github.event_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run changetool unit tests
|
||||
run: npm --workspace changetool test
|
||||
|
||||
# These checks do not need to be run as part of the same matrix that we use for the `unit-tests`
|
||||
# job.
|
||||
other-checks:
|
||||
|
||||
6
.github/workflows/rollback-release.yml
vendored
6
.github/workflows/rollback-release.yml
vendored
@@ -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
|
||||
|
||||
23
.github/workflows/script/bundle_changelog.py
vendored
Executable file
23
.github/workflows/script/bundle_changelog.py
vendored
Executable file
@@ -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
.github/workflows/script/prepare_changelog.py
vendored
Executable file
35
.github/workflows/script/prepare_changelog.py
vendored
Executable file
@@ -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))
|
||||
62
.github/workflows/script/rollback_changelog.py
vendored
Normal file
62
.github/workflows/script/rollback_changelog.py
vendored
Normal 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)
|
||||
7
.github/workflows/update-bundle.yml
vendored
7
.github/workflows/update-bundle.yml
vendored
@@ -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
|
||||
|
||||
|
||||
109
CHANGELOG.md
109
CHANGELOG.md
@@ -2,88 +2,61 @@
|
||||
|
||||
See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs.
|
||||
|
||||
## 3.38.0 - 09 Sept 2026
|
||||
|
||||
- On GitHub-hosted runners, the CodeQL Action now deletes unused CodeQL bundles from the toolcache before downloading a different bundle, which frees up disk space for the analysis. We expect to roll this change out to everyone in September. [#4124](https://github.com/github/codeql-action/pull/4124)
|
||||
- The CodeQL Action now supports CodeQL releases that are compatible with Linux Arm64 and downloads the native `linux-arm64` CodeQL bundle when available. [#4072](https://github.com/github/codeql-action/pull/4072)
|
||||
- Update default CodeQL bundle version to [2.27.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.0). [#4129](https://github.com/github/codeql-action/pull/4129)
|
||||
|
||||
## 3.37.9 - 26 Aug 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.4](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.4). [#4106](https://github.com/github/codeql-action/pull/4106)
|
||||
|
||||
## 3.37.8 - 21 Aug 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.37.7 - 13 Aug 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3). [#4085](https://github.com/github/codeql-action/pull/4085)
|
||||
|
||||
## 3.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)
|
||||
|
||||
## 3.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)
|
||||
|
||||
## 3.37.4 - 29 Jul 2026
|
||||
## [UNRELEASED]
|
||||
|
||||
- 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)
|
||||
|
||||
## 3.37.3 - 22 Jul 2026
|
||||
## 4.37.3 - 22 Jul 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.37.2 - 21 Jul 2026
|
||||
## 4.37.2 - 21 Jul 2026
|
||||
|
||||
- The new address format for the `config-file` input that was introduced in CodeQL Action 4.37.0 is now enabled by default. In addition to the format described there, the `remote=` prefix can now be used to explicitly indicate that the input refers to a remote file. All previous input formats continue to be accepted as well. [#4023](https://github.com/github/codeql-action/pull/4023)
|
||||
- The CodeQL Action can now make use of [configured private registries](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries) in Default Setup to retrieve CodeQL configuration files from remote repositories that require authentication. This will allow customers to store their CodeQL configuration in a single repository that can then be referenced by Default Setup workflows in other repositories. We expect to roll this and other, related changes out to everyone in July. [#4007](https://github.com/github/codeql-action/pull/4007)
|
||||
|
||||
## 3.37.1 - 16 Jul 2026
|
||||
## 4.37.1 - 16 Jul 2026
|
||||
|
||||
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. [#3956](https://github.com/github/codeql-action/pull/3956)
|
||||
- Update default CodeQL bundle version to [2.26.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.1). [#4019](https://github.com/github/codeql-action/pull/4019)
|
||||
|
||||
## 3.37.0 - 08 Jul 2026
|
||||
## 4.37.0 - 08 Jul 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.26.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.0). [#3995](https://github.com/github/codeql-action/pull/3995)
|
||||
- In addition to the existing input format, the `config-file` input for the `codeql-action/init` step will soon support a new `[owner/]repo[@ref][:path]` format. All components except the repository name are optional. If omitted, `owner` defaults to the same owner as the repository the analysis is running for, `ref` to `main`, and `path` to `.github/codeql-action.yaml`. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. [#3973](https://github.com/github/codeql-action/pull/3973)
|
||||
|
||||
## 3.36.3 - 01 Jul 2026
|
||||
## 4.36.3 - 01 Jul 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.36.2 - 04 Jun 2026
|
||||
## 4.36.2 - 04 Jun 2026
|
||||
|
||||
- Cache CodeQL CLI version information across Actions steps. [#3943](https://github.com/github/codeql-action/pull/3943)
|
||||
- Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. [#3937](https://github.com/github/codeql-action/pull/3937)
|
||||
- Update default CodeQL bundle version to [2.25.6](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.6). [#3948](https://github.com/github/codeql-action/pull/3948)
|
||||
|
||||
## 3.36.1 - 02 Jun 2026
|
||||
## 4.36.1 - 02 Jun 2026
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.36.0 - 22 May 2026
|
||||
## 4.36.0 - 22 May 2026
|
||||
|
||||
- _Breaking change_: Bump the minimum required CodeQL bundle version to 2.19.4. [#3894](https://github.com/github/codeql-action/pull/3894)
|
||||
- Add support for SHA-256 Git object IDs. [#3893](https://github.com/github/codeql-action/pull/3893)
|
||||
- Update default CodeQL bundle version to [2.25.5](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.5). [#3926](https://github.com/github/codeql-action/pull/3926)
|
||||
|
||||
## 3.35.5 - 15 May 2026
|
||||
## 4.35.5 - 15 May 2026
|
||||
|
||||
- We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. [#3899](https://github.com/github/codeql-action/pull/3899)
|
||||
- For performance and accuracy reasons, [improved incremental analysis](https://github.com/github/roadmap/issues/1158) will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. [#3791](https://github.com/github/codeql-action/pull/3791)
|
||||
- If multiple inputs are provided for the GitHub-internal `analysis-kinds` input, only `code-scanning` will be enabled. The `analysis-kinds` input is experimental, for GitHub-internal use only, and may change without notice at any time. [#3892](https://github.com/github/codeql-action/pull/3892)
|
||||
- Added an experimental change which, when running a Code Scanning analysis for a PR with [improved incremental analysis](https://github.com/github/roadmap/issues/1158) enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. [#3880](https://github.com/github/codeql-action/pull/3880)
|
||||
|
||||
## 3.35.4 - 07 May 2026
|
||||
## 4.35.4 - 07 May 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.25.4](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.4). [#3881](https://github.com/github/codeql-action/pull/3881)
|
||||
|
||||
## 3.35.3 - 01 May 2026
|
||||
## 4.35.3 - 01 May 2026
|
||||
|
||||
- _Upcoming breaking change_: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. [#3837](https://github.com/github/codeql-action/pull/3837)
|
||||
- Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. [#3850](https://github.com/github/codeql-action/pull/3850)
|
||||
@@ -91,7 +64,7 @@ No user facing changes.
|
||||
- Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. [#3852](https://github.com/github/codeql-action/pull/3852)
|
||||
- Update default CodeQL bundle version to [2.25.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.3). [#3865](https://github.com/github/codeql-action/pull/3865)
|
||||
|
||||
## 3.35.2 - 15 Apr 2026
|
||||
## 4.35.2 - 15 Apr 2026
|
||||
|
||||
- The undocumented TRAP cache cleanup feature that could be enabled using the `CODEQL_ACTION_CLEANUP_TRAP_CACHES` environment variable is deprecated and will be removed in May 2026. If you are affected by this, we recommend disabling TRAP caching by passing the `trap-caching: false` input to the `init` Action. [#3795](https://github.com/github/codeql-action/pull/3795)
|
||||
- The Git version 2.36.0 requirement for improved incremental analysis now only applies to repositories that contain submodules. [#3789](https://github.com/github/codeql-action/pull/3789)
|
||||
@@ -99,26 +72,26 @@ No user facing changes.
|
||||
- Fixed a bug in the validation of OIDC configurations for private registries that was added in CodeQL Action 4.33.0 / 3.33.0. [#3807](https://github.com/github/codeql-action/pull/3807)
|
||||
- Update default CodeQL bundle version to [2.25.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.2). [#3823](https://github.com/github/codeql-action/pull/3823)
|
||||
|
||||
## 3.35.1 - 27 Mar 2026
|
||||
## 4.35.1 - 27 Mar 2026
|
||||
|
||||
- Fix incorrect minimum required Git version for [improved incremental analysis](https://github.com/github/roadmap/issues/1158): it should have been 2.36.0, not 2.11.0. [#3781](https://github.com/github/codeql-action/pull/3781)
|
||||
|
||||
## 3.35.0 - 27 Mar 2026
|
||||
## 4.35.0 - 27 Mar 2026
|
||||
|
||||
- Reduced the minimum Git version required for [improved incremental analysis](https://github.com/github/roadmap/issues/1158) from 2.38.0 to 2.11.0. [#3767](https://github.com/github/codeql-action/pull/3767)
|
||||
- Update default CodeQL bundle version to [2.25.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.1). [#3773](https://github.com/github/codeql-action/pull/3773)
|
||||
|
||||
## 3.34.1 - 20 Mar 2026
|
||||
## 4.34.1 - 20 Mar 2026
|
||||
|
||||
- Downgrade default CodeQL bundle version to [2.24.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3) due to issues with a small percentage of Actions and JavaScript analyses. [#3762](https://github.com/github/codeql-action/pull/3762)
|
||||
|
||||
## 3.34.0 - 20 Mar 2026
|
||||
## 4.34.0 - 20 Mar 2026
|
||||
|
||||
- Added an experimental change which disables TRAP caching when [improved incremental analysis](https://github.com/github/roadmap/issues/1158) is enabled, since improved incremental analysis supersedes TRAP caching. This will improve performance and reduce Actions cache usage. We expect to roll this change out to everyone in March. [#3569](https://github.com/github/codeql-action/pull/3569)
|
||||
- We are rolling out improved incremental analysis to C/C++ analyses that use build mode `none`. We expect this rollout to be complete by the end of April 2026. [#3584](https://github.com/github/codeql-action/pull/3584)
|
||||
- Update default CodeQL bundle version to [2.25.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.25.0). [#3585](https://github.com/github/codeql-action/pull/3585)
|
||||
|
||||
## 3.33.0 - 16 Mar 2026
|
||||
## 4.33.0 - 16 Mar 2026
|
||||
|
||||
- Upcoming change: Starting April 2026, the CodeQL Action will skip collecting file coverage information on pull requests to improve analysis performance. File coverage information will still be computed on non-PR analyses. Pull request analyses will log a warning about this upcoming change. [#3562](https://github.com/github/codeql-action/pull/3562)
|
||||
|
||||
@@ -132,11 +105,11 @@ No user facing changes.
|
||||
- Fixed the retry mechanism for database uploads. Previously this would fail with the error "Response body object should not be disturbed or locked". [#3564](https://github.com/github/codeql-action/pull/3564)
|
||||
- A warning is now emitted if the CodeQL Action detects a repository property whose name suggests that it relates to the CodeQL Action, but which is not one of the properties recognised by the current version of the CodeQL Action. [#3570](https://github.com/github/codeql-action/pull/3570)
|
||||
|
||||
## 3.32.6 - 05 Mar 2026
|
||||
## 4.32.6 - 05 Mar 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.24.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.3). [#3548](https://github.com/github/codeql-action/pull/3548)
|
||||
|
||||
## 3.32.5 - 02 Mar 2026
|
||||
## 4.32.5 - 02 Mar 2026
|
||||
|
||||
- Repositories owned by an organization can now set up the `github-codeql-disable-overlay` custom repository property to disable [improved incremental analysis for CodeQL](https://github.com/github/roadmap/issues/1158). First, create a custom repository property with the name `github-codeql-disable-overlay` and the type "True/false" in the organization's settings. Then in the repository's settings, set this property to `true` to disable improved incremental analysis. For more information, see [Managing custom properties for repositories in your organization](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature is not yet available on GitHub Enterprise Server. [#3507](https://github.com/github/codeql-action/pull/3507)
|
||||
- Added an experimental change so that when [improved incremental analysis](https://github.com/github/roadmap/issues/1158) fails on a runner — potentially due to insufficient disk space — the failure is recorded in the Actions cache so that subsequent runs will automatically skip improved incremental analysis until something changes (e.g. a larger runner is provisioned or a new CodeQL version is released). We expect to roll this change out to everyone in March. [#3487](https://github.com/github/codeql-action/pull/3487)
|
||||
@@ -146,7 +119,7 @@ No user facing changes.
|
||||
- Added an experimental change which allows the `start-proxy` action to resolve the CodeQL CLI version from feature flags instead of using the linked CLI bundle version. We expect to roll this change out to everyone in March. [#3512](https://github.com/github/codeql-action/pull/3512)
|
||||
- The previously experimental changes from versions 4.32.3, 4.32.4, 3.32.3 and 3.32.4 are now enabled by default. [#3503](https://github.com/github/codeql-action/pull/3503), [#3504](https://github.com/github/codeql-action/pull/3504)
|
||||
|
||||
## 3.32.4 - 20 Feb 2026
|
||||
## 4.32.4 - 20 Feb 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.24.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.2). [#3493](https://github.com/github/codeql-action/pull/3493)
|
||||
- Added an experimental change which improves how certificates are generated for the authentication proxy that is used by the CodeQL Action in Default Setup when [private package registries are configured](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries). This is expected to generate more widely compatible certificates and should have no impact on analyses which are working correctly already. We expect to roll this change out to everyone in February. [#3473](https://github.com/github/codeql-action/pull/3473)
|
||||
@@ -154,89 +127,89 @@ No user facing changes.
|
||||
- Added a setting which allows the CodeQL Action to enable network debugging for Java programs. This will help GitHub staff support customers with troubleshooting issues in GitHub-managed CodeQL workflows, such as Default Setup. This setting can only be enabled by GitHub staff. [#3485](https://github.com/github/codeql-action/pull/3485)
|
||||
- Added a setting which enables GitHub-managed workflows, such as Default Setup, to use a [nightly CodeQL CLI release](https://github.com/dsp-testing/codeql-cli-nightlies) instead of the latest, stable release that is used by default. This will help GitHub staff support customers whose analyses for a given repository or organization require early access to a change in an upcoming CodeQL CLI release. This setting can only be enabled by GitHub staff. [#3484](https://github.com/github/codeql-action/pull/3484)
|
||||
|
||||
## 3.32.3 - 13 Feb 2026
|
||||
## 4.32.3 - 13 Feb 2026
|
||||
|
||||
- Added experimental support for testing connections to [private package registries](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries). This feature is not currently enabled for any analysis. In the future, it may be enabled by default for Default Setup. [#3466](https://github.com/github/codeql-action/pull/3466)
|
||||
|
||||
## 3.32.2 - 05 Feb 2026
|
||||
## 4.32.2 - 05 Feb 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.24.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.1). [#3460](https://github.com/github/codeql-action/pull/3460)
|
||||
|
||||
## 3.32.1 - 02 Feb 2026
|
||||
## 4.32.1 - 02 Feb 2026
|
||||
|
||||
- A warning is now shown in Default Setup workflow logs if a [private package registry is configured](https://docs.github.com/en/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries) using a GitHub Personal Access Token (PAT), but no username is configured. [#3422](https://github.com/github/codeql-action/pull/3422)
|
||||
- Fixed a bug which caused the CodeQL Action to fail when repository properties cannot successfully be retrieved. [#3421](https://github.com/github/codeql-action/pull/3421)
|
||||
|
||||
## 3.32.0 - 26 Jan 2026
|
||||
## 4.32.0 - 26 Jan 2026
|
||||
|
||||
- Update default CodeQL bundle version to [2.24.0](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.24.0). [#3425](https://github.com/github/codeql-action/pull/3425)
|
||||
|
||||
## 3.31.11 - 23 Jan 2026
|
||||
## 4.31.11 - 23 Jan 2026
|
||||
|
||||
- When running a Default Setup workflow with [Actions debugging enabled](https://docs.github.com/en/actions/how-tos/monitor-workflows/enable-debug-logging), the CodeQL Action will now use more unique names when uploading logs from the Dependabot authentication proxy as workflow artifacts. This ensures that the artifact names do not clash between multiple jobs in a build matrix. [#3409](https://github.com/github/codeql-action/pull/3409)
|
||||
- Improved error handling throughout the CodeQL Action. [#3415](https://github.com/github/codeql-action/pull/3415)
|
||||
- Added experimental support for automatically excluding [generated files](https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github) from the analysis. This feature is not currently enabled for any analysis. In the future, it may be enabled by default for some GitHub-managed analyses. [#3318](https://github.com/github/codeql-action/pull/3318)
|
||||
- The changelog extracts that are included with releases of the CodeQL Action are now shorter to avoid duplicated information from appearing in Dependabot PRs. [#3403](https://github.com/github/codeql-action/pull/3403)
|
||||
|
||||
## 3.31.10 - 12 Jan 2026
|
||||
## 4.31.10 - 12 Jan 2026
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.9. [#3393](https://github.com/github/codeql-action/pull/3393)
|
||||
|
||||
## 3.31.9 - 16 Dec 2025
|
||||
## 4.31.9 - 16 Dec 2025
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.31.8 - 11 Dec 2025
|
||||
## 4.31.8 - 11 Dec 2025
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.8. [#3354](https://github.com/github/codeql-action/pull/3354)
|
||||
|
||||
## 3.31.7 - 05 Dec 2025
|
||||
## 4.31.7 - 05 Dec 2025
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.7. [#3343](https://github.com/github/codeql-action/pull/3343)
|
||||
|
||||
## 3.31.6 - 01 Dec 2025
|
||||
## 4.31.6 - 01 Dec 2025
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.31.5 - 24 Nov 2025
|
||||
## 4.31.5 - 24 Nov 2025
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.6. [#3321](https://github.com/github/codeql-action/pull/3321)
|
||||
|
||||
## 3.31.4 - 18 Nov 2025
|
||||
## 4.31.4 - 18 Nov 2025
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.31.3 - 13 Nov 2025
|
||||
## 4.31.3 - 13 Nov 2025
|
||||
|
||||
- CodeQL Action v3 will be deprecated in December 2026. The Action now logs a warning for customers who are running v3 but could be running v4. For more information, see [Upcoming deprecation of CodeQL Action v3](https://github.blog/changelog/2025-10-28-upcoming-deprecation-of-codeql-action-v3/).
|
||||
- Update default CodeQL bundle version to 2.23.5. [#3288](https://github.com/github/codeql-action/pull/3288)
|
||||
|
||||
## 3.31.2 - 30 Oct 2025
|
||||
## 4.31.2 - 30 Oct 2025
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.31.1 - 30 Oct 2025
|
||||
## 4.31.1 - 30 Oct 2025
|
||||
|
||||
- The `add-snippets` input has been removed from the `analyze` action. This input has been deprecated since CodeQL Action 3.26.4 in August 2024 when this removal was announced.
|
||||
|
||||
## 3.31.0 - 24 Oct 2025
|
||||
## 4.31.0 - 24 Oct 2025
|
||||
|
||||
- Bump minimum CodeQL bundle version to 2.17.6. [#3223](https://github.com/github/codeql-action/pull/3223)
|
||||
- When SARIF files are uploaded by the `analyze` or `upload-sarif` actions, the CodeQL Action automatically performs post-processing steps to prepare the data for the upload. Previously, these post-processing steps were only performed before an upload took place. We are now changing this so that the post-processing steps will always be performed, even when the SARIF files are not uploaded. This does not change anything for the `upload-sarif` action. For `analyze`, this may affect Advanced Setup for CodeQL users who specify a value other than `always` for the `upload` input. [#3222](https://github.com/github/codeql-action/pull/3222)
|
||||
|
||||
## 3.30.9 - 17 Oct 2025
|
||||
## 4.30.9 - 17 Oct 2025
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.3. [#3205](https://github.com/github/codeql-action/pull/3205)
|
||||
- Experimental: A new `setup-codeql` action has been added which is similar to `init`, except it only installs the CodeQL CLI and does not initialize a database. Do not use this in production as it is part of an internal experiment and subject to change at any time. [#3204](https://github.com/github/codeql-action/pull/3204)
|
||||
|
||||
## 3.30.8 - 10 Oct 2025
|
||||
## 4.30.8 - 10 Oct 2025
|
||||
|
||||
No user facing changes.
|
||||
|
||||
## 3.30.7 - 06 Oct 2025
|
||||
## 4.30.7 - 06 Oct 2025
|
||||
|
||||
- [v4+ only] The CodeQL Action now runs on Node.js v24. [#3169](https://github.com/github/codeql-action/pull/3169)
|
||||
|
||||
No user facing changes.
|
||||
## 3.30.6 - 02 Oct 2025
|
||||
|
||||
- Update default CodeQL bundle version to 2.23.2. [#3168](https://github.com/github/codeql-action/pull/3168)
|
||||
|
||||
@@ -60,13 +60,10 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
This workflow goes through the pull requests that have been merged to `main` since the last release, creates a changelog, then opens a pull request to merge the changes since the last release into the `releases/v3` release branch.
|
||||
|
||||
You can start a release by triggering this workflow via [workflow dispatch](https://github.com/github/codeql-action/actions/workflows/update-release-branch.yml).
|
||||
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it **with a merge commit** (`gh pr merge --merge`).
|
||||
1. The workflow run will open a pull request titled "Merge main into releases/v3". Follow the steps on the checklist in the pull request. Once you've checked off all but the last two of these, approve the PR and automerge it.
|
||||
1. When the "Merge main into releases/v3" pull request is merged into the `releases/v3` branch, a mergeback pull request to `main` will be automatically created. This mergeback pull request incorporates the changelog updates into `main`, tags the release using the merge commit of the "Merge main into releases/v3" pull request, and bumps the patch version of the CodeQL Action.
|
||||
1. If a backport to an older major version is required, a pull request targeting that version's branch will also be automatically created.
|
||||
1. Approve the mergeback and backport pull request (if applicable) and automerge them **with a merge commit** (`gh pr merge --merge`).
|
||||
|
||||
> [!NOTE]
|
||||
> The release, mergeback, and backport pull requests must always be merged with a merge commit — **never squash or rebase**. The mergeback tags the release using the merge commit of the "Merge main into releases/v3" pull request, so squashing or rebasing breaks tagging and the branch linkage the release automation relies on.
|
||||
1. Approve the mergeback and backport pull request (if applicable) and automerge them.
|
||||
|
||||
Once the mergeback and backport pull request have been merged, the release is complete.
|
||||
|
||||
|
||||
@@ -94,6 +94,6 @@ outputs:
|
||||
sarif-id:
|
||||
description: The ID of the uploaded SARIF file.
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: "../lib/analyze-entry.js"
|
||||
post: "../lib/analyze-post-entry.js"
|
||||
|
||||
@@ -15,5 +15,5 @@ inputs:
|
||||
$GITHUB_WORKSPACE as its working directory.
|
||||
required: false
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: '../lib/autobuild-entry.js'
|
||||
|
||||
@@ -209,18 +209,4 @@ export default [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["scripts/changetool/**/*.ts"],
|
||||
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./scripts/changetool/tsconfig.json",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import/extensions": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -164,19 +164,12 @@ inputs:
|
||||
[Internal] The ID of the check run, as provided by the Actions runtime environment. Do not set this value manually.
|
||||
default: ${{ job.check_run_id }}
|
||||
required: false
|
||||
job-status:
|
||||
description: >-
|
||||
[Internal] The status of the job, as provided by the Actions runtime environment. This is how the
|
||||
post step learns whether the job as a whole succeeded, failed, or was cancelled. Do not set this
|
||||
value manually.
|
||||
default: ${{ job.status }}
|
||||
required: false
|
||||
outputs:
|
||||
codeql-path:
|
||||
description: The path of the CodeQL binary used for analysis
|
||||
codeql-version:
|
||||
description: The version of the CodeQL binary used for analysis
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: '../lib/init-entry.js'
|
||||
post: '../lib/init-post-entry.js'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
6469
lib/entry-points.js
generated
6469
lib/entry-points.js
generated
File diff suppressed because it is too large
Load Diff
1937
package-lock.json
generated
1937
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codeql",
|
||||
"version": "3.38.0",
|
||||
"version": "4.37.4",
|
||||
"private": true,
|
||||
"description": "CodeQL action",
|
||||
"scripts": {
|
||||
@@ -17,8 +17,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"pr-checks",
|
||||
"scripts/changetool"
|
||||
"pr-checks"
|
||||
],
|
||||
"dependencies": {
|
||||
"@actions/artifact": "^5.0.3",
|
||||
@@ -31,50 +30,50 @@
|
||||
"@actions/http-client": "^3.0.0",
|
||||
"@actions/io": "^2.0.0",
|
||||
"@actions/tool-cache": "^3.0.1",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": "^15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.1",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": "^14.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"@octokit/plugin-retry": "^8.1.0",
|
||||
"archiver": "^8.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"follow-redirects": "^1.16.0",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"js-yaml": "^5.4.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"jsonschema": "1.5.0",
|
||||
"long": "^5.3.2",
|
||||
"node-forge": "^1.4.0",
|
||||
"semver": "^7.8.5",
|
||||
"uuid": "^14.0.2",
|
||||
"undici": "^6.28.0"
|
||||
"uuid": "^14.0.1",
|
||||
"undici": "^6.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "6.0.0",
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@microsoft/eslint-formatter-sarif": "^3.1.0",
|
||||
"@octokit/types": "^17.0.0",
|
||||
"@octokit/types": "^16.0.0",
|
||||
"@types/archiver": "^8.0.0",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^20.19.43",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/sarif": "^2.1.7",
|
||||
"@types/semver": "^7.8.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^22.0.0",
|
||||
"ava": "^6.4.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-github": "^6.1.2",
|
||||
"eslint-plugin-github": "^6.1.1",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^64.2.1",
|
||||
"eslint-plugin-jsdoc": "^62.9.0",
|
||||
"eslint-plugin-no-async-foreach": "^0.1.1",
|
||||
"glob": "^13.0.6",
|
||||
"globals": "^17.11.0",
|
||||
"nock": "^14.0.17",
|
||||
"sinon": "^22.1.0",
|
||||
"globals": "^17.7.0",
|
||||
"nock": "^14.0.16",
|
||||
"sinon": "^22.0.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.68.0"
|
||||
"typescript-eslint": "^8.64.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@actions/tool-cache": {
|
||||
@@ -96,6 +95,6 @@
|
||||
"semver": ">=6.3.1"
|
||||
},
|
||||
"glob": "^13.0.6",
|
||||
"undici": "^6.28.0"
|
||||
"undici": "^6.24.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
|
||||
/** The type of the Octokit client. */
|
||||
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
|
||||
/** Constructs an `ApiClient` using `token` for authentication. */
|
||||
export function getApiClient(token: string): ApiClient {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ name: "All-platform bundle"
|
||||
description: "Tests using an all-platform CodeQL Bundle"
|
||||
operatingSystems:
|
||||
- ubuntu
|
||||
- os: macos
|
||||
runner-image: macos-latest-xlarge
|
||||
- macos
|
||||
- windows
|
||||
versions:
|
||||
- nightly-latest
|
||||
|
||||
@@ -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'."
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: "Linux Arm64"
|
||||
description: "An end-to-end integration test running on a Linux Arm64 runner, checking that the native linux-arm64 CodeQL bundle is downloaded and can analyze interpreted and compiled code"
|
||||
operatingSystems:
|
||||
- os: ubuntu
|
||||
runner-image: ubuntu-24.04-arm
|
||||
# The native linux-arm64 CodeQL bundle is only available in recent CLI releases, so we restrict this
|
||||
# check to `nightly-latest`, which is guaranteed to ship it. Older stable versions do not have an
|
||||
# arm64 asset, and `prepare-test` would resolve an x64 bundle URL for them on this runner.
|
||||
versions:
|
||||
- nightly-latest
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
# The set of languages CodeQL supports on this platform, excluding Swift (macOS only).
|
||||
env:
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
|
||||
steps:
|
||||
- uses: ./../action/init
|
||||
with:
|
||||
languages: ${{ env.LANGUAGES }}
|
||||
tools: ${{ steps.prepare-test.outputs.tools-url }}
|
||||
- name: Build code
|
||||
run: ./build.sh
|
||||
- uses: ./../action/analyze
|
||||
with:
|
||||
upload-database: false
|
||||
- name: Assert databases exist
|
||||
run: |
|
||||
cd "$RUNNER_TEMP/codeql_databases"
|
||||
for lang in ${LANGUAGES//,/ }; do
|
||||
if [[ ! -d "$lang" ]]; then
|
||||
echo "Did not find a database for $lang"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found database for $lang"
|
||||
done
|
||||
@@ -15,7 +15,6 @@ operatingSystems:
|
||||
- stable-v2.21.4
|
||||
- stable-v2.22.4
|
||||
env:
|
||||
CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES: true
|
||||
CODEQL_ACTION_RESOLVE_SUPPORTED_LANGUAGES_USING_CLI: true
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,8 +5,7 @@ versions:
|
||||
- default
|
||||
- nightly-latest
|
||||
operatingSystems:
|
||||
- os: macos
|
||||
runner-image: macos-latest-xlarge
|
||||
- macos
|
||||
installGo: true
|
||||
installDotNet: true
|
||||
env:
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^2.0.3",
|
||||
"@actions/github": "^8.0.1",
|
||||
"@octokit/core": "^7.0.7",
|
||||
"@octokit/plugin-paginate-rest": ">=15.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
|
||||
"@octokit/core": "^7.0.6",
|
||||
"@octokit/plugin-paginate-rest": ">=9.2.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"tsx": "^4.23.12"
|
||||
"tsx": "^4.23.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -253,8 +253,8 @@ const languageSetups: LanguageSetups = {
|
||||
name: "Install Java",
|
||||
uses: pinnedUses(
|
||||
"actions/setup-java",
|
||||
"dd06d9cba3e5552c54d9f8ea23572deb30010f7c",
|
||||
"v6.0.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);
|
||||
}
|
||||
@@ -21,5 +21,5 @@ outputs:
|
||||
environment:
|
||||
description: The inferred build environment configuration.
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: '../lib/resolve-environment-entry.js'
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
isValidChangenoteContent,
|
||||
isValidChangenoteFile,
|
||||
isValidChangenoteFilename,
|
||||
hasValidChangenoteCategory,
|
||||
VALID_CHANGE_NOTE_CATEGORIES,
|
||||
} from "./validate.ts";
|
||||
|
||||
async function withTmpFile<T>(
|
||||
baseFileName: string,
|
||||
contents: string,
|
||||
body: (filePath: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const tmpDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "changetool-validate-test-"),
|
||||
);
|
||||
try {
|
||||
const filePath = path.join(tmpDir, baseFileName);
|
||||
fs.writeFileSync(filePath, contents);
|
||||
return await body(filePath);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
await describe("isValidChangenoteContent", async () => {
|
||||
await it("recognizes an unordered Markdown list", async () => {
|
||||
const inputs = [
|
||||
"- One changenote entry",
|
||||
"- First item\n- Second item",
|
||||
"\n\n\n\n- Fixed a bug\n- Added a feature",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize non-Markdown text", async () => {
|
||||
const inputs = [
|
||||
"This is not a list.",
|
||||
'["this", "is", "JSON"]',
|
||||
"---",
|
||||
"***",
|
||||
"___",
|
||||
"paragraph",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not recognize ordered Markdown lists", async () => {
|
||||
const inputs = [
|
||||
"1. First item\n2. Second item",
|
||||
"\n\n\n1. First item\n1. Second item",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("requires all list items to use a hyphen bullet", async () => {
|
||||
const inputs = [
|
||||
"* Fixed a bug\n* Added feature",
|
||||
"+ Fixed a bug\n+ Added feature",
|
||||
"- Fixed a bug\n* Added feature",
|
||||
"- Fixed a bug\n+ Added feature",
|
||||
"- Fixed a bug\n * Added feature\n + Updated docs",
|
||||
"\n\n\n* Fixed a bug",
|
||||
"\n\n\n+ Fixed a bug",
|
||||
"---\n* Fixed a bug\n* Added feature",
|
||||
] as const;
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("does not contain other Markdown elements", async () => {
|
||||
const inputs = [
|
||||
"- Fixed a bug\n\nParagraph of text",
|
||||
"- Fixed a bug\n\n* Added a feature",
|
||||
"# Header\n- Fixed a bug",
|
||||
"- Fixed a bug\n## Subheader",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteContent(input), false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidChangenoteFilename", async () => {
|
||||
await it("accepts valid filenames", async () => {
|
||||
const inputs = [
|
||||
"2023-01-01-fix-bug.md",
|
||||
"2023-12-31-add-feature.md",
|
||||
"2023-06-15-update-docs.md",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteFilename(input), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("rejects invalid filenames", async () => {
|
||||
const inputs = [
|
||||
"missing-date-from-filename.md",
|
||||
"2021-01-01.md",
|
||||
"2026-12-19-wrong-file-name-extension.txt",
|
||||
];
|
||||
|
||||
for (const input of inputs) {
|
||||
assert.equal(isValidChangenoteFilename(input), false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await describe("hasValidChangenoteCategory", async () => {
|
||||
await it("accepts valid categories", async () => {
|
||||
for (const category of Object.keys(VALID_CHANGE_NOTE_CATEGORIES)) {
|
||||
const frontmatter = { category };
|
||||
assert.equal(hasValidChangenoteCategory(frontmatter), true);
|
||||
}
|
||||
});
|
||||
|
||||
await it("rejects invalid categories", async () => {
|
||||
const inputs = [
|
||||
"",
|
||||
"invalid-category",
|
||||
"bug-fix",
|
||||
"new-feature",
|
||||
"security-patch",
|
||||
"miscellaneous",
|
||||
"documentation",
|
||||
];
|
||||
|
||||
for (const category of inputs) {
|
||||
const frontmatter = { category };
|
||||
assert.equal(hasValidChangenoteCategory(frontmatter), false);
|
||||
}
|
||||
});
|
||||
|
||||
await it("reject missing category", async () => {
|
||||
assert.equal(hasValidChangenoteCategory({}), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: null }), false);
|
||||
assert.equal(hasValidChangenoteCategory({ category: undefined }), false);
|
||||
});
|
||||
});
|
||||
|
||||
await describe("isValidChangenoteFile", async () => {
|
||||
await it("accepts a valid change-note file", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects invalid filename", async () => {
|
||||
await withTmpFile(
|
||||
"fix-bug.md",
|
||||
"---\ncategory: fix\n---\n- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects missing frontmatter", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"- Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await it("rejects invalid Markdown", async () => {
|
||||
await withTmpFile(
|
||||
"2026-01-01-fix-bug.md",
|
||||
"---\ncategory: fix\n---\n* Fixed a bug\n",
|
||||
async (filePath) => {
|
||||
assert.equal(isValidChangenoteFile(filePath), false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { matter } from "lite-matter";
|
||||
import type { List, ListItem } from "mdast";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
|
||||
// Regex for filename: YYYY-MM-DD-id.md
|
||||
const VALID_CHANGE_NOTE_FILENAME_PATTERN =
|
||||
/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])-([a-z0-9]+(?:-[a-z0-9]+)*)\.md$/;
|
||||
|
||||
export const VALID_CHANGE_NOTE_CATEGORIES = {
|
||||
breaking: "Breaking Changes",
|
||||
feature: "New Features",
|
||||
improvement: "Improvements",
|
||||
securityFix: "Security Fixes",
|
||||
fix: "Bug Fixes",
|
||||
unship: "Removed Features",
|
||||
deprecation: "Deprecations",
|
||||
knownIssue: "Known Issues",
|
||||
misc: "Miscellaneous",
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the given Markdown string meets the criteria for a change-note, which is:
|
||||
* - A single unordered list
|
||||
* - Each list item must start with a hyphen (-)
|
||||
* - No other Markdown elements are allowed
|
||||
* @param content The Markdown string to validate
|
||||
* @returns True if the string is a valid change-note, false otherwise
|
||||
*/
|
||||
export function isValidChangenoteContent(content: string): boolean {
|
||||
const ast = fromMarkdown(content);
|
||||
const lines = content.split("\n");
|
||||
|
||||
function listHasHyphenBullets(node: List | ListItem): boolean {
|
||||
if (node.type === "list") {
|
||||
return node.children.every(listHasHyphenBullets);
|
||||
}
|
||||
|
||||
const line = lines[node.position!.start.line - 1].trim();
|
||||
return (
|
||||
line.startsWith("-") &&
|
||||
node.children.every(
|
||||
(child) => child.type !== "list" || listHasHyphenBullets(child),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
ast.children.length === 1 &&
|
||||
ast.children[0].type === "list" &&
|
||||
ast.children[0].ordered === false &&
|
||||
listHasHyphenBullets(ast.children[0])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given filename meets the criteria for a change-note filename.
|
||||
* @param filename The name of the change-note file to validate.
|
||||
* @returns True if the filename is valid, false otherwise.
|
||||
*/
|
||||
export function isValidChangenoteFilename(filename: string): boolean {
|
||||
return filename.match(VALID_CHANGE_NOTE_FILENAME_PATTERN) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given frontmatter has a valid change-note category.
|
||||
* @param frontmatter The frontmatter object to validate.
|
||||
* @returns True if the frontmatter has a valid category, false otherwise.
|
||||
*/
|
||||
export function hasValidChangenoteCategory(
|
||||
frontmatter: Record<string, unknown>,
|
||||
): boolean {
|
||||
const category = frontmatter["category"];
|
||||
return (
|
||||
typeof category === "string" &&
|
||||
Object.hasOwn(VALID_CHANGE_NOTE_CATEGORIES, category)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the given change-note file meets all of the criteria for a change-note.
|
||||
* @param filename The name of the change-note file to validate.
|
||||
* @returns True if the file is a valid change-note, false otherwise.
|
||||
*/
|
||||
export function isValidChangenoteFile(filename: string): boolean {
|
||||
let isValid: boolean = true;
|
||||
|
||||
let fileData: string | undefined;
|
||||
try {
|
||||
fileData = fs.readFileSync(filename, "utf8");
|
||||
} catch (error) {
|
||||
console.error(`${filename}: failed to read file`, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data: frontmatter, content } = matter(fileData);
|
||||
|
||||
if (!isValidChangenoteFilename(path.basename(filename))) {
|
||||
isValid = false;
|
||||
console.error(
|
||||
`${filename}: invalid filename; must match pattern YYYY-MM-DD-id.md`,
|
||||
);
|
||||
}
|
||||
if (!hasValidChangenoteCategory(frontmatter)) {
|
||||
isValid = false;
|
||||
const categories = Object.keys(VALID_CHANGE_NOTE_CATEGORIES).join(", ");
|
||||
console.error(
|
||||
`${filename}: invalid category; must be one of: ${categories}`,
|
||||
);
|
||||
}
|
||||
if (!isValidChangenoteContent(content)) {
|
||||
isValid = false;
|
||||
console.error(
|
||||
`${filename}: invalid Markdown; content must be a single unordered list with hyphen bullets and no other Markdown elements`,
|
||||
);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { isValidChangenoteFile } from "./cli/validate.ts";
|
||||
|
||||
const entryPoint = process.argv[1];
|
||||
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
|
||||
try {
|
||||
process.exit(main());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const { positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
strict: true,
|
||||
});
|
||||
const [command, ...paths] = positionals;
|
||||
switch (command) {
|
||||
case undefined:
|
||||
case "help":
|
||||
return usage();
|
||||
case "validate":
|
||||
return validate(paths);
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function usage(): number {
|
||||
console.log("Usage: changetool validate <path> [<path> ...]");
|
||||
return 0;
|
||||
}
|
||||
|
||||
function validate(paths: string[]): number {
|
||||
let valid = true;
|
||||
if (paths.length === 0) {
|
||||
console.error("error: no paths provided (see 'help' command for usage)");
|
||||
return 1;
|
||||
}
|
||||
for (const path of paths) {
|
||||
if (!isValidChangenoteFile(path)) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "changetool",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Validates change-notes and merges them into CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx index.ts",
|
||||
"test": "node --test --experimental-strip-types cli/*.test.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.2.0",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"lite-matter": "^0.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "preserve",
|
||||
"allowImportingTsExtensions": true,
|
||||
"rootDir": ".",
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -54,5 +54,5 @@ outputs:
|
||||
codeql-version:
|
||||
description: The version of the CodeQL binary that was installed.
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: '../lib/setup-codeql-entry.js'
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,19 +277,6 @@ export function isSelfHostedRunner(env: Env = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "self-hosted";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the job is running on a runner that GitHub hosts, and whose toolcache is therefore thrown
|
||||
* away once the job has finished.
|
||||
*
|
||||
* Unlike `looksLikeHostedRunner`, this is based on what the service reports for the job rather than
|
||||
* on how the runner's filesystem happens to be laid out, so it does not match self-hosted runners
|
||||
* that are configured to resemble hosted ones, such as those that mount a persistent volume at
|
||||
* `/opt/hostedtoolcache`.
|
||||
*/
|
||||
export function isGitHubHostedRunner(env: Env = getEnv()) {
|
||||
return env.getOptional(ActionsEnvVars.RUNNER_ENVIRONMENT) === "github-hosted";
|
||||
}
|
||||
|
||||
/** Determines whether the workflow trigger is `dynamic`. */
|
||||
export function isDynamicWorkflow(env: Env = getEnv()): boolean {
|
||||
return getWorkflowEventName(env) === "dynamic";
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function runWrapper() {
|
||||
logger,
|
||||
);
|
||||
if (config !== undefined) {
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
const version = await codeql.getVersion();
|
||||
await debugArtifacts.uploadCombinedSarifArtifacts(
|
||||
logger,
|
||||
|
||||
@@ -255,7 +255,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
if (hasBadExpectErrorInput()) {
|
||||
throw new util.ConfigurationError(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as githubUtils from "@actions/github/lib/utils";
|
||||
import { type Octokit } from "@octokit/core";
|
||||
import { type PaginateInterface } from "@octokit/plugin-paginate-rest";
|
||||
import { type Api } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import * as retry from "@octokit/plugin-retry";
|
||||
import { RequestRequestOptions } from "@octokit/types";
|
||||
import {
|
||||
@@ -125,7 +128,7 @@ export function makeProxyRequestOptions(
|
||||
}
|
||||
|
||||
/** The type of GitHub API client we use. */
|
||||
export type ApiClient = InstanceType<typeof githubUtils.GitHub>;
|
||||
export type ApiClient = Octokit & Api & { paginate: PaginateInterface };
|
||||
|
||||
/** Options for `createApiClientWithDetails`. */
|
||||
interface CreateApiClientOptions {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion":"3.23","minimumVersion":"3.17"}
|
||||
{"maximumVersion": "3.22", "minimumVersion": "3.17"}
|
||||
|
||||
@@ -99,7 +99,7 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
|
||||
);
|
||||
}
|
||||
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
languages = await determineAutobuildLanguages(codeql, config, logger);
|
||||
if (languages !== undefined) {
|
||||
|
||||
@@ -155,7 +155,7 @@ export async function runAutobuild(
|
||||
logger: Logger,
|
||||
) {
|
||||
logger.startGroup(`Attempting to automatically build ${language} code`);
|
||||
const codeQL = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeQL = await getCodeQL(config.codeQLCmd);
|
||||
if (language === BuiltInLanguage.cpp) {
|
||||
await setupCppAutobuild(codeQL, logger);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as core from "@actions/core";
|
||||
import { getOptionalInput, isDefaultSetup } from "./actions-util";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Logger } from "./logging";
|
||||
import { looksLikeHostedRunner, tryGetFolderBytes } from "./util";
|
||||
import { isHostedRunner, tryGetFolderBytes } from "./util";
|
||||
|
||||
/**
|
||||
* Returns the total size of all the specified paths.
|
||||
@@ -109,7 +109,7 @@ export function getDependencyCachingEnabled(): CachingKind {
|
||||
if (dependencyCaching !== undefined) return getCachingKind(dependencyCaching);
|
||||
|
||||
// On self-hosted runners which may have dependencies installed centrally, disable caching by default
|
||||
if (!looksLikeHostedRunner()) return CachingKind.None;
|
||||
if (!isHostedRunner()) return CachingKind.None;
|
||||
|
||||
// Disable in advanced workflows by default.
|
||||
if (!isDefaultSetup()) return CachingKind.None;
|
||||
|
||||
@@ -128,6 +128,7 @@ test("CliError constructor with empty stderr", (t) => {
|
||||
|
||||
for (const [platform, arch] of [
|
||||
["weird_plat", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "arm64"],
|
||||
]) {
|
||||
test.serial(
|
||||
@@ -156,34 +157,20 @@ for (const [platform, arch] of [
|
||||
);
|
||||
}
|
||||
|
||||
for (const [platform, arch] of [
|
||||
["linux", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "x64"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "arm64"],
|
||||
]) {
|
||||
test.serial(
|
||||
`wrapCliConfigurationError - ${platform}/${arch} supported`,
|
||||
(t) => {
|
||||
sinon.stub(process, "platform").value(platform);
|
||||
sinon.stub(process, "arch").value(arch);
|
||||
const commandError = new CommandInvocationError(
|
||||
"codeql",
|
||||
["version"],
|
||||
1,
|
||||
"Some error",
|
||||
);
|
||||
const cliError = new CliError(commandError);
|
||||
|
||||
const wrappedError = wrapCliConfigurationError(cliError);
|
||||
|
||||
// Should return the original error since the platform is supported, rather
|
||||
// than replacing it with the unsupported-platform ConfigurationError.
|
||||
t.is(wrappedError, cliError);
|
||||
},
|
||||
test("wrapCliConfigurationError - supported platform", (t) => {
|
||||
const commandError = new CommandInvocationError(
|
||||
"codeql",
|
||||
["version"],
|
||||
1,
|
||||
"Some error",
|
||||
);
|
||||
}
|
||||
const cliError = new CliError(commandError);
|
||||
|
||||
const wrappedError = wrapCliConfigurationError(cliError);
|
||||
|
||||
// Should return the original error since platform is supported
|
||||
t.is(wrappedError, cliError);
|
||||
});
|
||||
|
||||
test("wrapCliConfigurationError - autobuild error", (t) => {
|
||||
const commandError = new CommandInvocationError(
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ConfigurationError } from "./util";
|
||||
|
||||
const SUPPORTED_PLATFORMS = [
|
||||
["linux", "x64"],
|
||||
["linux", "arm64"],
|
||||
["win32", "x64"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "arm64"],
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { getRunnerLogger } from "../logging";
|
||||
import { setupTests } from "../testing-utils";
|
||||
import * as util from "../util";
|
||||
|
||||
import { getCachedCodeQlVersion } from "./output-cache";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
|
||||
fs.writeFileSync(
|
||||
cacheFilePath,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
t.deepEqual(
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
{
|
||||
version: "2.20.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
fs.writeFileSync(
|
||||
cacheFilePath,
|
||||
JSON.stringify({
|
||||
cmd: "/path/to/other-codeql",
|
||||
entries: { version: { version: "2.20.0" } },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
t.is(
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a malformed persisted value",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
fs.writeFileSync(cacheFilePath, "not valid json", "utf8");
|
||||
t.is(
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
|
||||
async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
const testValues = [
|
||||
{ cmd: "/path/to/codeql" },
|
||||
{ entries: { version: { version: "2.20.0" } } },
|
||||
{ cmd: "/path/to/codeql", entries: {} },
|
||||
{ cmd: "/path/to/codeql", entries: null },
|
||||
{ cmd: "/path/to/codeql", entries: { version: {} } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: null } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: "2.20.0" } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: null } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2.2 } } },
|
||||
{ cmd: "/path/to/codeql", entries: { version: { version: 2 } } },
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", overlayVersion: "1" } },
|
||||
},
|
||||
{
|
||||
cmd: "/path/to/codeql",
|
||||
entries: { version: { version: "2.20.0", features: "nope" } },
|
||||
},
|
||||
].map((v) => JSON.stringify(v));
|
||||
|
||||
for (const value of testValues) {
|
||||
fs.writeFileSync(cacheFilePath, value, "utf8");
|
||||
t.is(
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
value,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
|
||||
await util.withTmpDir(async (tmpDir: string) => {
|
||||
const cacheFilePath = path.join(tmpDir, "cache.json");
|
||||
t.notThrows(() => {
|
||||
t.is(
|
||||
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { getTemporaryDirectory } from "../actions-util";
|
||||
import { Env } from "../environment";
|
||||
import * as json from "../json";
|
||||
import { Logger } from "../logging";
|
||||
|
||||
import { VersionInfo, versionInfoBaseSchema } from "./types";
|
||||
|
||||
/**
|
||||
* The keys of the command cache. Each key corresponds to a command whose output we cache.
|
||||
*/
|
||||
export type CommandCacheKey = string;
|
||||
|
||||
/**
|
||||
* The JSON schema of the command cache that is persisted to disk.
|
||||
*/
|
||||
const outputCacheSchema = {
|
||||
cmd: json.string,
|
||||
entries: json.object({}),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* The type that describes the command cache that is persisted to disk.
|
||||
*/
|
||||
export type OutputCache = json.FromSchema<typeof outputCacheSchema> & {
|
||||
entries: { version: VersionInfo };
|
||||
};
|
||||
|
||||
/**
|
||||
* The name of the temporary file that backs the on-disk cache of
|
||||
* CLI responses between workflow steps.
|
||||
*/
|
||||
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
||||
|
||||
/**
|
||||
* The module-global variable that caches the CodeQL CLI version in-memory.
|
||||
*/
|
||||
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
|
||||
|
||||
/**
|
||||
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
|
||||
* which exercise multiple "steps" within a single process.
|
||||
*/
|
||||
export function resetCachedCodeQlVersion(): void {
|
||||
cachedCodeQlVersion = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the temporary file that backs the
|
||||
* on-disk cache of CLI responses between workflow steps.
|
||||
*/
|
||||
export function getCommandCacheFilePath(env: Env): string {
|
||||
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the CodeQL CLI version both in-memory and on disk.
|
||||
* @param cacheFilePath The path to the cache file.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
* @param version The version information to cache.
|
||||
*/
|
||||
export function cacheCodeQlVersion(
|
||||
cacheFilePath: string,
|
||||
cmd: string,
|
||||
version: VersionInfo,
|
||||
): void {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
throw new Error("cacheCodeQlVersion() should be called only once");
|
||||
}
|
||||
cachedCodeQlVersion = version;
|
||||
const outputCache = {
|
||||
cmd,
|
||||
entries: { version },
|
||||
} satisfies OutputCache;
|
||||
// Persist the version so that subsequent Actions steps, which run in separate
|
||||
// processes, can reuse it rather than invoking `codeql version` again. We
|
||||
// record the CLI path so that a different step using a different CodeQL bundle
|
||||
// doesn't pick up a stale version.
|
||||
fs.writeFileSync(cacheFilePath, JSON.stringify(outputCache), "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached CodeQL CLI version, if any.
|
||||
* @param logger The logger to use for logging messages.
|
||||
* @param cacheFilePath The path to the cache file.
|
||||
* @param cmd The path to the CodeQL CLI.
|
||||
*/
|
||||
export function getCachedCodeQlVersion(
|
||||
logger: Logger,
|
||||
cacheFilePath: string,
|
||||
cmd?: string,
|
||||
): undefined | VersionInfo {
|
||||
if (cachedCodeQlVersion !== undefined) {
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
// Fall back to the value persisted by an earlier Actions step, if any. This is
|
||||
// best-effort: any malformed or mismatched value is ignored so that the caller
|
||||
// invokes `codeql version` instead.
|
||||
let serialized: string;
|
||||
try {
|
||||
serialized = fs.readFileSync(cacheFilePath, "utf8");
|
||||
} catch (e) {
|
||||
logger.debug(`Cannot read CLI-cache file ${cacheFilePath}: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
let persisted: unknown;
|
||||
try {
|
||||
persisted = JSON.parse(serialized);
|
||||
} catch (e) {
|
||||
logger.debug(`Cannot parse CLI-cache data as JSON: ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!isOutputCache(persisted) ||
|
||||
(cmd !== undefined && persisted.cmd !== cmd)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Memoize the parsed value so that subsequent calls in this process don't
|
||||
// re-parse the environment variable.
|
||||
cachedCodeQlVersion = persisted.entries.version as VersionInfo;
|
||||
return cachedCodeQlVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `VersionInfo` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isVersionInfo(x: unknown): x is VersionInfo {
|
||||
return json.isObject(x) && json.validateSchema(versionInfoBaseSchema, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a value is a `OutputCache` object.
|
||||
* @param x The value to test
|
||||
*/
|
||||
function isOutputCache(x: unknown): x is OutputCache {
|
||||
return (
|
||||
json.isObject(x) &&
|
||||
json.validateSchema(outputCacheSchema, x) &&
|
||||
json.isObject<{ version: unknown }>(x.entries) &&
|
||||
isVersionInfo(x.entries.version)
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as json from "../json";
|
||||
|
||||
/**
|
||||
* The JSON schema of the expected output of the `codeql version` command.
|
||||
*/
|
||||
export const versionInfoBaseSchema = {
|
||||
version: json.string,
|
||||
features: json.optional(json.object({})),
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion: json.optional(json.number),
|
||||
} as const satisfies json.Schema;
|
||||
|
||||
/**
|
||||
* The base type that describes the expected output of the `codeql version` command.
|
||||
*/
|
||||
export type VersionInfoBase = json.FromSchema<typeof versionInfoBaseSchema>;
|
||||
|
||||
/**
|
||||
* The full type that describes the expected output of the `codeql version` command.
|
||||
*/
|
||||
export type VersionInfo = Omit<VersionInfoBase, "features"> & {
|
||||
features?: { [name: string]: boolean };
|
||||
};
|
||||
@@ -51,31 +51,6 @@ test.beforeEach(() => {
|
||||
});
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - true for expected errors", async (t) => {
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(new Error("ENOSPC: Out of disk space")),
|
||||
);
|
||||
t.true(
|
||||
codeql.isDiskConfigurationError(
|
||||
new Error(
|
||||
"EACCES: permission denied, mkdir /opt/hostedtoolcache/CodeQL/",
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("isDiskConfigurationError - false for other errors", async (t) => {
|
||||
t.false(codeql.isDiskConfigurationError("Not an Error instance"));
|
||||
|
||||
const otherMessages = [
|
||||
"Does not contain an error code we test for",
|
||||
"ENOSP: Not quite the full error code",
|
||||
];
|
||||
for (const otherMessage of otherMessages) {
|
||||
t.false(codeql.isDiskConfigurationError(new Error(otherMessage)));
|
||||
}
|
||||
});
|
||||
|
||||
async function installIntoToolcache({
|
||||
apiDetails = SAMPLE_DOTCOM_API_DETAILS,
|
||||
cliVersion,
|
||||
@@ -605,6 +580,7 @@ const injectedConfigMacro = makeMacro({
|
||||
"",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -880,6 +856,7 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
"/path/to/qlconfig.yml",
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as string[];
|
||||
@@ -910,6 +887,7 @@ test.serial(
|
||||
"",
|
||||
undefined,
|
||||
undefined, // undefined qlconfigFile
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const args = runnerConstructorStub.firstCall.args[1] as any[];
|
||||
@@ -1088,6 +1066,7 @@ test.serial(
|
||||
"sourceRoot",
|
||||
undefined,
|
||||
undefined,
|
||||
getRunnerLogger(false),
|
||||
);
|
||||
|
||||
t.true(runnerConstructorStub.calledOnce);
|
||||
|
||||
@@ -12,12 +12,10 @@ import {
|
||||
runTool,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import * as outputCache from "./cli/output-cache";
|
||||
import type { VersionInfo } from "./cli/types";
|
||||
import { CliError, wrapCliConfigurationError } from "./cli-errors";
|
||||
import { appendExtraQueryExclusions, type Config } from "./config-utils";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import {
|
||||
CodeQLDefaultVersionInfo,
|
||||
Feature,
|
||||
@@ -25,7 +23,7 @@ import {
|
||||
} from "./feature-flags";
|
||||
import { isAnalyzingDefaultBranch } from "./git-utils";
|
||||
import { Language } from "./languages";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { Logger } from "./logging";
|
||||
import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "./overlay";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import * as setupCodeql from "./setup-codeql";
|
||||
@@ -93,6 +91,7 @@ export interface CodeQL {
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Runs the autobuilder for the given language.
|
||||
@@ -216,6 +215,20 @@ export interface CodeQL {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
features?: { [name: string]: boolean };
|
||||
/**
|
||||
* The overlay version helps deal with backward incompatible changes for
|
||||
* overlay analysis. When a precompiled query pack reports the same overlay
|
||||
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
|
||||
* analysis with that pack. Otherwise, if the overlay versions are different,
|
||||
* or if either the pack or the CLI does not report an overlay version,
|
||||
* we need to revert to non-overlay analysis.
|
||||
*/
|
||||
overlayVersion?: number;
|
||||
}
|
||||
|
||||
export interface ResolveDatabaseOutput {
|
||||
overlayBaseSpecifier?: string;
|
||||
}
|
||||
@@ -273,26 +286,6 @@ const GHES_MOST_RECENT_DEPRECATION_DATE = "2026-07-01";
|
||||
/** The CLI verbosity level to use for extraction in debug mode. */
|
||||
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
|
||||
|
||||
/**
|
||||
* Decides whether `e` is a disk-related error outside of our control
|
||||
* that should be classified as a `ConfigurationError`.
|
||||
*
|
||||
* @param e The error to check.
|
||||
* @returns True if the error should be treated as a `ConfigurationError` or false if not.
|
||||
*/
|
||||
export function isDiskConfigurationError(e: unknown): boolean {
|
||||
if (!(e instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
// out of disk space
|
||||
e.message.includes("ENOSPC") ||
|
||||
// access denied
|
||||
e.message.includes("EACCES")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up CodeQL CLI access.
|
||||
*
|
||||
@@ -353,7 +346,7 @@ export async function setupCodeQL(
|
||||
);
|
||||
}
|
||||
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, codeqlCmd, checkVersion);
|
||||
cachedCodeQL = await getCodeQLForCmd(codeqlCmd, checkVersion);
|
||||
return {
|
||||
codeql: cachedCodeQL,
|
||||
toolsDownloadStatusReport,
|
||||
@@ -363,7 +356,8 @@ export async function setupCodeQL(
|
||||
} catch (rawError) {
|
||||
const e = api.wrapApiConfigurationError(rawError);
|
||||
const ErrorClass =
|
||||
e instanceof util.ConfigurationError || isDiskConfigurationError(e)
|
||||
e instanceof util.ConfigurationError ||
|
||||
(e instanceof Error && e.message.includes("ENOSPC")) // out of disk space
|
||||
? util.ConfigurationError
|
||||
: Error;
|
||||
|
||||
@@ -378,9 +372,9 @@ export async function setupCodeQL(
|
||||
/**
|
||||
* Use the CodeQL executable located at the given path.
|
||||
*/
|
||||
export async function getCodeQL(logger: Logger, cmd: string): Promise<CodeQL> {
|
||||
export async function getCodeQL(cmd: string): Promise<CodeQL> {
|
||||
if (cachedCodeQL === undefined) {
|
||||
cachedCodeQL = await getCodeQLForCmd(logger, cmd, true);
|
||||
cachedCodeQL = await getCodeQLForCmd(cmd, true);
|
||||
}
|
||||
return cachedCodeQL;
|
||||
}
|
||||
@@ -487,9 +481,8 @@ export function createStubCodeQL(partialCodeql: Partial<CodeQL>): CodeQL {
|
||||
*/
|
||||
export async function getCodeQLForTesting(
|
||||
cmd = "codeql-for-testing",
|
||||
logger: Logger = getRunnerLogger(true),
|
||||
): Promise<CodeQL> {
|
||||
return getCodeQLForCmd(logger, cmd, false);
|
||||
return getCodeQLForCmd(cmd, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,7 +494,6 @@ export async function getCodeQLForTesting(
|
||||
* @returns A new CodeQL object
|
||||
*/
|
||||
async function getCodeQLForCmd(
|
||||
logger: Logger,
|
||||
cmd: string,
|
||||
checkVersion: boolean,
|
||||
): Promise<CodeQL> {
|
||||
@@ -510,12 +502,7 @@ async function getCodeQLForCmd(
|
||||
return cmd;
|
||||
},
|
||||
async getVersion() {
|
||||
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
|
||||
let result = outputCache.getCachedCodeQlVersion(
|
||||
logger,
|
||||
cacheFilePath,
|
||||
cmd,
|
||||
);
|
||||
let result = util.getCachedCodeQlVersion(cmd);
|
||||
if (result === undefined) {
|
||||
result = await runCliJson<VersionInfo>(
|
||||
cmd,
|
||||
@@ -524,7 +511,7 @@ async function getCodeQLForCmd(
|
||||
noStreamStdout: true,
|
||||
},
|
||||
);
|
||||
outputCache.cacheCodeQlVersion(cacheFilePath, cmd, result);
|
||||
util.cacheCodeQlVersion(cmd, result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -552,6 +539,7 @@ async function getCodeQLForCmd(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
) {
|
||||
const extraArgs = config.languages.map(
|
||||
(language) => `--language=${language}`,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AnalysisKind, supportedAnalysisKinds } from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind } from "./caching-utils";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { UserConfig } from "./config/db-config";
|
||||
import { UserConfig, UserConfigWithActionExtensions } from "./config/db-config";
|
||||
import * as file from "./config/file";
|
||||
import * as configUtils from "./config-utils";
|
||||
import * as errorMessages from "./error-messages";
|
||||
@@ -1295,12 +1295,13 @@ checkOverlayEnablementMacro.serial(
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"No overlay-base database on default branch if runner disk space is below minimum",
|
||||
"No overlay-base database on default branch if runner disk space is below v2 limit and v2 resource checks enabled",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1314,12 +1315,13 @@ checkOverlayEnablementMacro.serial(
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"Overlay-base database on default branch if runner disk space is above minimum",
|
||||
"Overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks enabled",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
@@ -1333,93 +1335,17 @@ checkOverlayEnablementMacro.serial(
|
||||
},
|
||||
);
|
||||
|
||||
// Check that each feature flag lowers the limit to the threshold that its name
|
||||
// declares. Both sides of the boundary are needed to pin the threshold down: a
|
||||
// mapping to a lower value would still pass the case at the limit, and one to a
|
||||
// higher value would still fail the case below it.
|
||||
for (const [feature, thresholdGb] of [
|
||||
[Feature.OverlayAnalysisMinDisk8Gb, 8],
|
||||
[Feature.OverlayAnalysisMinDisk9Gb, 9],
|
||||
[Feature.OverlayAnalysisMinDisk10Gb, 10],
|
||||
[Feature.OverlayAnalysisMinDisk11Gb, 11],
|
||||
[Feature.OverlayAnalysisMinDisk12Gb, 12],
|
||||
[Feature.OverlayAnalysisMinDisk13Gb, 13],
|
||||
] as Array<[Feature, number]>) {
|
||||
const features = [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
feature,
|
||||
];
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
`Overlay-base database on default branch if ${feature} is enabled and runner disk space is at its limit`,
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features,
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: thresholdGb * 1_000_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
useOverlayDatabaseCaching: true,
|
||||
},
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
`No overlay-base database on default branch if ${feature} is enabled and runner disk space is below its limit`,
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features,
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: thresholdGb * 1_000_000_000 - 1_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
disabledReason: OverlayDisabledReason.InsufficientDiskSpace,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"Overlay-base database on default branch if runner disk space is exactly at the lowest limit enabled by a feature flag",
|
||||
"No overlay-base database on default branch if runner disk space is between v2 and v1 limits and v2 resource checks not enabled",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisMinDisk9Gb,
|
||||
Feature.OverlayAnalysisMinDisk12Gb,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: 9_000_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
useOverlayDatabaseCaching: true,
|
||||
},
|
||||
);
|
||||
|
||||
checkOverlayEnablementMacro.serial(
|
||||
"No overlay-base database on default branch if runner disk space is below the lowest limit enabled by a feature flag",
|
||||
{
|
||||
languages: [BuiltInLanguage.javascript],
|
||||
features: [
|
||||
Feature.OverlayAnalysis,
|
||||
Feature.OverlayAnalysisCodeScanningJavascript,
|
||||
Feature.OverlayAnalysisMinDisk9Gb,
|
||||
Feature.OverlayAnalysisMinDisk12Gb,
|
||||
],
|
||||
isDefaultBranch: true,
|
||||
diskUsage: {
|
||||
numAvailableBytes: 8_500_000_000,
|
||||
numAvailableBytes: 15_000_000_000,
|
||||
numTotalBytes: 100_000_000_000,
|
||||
},
|
||||
},
|
||||
@@ -2618,6 +2544,7 @@ test("loadUserConfig - loads local configuration files", async (t) => {
|
||||
) =>
|
||||
configUtils.loadUserConfig(
|
||||
actionState,
|
||||
[AnalysisKind.CodeScanning],
|
||||
filePath,
|
||||
workspaceDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
@@ -2661,7 +2588,13 @@ 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(
|
||||
@@ -2739,6 +2672,7 @@ test.serial(
|
||||
|
||||
// Prepare the test call to `loadUserConfig`.
|
||||
const targetWithArgs = target.withArgs(
|
||||
[AnalysisKind.CodeScanning],
|
||||
address,
|
||||
tmpDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
@@ -2779,3 +2713,57 @@ test.serial(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("loadUserConfig - applies analysis-specific settings", async (t) => {
|
||||
await withTmpDir(async (workspaceDir) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
// Construct the test target.
|
||||
const loadUserConfig = (
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
filePath: string,
|
||||
analysisKind: AnalysisKind,
|
||||
) =>
|
||||
configUtils.loadUserConfig(
|
||||
actionState,
|
||||
[analysisKind],
|
||||
filePath,
|
||||
workspaceDir,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
);
|
||||
const target = callee(loadUserConfig);
|
||||
|
||||
for (const analysisKind of Object.values(AnalysisKind)) {
|
||||
const analysisSpecificConfig: UserConfig = {
|
||||
paths: ["custom"],
|
||||
queries: [{ uses: "foo" }],
|
||||
};
|
||||
const baseConfig: UserConfig = { "threat-models": ["remote"] };
|
||||
|
||||
// Write a local configuration file with an `analysisKind`-specific section.
|
||||
const configPath = path.join(workspaceDir, "codeql.yml");
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
...baseConfig,
|
||||
[analysisKind]: analysisSpecificConfig,
|
||||
} satisfies UserConfigWithActionExtensions),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// The resulting configuration should be the `baseConfig` with the `analysisSpecificConfig`
|
||||
// merged into it if the FF is enabled.
|
||||
await target
|
||||
.withArgs(configPath, analysisKind)
|
||||
.withFeatures([Feature.ScopedConfigurations])
|
||||
.passes(t.deepEqual, { ...baseConfig, ...analysisSpecificConfig });
|
||||
|
||||
// Without the FF, it should be the `baseConfig`. This also validates that the
|
||||
// `AnalysisKind`-specific sections are removed even if the FF is off.
|
||||
await target
|
||||
.withArgs(configPath, analysisKind)
|
||||
.passes(t.deepEqual, baseConfig);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,11 +24,13 @@ import { getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { type Config } from "./config/action-config";
|
||||
import {
|
||||
applyAnalysisKindConfig,
|
||||
calculateAugmentation,
|
||||
ExcludeQueryFilter,
|
||||
generateCodeScanningConfig,
|
||||
mergeDefaultSetupAndUserConfigs,
|
||||
parseUserConfig,
|
||||
removeAnalysisKindConfigs,
|
||||
UserConfig,
|
||||
} from "./config/db-config";
|
||||
import {
|
||||
@@ -48,7 +50,7 @@ import {
|
||||
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
|
||||
import { EnvVar } from "./environment";
|
||||
import * as errorMessages from "./error-messages";
|
||||
import { Feature, FeatureEnablement, FeatureWithoutCLI } from "./feature-flags";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import {
|
||||
RepositoryProperties,
|
||||
RepositoryPropertyName,
|
||||
@@ -90,8 +92,9 @@ import {
|
||||
Result,
|
||||
Success,
|
||||
Failure,
|
||||
looksLikeHostedRunner,
|
||||
isHostedRunner,
|
||||
} from "./util";
|
||||
|
||||
export { type Config } from "./config/action-config";
|
||||
|
||||
/**
|
||||
@@ -100,23 +103,19 @@ export { type Config } from "./config/action-config";
|
||||
* whether to perform overlay analysis, then the action will not perform overlay
|
||||
* analysis unless overlay analysis has been explicitly enabled via environment
|
||||
* variable.
|
||||
*
|
||||
* This threshold can be lowered by the feature flags in
|
||||
* `OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE`.
|
||||
*/
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 14000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB = 20000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES =
|
||||
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB * 1_000_000;
|
||||
|
||||
/**
|
||||
* Minimum available disk space (in MB) enabled by each overlay feature flag.
|
||||
* The v2 minimum available disk space (in MB) required to perform overlay
|
||||
* analysis. This is a lower threshold than the v1 limit, allowing overlay
|
||||
* analysis to run on runners with less available disk space.
|
||||
*/
|
||||
const OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE = {
|
||||
[Feature.OverlayAnalysisMinDisk8Gb]: 8000,
|
||||
[Feature.OverlayAnalysisMinDisk9Gb]: 9000,
|
||||
[Feature.OverlayAnalysisMinDisk10Gb]: 10000,
|
||||
[Feature.OverlayAnalysisMinDisk11Gb]: 11000,
|
||||
[Feature.OverlayAnalysisMinDisk12Gb]: 12000,
|
||||
[Feature.OverlayAnalysisMinDisk13Gb]: 13000,
|
||||
} satisfies Partial<Record<FeatureWithoutCLI, number>>;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB = 14000;
|
||||
const OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES =
|
||||
OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_MB * 1_000_000;
|
||||
|
||||
/**
|
||||
* The minimum memory (in MB) that must be available for CodeQL to perform overlay analysis. If
|
||||
@@ -487,11 +486,14 @@ async function downloadCacheWithTime(
|
||||
*/
|
||||
export async function loadUserConfig(
|
||||
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
|
||||
analysisKinds: AnalysisKind[],
|
||||
configFile: string,
|
||||
workspacePath: string,
|
||||
apiDetails: api.GitHubApiCombinedDetails,
|
||||
tempDir: string,
|
||||
): Promise<UserConfig> {
|
||||
let config: UserConfig;
|
||||
|
||||
if (isLocal(configFile)) {
|
||||
if (configFile !== userConfigFromActionPath(tempDir)) {
|
||||
// If the config file is not generated by the Action, it should be relative to the workspace.
|
||||
@@ -506,7 +508,7 @@ export async function loadUserConfig(
|
||||
const validateConfig = await actionState.features.getValue(
|
||||
Feature.ValidateDbConfig,
|
||||
);
|
||||
return getLocalConfig(actionState.logger, configFile, validateConfig);
|
||||
config = getLocalConfig(actionState.logger, configFile, validateConfig);
|
||||
} else {
|
||||
// Drop the explicit prefix if it is present. Since `REMOTE_PATH_PREFIX` is chosen
|
||||
// to not conflict with permissible characters in "owner" or "repo" components,
|
||||
@@ -514,8 +516,29 @@ export async function loadUserConfig(
|
||||
if (isExplicitRemotePath(configFile)) {
|
||||
configFile = configFile.substring(REMOTE_PATH_PREFIX.length);
|
||||
}
|
||||
return await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
config = await getRemoteConfig(actionState, configFile, apiDetails);
|
||||
}
|
||||
|
||||
// If the `ScopedConfigurations` feature is enabled, we allow options specific to the
|
||||
// current `AnalysisKind` to be applied on top of a base configuration.
|
||||
const allowScopedConfig = await actionState.features.getValue(
|
||||
Feature.ScopedConfigurations,
|
||||
);
|
||||
|
||||
if (allowScopedConfig && analysisKinds.length === 1) {
|
||||
// Apply the `analysisKind`-specific configuration.
|
||||
return applyAnalysisKindConfig(analysisKinds[0], config);
|
||||
} else if (allowScopedConfig) {
|
||||
actionState.logger.info(
|
||||
`Ignoring '${Feature.ScopedConfigurations}' feature, because multiple analysis kinds are enabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Remove `AnalysisKind`-specific sections from `config` even if the FF is not enabled,
|
||||
// because the CLI will warn about unknown keys in the configuration. This is to guard
|
||||
// against the case where we ship the feature, enable the FF, start using the new keys,
|
||||
// and then need to roll the FF back.
|
||||
return removeAnalysisKindConfigs(config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,44 +614,24 @@ async function checkOverlayAnalysisFeatureEnabled(
|
||||
return new Success(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum available disk space (in MB) required to perform overlay
|
||||
* analysis, which is the lowest threshold enabled by a feature flag, or the
|
||||
* default threshold if no such feature flag is enabled.
|
||||
*/
|
||||
async function getMinimumDiskSpaceMb(
|
||||
features: FeatureEnablement,
|
||||
): Promise<number> {
|
||||
let minimumMb = OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_MB;
|
||||
for (const [feature, thresholdMb] of Object.entries(
|
||||
OVERLAY_MINIMUM_DISK_SPACE_MB_BY_FEATURE,
|
||||
)) {
|
||||
if (await features.getValue(feature as FeatureWithoutCLI)) {
|
||||
minimumMb = Math.min(minimumMb, thresholdMb);
|
||||
}
|
||||
}
|
||||
return minimumMb;
|
||||
}
|
||||
|
||||
/** Checks if the runner has enough disk space for overlay analysis. */
|
||||
function runnerHasSufficientDiskSpace(
|
||||
diskUsage: DiskUsage,
|
||||
logger: Logger,
|
||||
minimumDiskSpaceMb: number,
|
||||
useV2ResourceChecks: boolean,
|
||||
): boolean {
|
||||
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
|
||||
if (diskUsage.numAvailableBytes < minimumDiskSpaceMb * 1_000_000) {
|
||||
const minimumDiskSpaceBytes = useV2ResourceChecks
|
||||
? OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_V2_BYTES
|
||||
: OVERLAY_MINIMUM_AVAILABLE_DISK_SPACE_BYTES;
|
||||
if (diskUsage.numAvailableBytes < minimumDiskSpaceBytes) {
|
||||
const diskSpaceMb = Math.round(diskUsage.numAvailableBytes / 1_000_000);
|
||||
const minimumDiskSpaceMb = Math.round(minimumDiskSpaceBytes / 1_000_000);
|
||||
logger.info(
|
||||
`Setting overlay database mode to ${OverlayDatabaseMode.None} ` +
|
||||
`due to insufficient disk space (${diskSpaceMb} MB, needed ${minimumDiskSpaceMb} MB).`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Disk space available for CodeQL analysis is ${diskSpaceMb} MB, which is at or above the ` +
|
||||
`minimum of ${minimumDiskSpaceMb} MB.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -660,7 +663,7 @@ async function runnerHasSufficientMemory(
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is at or above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
|
||||
`Memory available for CodeQL analysis is ${memoryFlagValue} MB, which is above the minimum of ${OVERLAY_MINIMUM_MEMORY_MB} MB.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -671,13 +674,12 @@ async function runnerHasSufficientMemory(
|
||||
*/
|
||||
async function checkRunnerResources(
|
||||
codeql: CodeQL,
|
||||
features: FeatureEnablement,
|
||||
diskUsage: DiskUsage,
|
||||
ramInput: string | undefined,
|
||||
logger: Logger,
|
||||
useV2ResourceChecks: boolean,
|
||||
): Promise<Result<void, OverlayDisabledReason>> {
|
||||
const minimumDiskSpaceMb = await getMinimumDiskSpaceMb(features);
|
||||
if (!runnerHasSufficientDiskSpace(diskUsage, logger, minimumDiskSpaceMb)) {
|
||||
if (!runnerHasSufficientDiskSpace(diskUsage, logger, useV2ResourceChecks)) {
|
||||
return new Failure(OverlayDisabledReason.InsufficientDiskSpace);
|
||||
}
|
||||
if (!(await runnerHasSufficientMemory(codeql, ramInput, logger))) {
|
||||
@@ -776,6 +778,9 @@ export async function checkOverlayEnablement(
|
||||
Feature.OverlayAnalysisSkipResourceChecks,
|
||||
codeql,
|
||||
));
|
||||
const useV2ResourceChecks = await features.getValue(
|
||||
Feature.OverlayAnalysisResourceChecksV2,
|
||||
);
|
||||
const checkOverlayStatus = await features.getValue(
|
||||
Feature.OverlayAnalysisStatusCheck,
|
||||
);
|
||||
@@ -791,10 +796,10 @@ export async function checkOverlayEnablement(
|
||||
performResourceChecks && diskUsage !== undefined
|
||||
? await checkRunnerResources(
|
||||
codeql,
|
||||
features,
|
||||
diskUsage,
|
||||
ramInput,
|
||||
logger,
|
||||
useV2ResourceChecks,
|
||||
)
|
||||
: new Success<void>(undefined);
|
||||
if (resourceResult.isFailure()) {
|
||||
@@ -937,7 +942,7 @@ export async function isTrapCachingEnabled(
|
||||
if (trapCaching !== undefined) return trapCaching === "true";
|
||||
|
||||
// On self-hosted runners which may have slow network access, disable TRAP caching by default.
|
||||
if (!looksLikeHostedRunner()) return false;
|
||||
if (!isHostedRunner()) return false;
|
||||
|
||||
// If overlay analysis is enabled, then disable TRAP caching since overlay analysis supersedes it.
|
||||
// This change is gated behind a feature flag.
|
||||
@@ -1092,6 +1097,7 @@ export async function determineUserConfig(
|
||||
);
|
||||
const fromConfigFile = await loadUserConfig(
|
||||
action,
|
||||
inputs.analysisKinds,
|
||||
inputs.configFile,
|
||||
inputs.workspacePath,
|
||||
inputs.apiDetails,
|
||||
@@ -1139,6 +1145,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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test, { ExecutionContext } from "ava";
|
||||
|
||||
import { AnalysisKind } from "../analyses";
|
||||
import { RepositoryProperties } from "../feature-flags/properties";
|
||||
import { BuiltInLanguage, Language } from "../languages";
|
||||
import { getRunnerLogger } from "../logging";
|
||||
@@ -14,6 +15,117 @@ import { ConfigurationError, prettyPrintPack } from "../util";
|
||||
|
||||
import * as dbConfig from "./db-config";
|
||||
|
||||
function makeUserConfigWithActionExtensions(options?: {
|
||||
analysisConfig?: dbConfig.UserConfig;
|
||||
analysisKinds?: AnalysisKind[];
|
||||
baseConfig?: dbConfig.UserConfig;
|
||||
}) {
|
||||
const testConfig = { ...options?.baseConfig };
|
||||
|
||||
const allAnalysisKinds = Object.values(AnalysisKind);
|
||||
const analysisKinds = options?.analysisKinds ?? allAnalysisKinds;
|
||||
for (const analysisKind of analysisKinds) {
|
||||
testConfig[analysisKind] = { ...options?.analysisConfig };
|
||||
}
|
||||
|
||||
return testConfig as dbConfig.UserConfigWithActionExtensions;
|
||||
}
|
||||
|
||||
test("hasAnalysisKindKey - finds `AnalysisKind` sections", async (t) => {
|
||||
const testConfig = makeUserConfigWithActionExtensions();
|
||||
|
||||
for (const analysisKind of Object.values(AnalysisKind)) {
|
||||
// Check that it finds the `analysisKind`-specific section among multiple.
|
||||
t.true(dbConfig.hasAnalysisKindKey(analysisKind, testConfig));
|
||||
// And for just the `analysisKind`-specific section.
|
||||
t.true(dbConfig.hasAnalysisKindKey(analysisKind, { [analysisKind]: {} }));
|
||||
}
|
||||
});
|
||||
|
||||
test("hasAnalysisKindKey - returns false if there is no relevant section", async (t) => {
|
||||
const testConfig = makeUserConfigWithActionExtensions();
|
||||
|
||||
for (const analysisKind of Object.values(AnalysisKind)) {
|
||||
// Clone the `testConfig` and remove the `analysisKind`-specific key.
|
||||
const testConfigClone = { ...testConfig };
|
||||
delete testConfigClone[analysisKind];
|
||||
|
||||
// Check that it doesn't find an `analysisKind`-specific section.
|
||||
t.false(dbConfig.hasAnalysisKindKey(analysisKind, testConfigClone));
|
||||
}
|
||||
});
|
||||
|
||||
test("applyAnalysisKindConfig - applies analysis-specific settings", async (t) => {
|
||||
for (const analysisKind of Object.values(AnalysisKind)) {
|
||||
const testConfig = makeUserConfigWithActionExtensions({
|
||||
analysisKinds: [analysisKind],
|
||||
analysisConfig: {
|
||||
"paths-ignore": [],
|
||||
},
|
||||
baseConfig: {
|
||||
"paths-ignore": ["a", "b", "c"],
|
||||
"threat-models": ["local"],
|
||||
},
|
||||
});
|
||||
const clonedTestConfig = { ...testConfig };
|
||||
|
||||
const result = dbConfig.applyAnalysisKindConfig(analysisKind, testConfig);
|
||||
|
||||
// The `analysisKind`-specific section should have been removed.
|
||||
// `threat-models` should be unchanged from the `baseConfig`.
|
||||
// `paths-ignore` should have been overwritten with the `analysisKind`-specific config.
|
||||
t.deepEqual(result, {
|
||||
"threat-models": ["local"],
|
||||
"paths-ignore": [],
|
||||
});
|
||||
|
||||
// The input `testConfig` should not have changed.
|
||||
t.deepEqual(testConfig, clonedTestConfig);
|
||||
}
|
||||
});
|
||||
|
||||
test("applyAnalysisKindConfig - removes all `AnalysisKind`-specific keys", async (t) => {
|
||||
for (const analysisKind of Object.values(AnalysisKind)) {
|
||||
const testConfig = makeUserConfigWithActionExtensions({
|
||||
analysisConfig: {
|
||||
"paths-ignore": ["a", "b", "c"],
|
||||
},
|
||||
baseConfig: {},
|
||||
});
|
||||
|
||||
const result = dbConfig.applyAnalysisKindConfig(analysisKind, testConfig);
|
||||
|
||||
// All `AnalysisKind`-specific sections should have been removed.
|
||||
// `paths-ignore` should have been overwritten with the analysis-specific config.
|
||||
t.deepEqual(result, {
|
||||
"paths-ignore": ["a", "b", "c"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("applyAnalysisKindConfig - returns the `baseConfig` if there is no `analysisKind`-specific key", async (t) => {
|
||||
const baseConfig = {
|
||||
"disable-default-queries": true,
|
||||
"paths-ignore": ["a", "b", "c"],
|
||||
queries: [{ name: "foo", uses: "something" }],
|
||||
packs: { test: ["bar"] },
|
||||
"default-setup": { org: { "model-packs": ["pack"] } },
|
||||
} satisfies dbConfig.UserConfig;
|
||||
|
||||
const testConfig = makeUserConfigWithActionExtensions({
|
||||
analysisKinds: [],
|
||||
baseConfig,
|
||||
});
|
||||
|
||||
const result = dbConfig.applyAnalysisKindConfig(
|
||||
AnalysisKind.CodeScanning,
|
||||
testConfig,
|
||||
);
|
||||
|
||||
// The result should be the same as the `baseConfig`.
|
||||
t.deepEqual(result, baseConfig);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test macro for ensuring the packs block is valid
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as yaml from "js-yaml";
|
||||
import * as jsonschema from "jsonschema";
|
||||
import * as semver from "semver";
|
||||
|
||||
import { AnalysisKind } from "../analyses";
|
||||
import {
|
||||
addNoLanguageDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
@@ -77,6 +78,68 @@ export interface UserConfig {
|
||||
"default-setup"?: DefaultSetupConfig;
|
||||
}
|
||||
|
||||
/** Represents a `UserConfig` with CodeQL Action extensions. */
|
||||
export type UserConfigWithActionExtensions = {
|
||||
/**
|
||||
* We add a key for each analysis kind that may contain options
|
||||
* specific to that kind of analysis.
|
||||
*/
|
||||
[T in keyof typeof AnalysisKind as (typeof AnalysisKind)[T]]?: UserConfig;
|
||||
} & UserConfig;
|
||||
|
||||
/**
|
||||
* Determines whether `config` contains an `analysisKind`-specific section.
|
||||
*
|
||||
* @param analysisKind The analysis section to check for.
|
||||
* @param config The base `UserConfig` value.
|
||||
*/
|
||||
export function hasAnalysisKindKey(
|
||||
analysisKind: AnalysisKind,
|
||||
config: UserConfig,
|
||||
): config is UserConfigWithActionExtensions {
|
||||
return analysisKind in config && typeof config[analysisKind] === "object";
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones `config` and removes any `AnalysisKind`-specific sections.
|
||||
*/
|
||||
export function removeAnalysisKindConfigs(config: UserConfig): UserConfig {
|
||||
// Clone the configuration and remove any `AnalysisKind`-specific sections.
|
||||
const configWithoutScopes = { ...config };
|
||||
for (const kind of Object.values(AnalysisKind)) {
|
||||
delete configWithoutScopes[kind];
|
||||
}
|
||||
return configWithoutScopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an `analysisKind`-specific configuration, if any, to the resulting configuration
|
||||
* and removes all `AnalysisKind`-specific configuration sections.
|
||||
*
|
||||
* @param analysisKind The `AnalysisKind` to apply specific configuration options for.
|
||||
* @param config The loaded configuration, possibly including `analysisKind`-specific settings.
|
||||
*/
|
||||
export function applyAnalysisKindConfig(
|
||||
analysisKind: AnalysisKind,
|
||||
config: UserConfig,
|
||||
): UserConfig {
|
||||
// Clone the configuration and remove any `AnalysisKind`-specific sections.
|
||||
const configWithoutScopes = removeAnalysisKindConfigs(config);
|
||||
|
||||
// If there is no `analysisKind`-specific section, return the input configuration
|
||||
// without any `AnalysisKind`-specific sections.
|
||||
if (!hasAnalysisKindKey(analysisKind, config)) {
|
||||
return configWithoutScopes;
|
||||
}
|
||||
|
||||
// Otherwise, extract the `analysisKind`-specific configuration from the input `config`.
|
||||
const analysisKindSpecificConfig = config[analysisKind];
|
||||
|
||||
// Return the base configuration (without any `AnalysisKind`-specific sections) and
|
||||
// override the top-level properties with the `analysisKind`-specific settings.
|
||||
return { ...configWithoutScopes, ...analysisKindSpecificConfig };
|
||||
}
|
||||
|
||||
/** A subset of the `UserConfig` schema that is used by Default Setup. */
|
||||
const DEFAULT_SETUP_CONFIG_SCHEMA = {
|
||||
"threat-models": json.optional(json.array(json.string)),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.27.0",
|
||||
"cliVersion": "2.27.0",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.4",
|
||||
"priorCliVersion": "2.26.4"
|
||||
"bundleVersion": "codeql-bundle-v2.26.1",
|
||||
"cliVersion": "2.26.1",
|
||||
"priorBundleVersion": "codeql-bundle-v2.26.0",
|
||||
"priorCliVersion": "2.26.0"
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ export enum EnvVar {
|
||||
*/
|
||||
CODE_SCANNING_REF = "CODE_SCANNING_REF",
|
||||
|
||||
/**
|
||||
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
|
||||
* invoking `codeql version` again.
|
||||
*/
|
||||
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
|
||||
|
||||
/** Whether the CodeQL Action has invoked the Go autobuilder. */
|
||||
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
|
||||
|
||||
@@ -63,12 +69,6 @@ export enum EnvVar {
|
||||
/** Whether the CodeQL Action has already warned the user about low disk space. */
|
||||
HAS_WARNED_ABOUT_DISK_SPACE = "CODEQL_ACTION_HAS_WARNED_ABOUT_DISK_SPACE",
|
||||
|
||||
/**
|
||||
* Whether a step in this job has already set up CodeQL. Steps that run afterwards may be holding
|
||||
* a path into the toolcache, so we must not delete anything from it.
|
||||
*/
|
||||
HAS_SET_UP_CODEQL = "CODEQL_ACTION_HAS_SET_UP_CODEQL",
|
||||
|
||||
/** Whether the `setup-codeql` action has been run. */
|
||||
SETUP_CODEQL_ACTION_HAS_RUN = "CODEQL_ACTION_SETUP_CODEQL_HAS_RUN",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -74,11 +74,6 @@ export enum Feature {
|
||||
AllowMergeConfigFiles = "allow_merge_config_files",
|
||||
/** Controls whether we allow multiple values for the `analysis-kinds` input. */
|
||||
AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds",
|
||||
/**
|
||||
* Controls whether we delete CodeQL bundles that we are not going to use from the toolcache
|
||||
* before downloading a different bundle, in order to reclaim disk space.
|
||||
*/
|
||||
CleanupToolcacheBundles = "cleanup_toolcache_bundles",
|
||||
CleanupTrapCaches = "cleanup_trap_caches",
|
||||
/** Whether to allow the `config-file` input to be specified via a repository property. */
|
||||
ConfigFileRepositoryProperty = "config_file_repository_property",
|
||||
@@ -126,37 +121,12 @@ export enum Feature {
|
||||
* `OverlayAnalysisMatchCodeqlVersion` overrides this flag.
|
||||
*/
|
||||
OverlayAnalysisMatchCodeqlVersionDryRun = "overlay_analysis_match_codeql_version_dry_run",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 8 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk8Gb = "overlay_analysis_min_disk_8_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 9 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk9Gb = "overlay_analysis_min_disk_9_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 10 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk10Gb = "overlay_analysis_min_disk_10_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 11 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk11Gb = "overlay_analysis_min_disk_11_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 12 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk12Gb = "overlay_analysis_min_disk_12_gb",
|
||||
/**
|
||||
* Lowers the overlay minimum available disk space to 13 GB. The lowest enabled limit wins; if
|
||||
* none are enabled, the default applies.
|
||||
*/
|
||||
OverlayAnalysisMinDisk13Gb = "overlay_analysis_min_disk_13_gb",
|
||||
OverlayAnalysisPython = "overlay_analysis_python",
|
||||
/**
|
||||
* Controls whether lower disk space requirements are used for overlay hardware checks.
|
||||
* Has no effect if `OverlayAnalysisSkipResourceChecks` is enabled.
|
||||
*/
|
||||
OverlayAnalysisResourceChecksV2 = "overlay_analysis_resource_checks_v2",
|
||||
OverlayAnalysisRuby = "overlay_analysis_ruby",
|
||||
/** Controls whether hardware checks are skipped for overlay analysis. */
|
||||
OverlayAnalysisSkipResourceChecks = "overlay_analysis_skip_resource_checks",
|
||||
@@ -167,6 +137,8 @@ export enum Feature {
|
||||
QaTelemetryEnabled = "qa_telemetry_enabled",
|
||||
/** Routes (some) API requests through the registry proxy. */
|
||||
ProxyApiRequests = "proxy_api_requests",
|
||||
/** Allows sections specific to analysis kinds in configuration files. */
|
||||
ScopedConfigurations = "scoped_configurations",
|
||||
/** Note that this currently only disables baseline file coverage information. */
|
||||
SkipFileCoverageOnPrs = "skip_file_coverage_on_prs",
|
||||
StartProxyUseFeaturesRelease = "start_proxy_use_features_release",
|
||||
@@ -216,11 +188,6 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CleanupToolcacheBundles]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TOOLCACHE_BUNDLES",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.CleanupTrapCaches]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_CLEANUP_TRAP_CACHES",
|
||||
@@ -389,34 +356,9 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MATCH_CODEQL_VERSION_DRY_RUN",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk8Gb]: {
|
||||
[Feature.OverlayAnalysisResourceChecksV2]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_8_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk9Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_9_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk10Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_10_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk11Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_11_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk12Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_12_GB",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisMinDisk13Gb]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_MIN_DISK_13_GB",
|
||||
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RESOURCE_CHECKS_V2",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.OverlayAnalysisStatusCheck]: {
|
||||
@@ -445,6 +387,11 @@ export const featureConfig = {
|
||||
envVar: "CODEQL_ACTION_PROXY_API_REQUESTS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.ScopedConfigurations]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_SCOPED_CONFIGURATIONS",
|
||||
minimumVersion: undefined,
|
||||
},
|
||||
[Feature.SkipFileCoverageOnPrs]: {
|
||||
defaultValue: false,
|
||||
envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",
|
||||
|
||||
@@ -15,12 +15,10 @@ import { getRunnerLogger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
import * as overlayStatus from "./overlay/status";
|
||||
import { parseRepositoryNwo } from "./repository";
|
||||
import { JobStatus } from "./status-report";
|
||||
import {
|
||||
createFeatures,
|
||||
createTestConfig,
|
||||
DEFAULT_ACTIONS_VARS,
|
||||
getTestEnv,
|
||||
makeMacro,
|
||||
makeVersionInfo,
|
||||
RecordingLogger,
|
||||
@@ -60,8 +58,6 @@ test.serial("init-post action with debug mode off", async (t) => {
|
||||
createTestConfig({ debugMode: false }),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -84,8 +80,6 @@ test.serial("init-post action with debug mode on", async (t) => {
|
||||
createTestConfig({ debugMode: true }),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -381,8 +375,6 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -451,8 +443,6 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -467,13 +457,8 @@ test.serial(
|
||||
test.serial("does not save overlay status when build successful", async (t) => {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
// Mark analyze as having completed successfully. `tryUploadSarifIfRunFailed` reads this from
|
||||
// the process environment, while `recordOverlayStatus` reads it from the environment it is
|
||||
// given.
|
||||
// Mark analyze as having completed successfully.
|
||||
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] = "true";
|
||||
const env = getTestEnv({
|
||||
[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY]: "true",
|
||||
});
|
||||
|
||||
sinon.stub(util, "checkDiskUsage").resolves({
|
||||
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
|
||||
@@ -495,8 +480,6 @@ test.serial("does not save overlay status when build successful", async (t) => {
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
"success",
|
||||
env,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -534,8 +517,6 @@ test.serial(
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([]),
|
||||
"success",
|
||||
getTestEnv(),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -547,137 +528,6 @@ test.serial(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Runs `uploadFailureInfo` for an overlay-base job that did not complete successfully, with the
|
||||
* given job status from the Actions runtime environment.
|
||||
*/
|
||||
async function runOverlayPostStep({
|
||||
jobStatus,
|
||||
codeQlReportedError = false,
|
||||
}: {
|
||||
jobStatus: string | undefined;
|
||||
codeQlReportedError?: boolean;
|
||||
}) {
|
||||
return await util.withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
delete process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY];
|
||||
const env = getTestEnv(
|
||||
codeQlReportedError
|
||||
? { [EnvVar.JOB_STATUS]: JobStatus.FailureStatus }
|
||||
: {},
|
||||
);
|
||||
|
||||
sinon.stub(util, "checkDiskUsage").resolves({
|
||||
numAvailableBytes: 100 * NUM_BYTES_PER_GIB,
|
||||
numTotalBytes: 200 * NUM_BYTES_PER_GIB,
|
||||
});
|
||||
|
||||
const saveOverlayStatusStub = sinon
|
||||
.stub(overlayStatus, "saveOverlayStatus")
|
||||
.resolves(true);
|
||||
|
||||
await initActionPostHelper.uploadFailureInfo(
|
||||
sinon.spy(),
|
||||
sinon.spy(),
|
||||
codeql.createStubCodeQL({}),
|
||||
createTestConfig({
|
||||
debugMode: false,
|
||||
languages: ["javascript"],
|
||||
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
|
||||
}),
|
||||
parseRepositoryNwo("github/codeql-action"),
|
||||
createFeatures([Feature.OverlayAnalysisStatusSave]),
|
||||
jobStatus,
|
||||
env,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
return { saveOverlayStatusStub };
|
||||
});
|
||||
}
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job was cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "cancelled",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"a cancellation tells us nothing about whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job status is not recognised",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "some-new-status",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"a status we do not recognise tells us nothing about whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"does not save overlay status when the job status is unavailable",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: undefined,
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.notCalled,
|
||||
"without a job status we cannot tell whether the analysis would have succeeded",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"saves overlay status when the job failed rather than being cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "failure",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"a failed job indicates that the analysis itself failed",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial("saves overlay status when the job succeeded", async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "success",
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"the analysis did not complete successfully even though the job as a whole succeeded",
|
||||
);
|
||||
});
|
||||
|
||||
test.serial(
|
||||
"saves overlay status when a CodeQL Action reported an error before the run was cancelled",
|
||||
async (t) => {
|
||||
const { saveOverlayStatusStub } = await runOverlayPostStep({
|
||||
jobStatus: "cancelled",
|
||||
codeQlReportedError: true,
|
||||
});
|
||||
|
||||
t.true(
|
||||
saveOverlayStatusStub.calledOnce,
|
||||
"the analysis genuinely failed, even though the run was later cancelled",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function createTestWorkflow(
|
||||
steps: workflow.WorkflowJobStep[],
|
||||
): workflow.Workflow {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
sanitizeArtifactName,
|
||||
} from "./debug-artifacts";
|
||||
import * as dependencyCaching from "./dependency-caching";
|
||||
import { EnvVar, ReadOnlyEnv } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { Logger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
|
||||
@@ -123,7 +123,6 @@ async function prepareFailedSarif(
|
||||
const category = `/language:${language}`;
|
||||
const checkoutPath = ".";
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -147,7 +146,6 @@ async function prepareFailedSarif(
|
||||
const checkoutPath = getCheckoutPathInputOrThrow(workflow, jobName, matrix);
|
||||
|
||||
const result = await generateFailedSarif(
|
||||
logger,
|
||||
features,
|
||||
config,
|
||||
category,
|
||||
@@ -158,7 +156,6 @@ async function prepareFailedSarif(
|
||||
}
|
||||
|
||||
async function generateFailedSarif(
|
||||
logger: Logger,
|
||||
features: FeatureEnablement,
|
||||
config: Config,
|
||||
category: string | undefined,
|
||||
@@ -166,7 +163,7 @@ async function generateFailedSarif(
|
||||
sarifFile?: string,
|
||||
) {
|
||||
const databasePath = config.dbLocation;
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
// Set the filename for the SARIF file if not already set.
|
||||
if (sarifFile === undefined) {
|
||||
@@ -316,8 +313,6 @@ export async function tryUploadSarifIfRunFailed(
|
||||
* @param config The CodeQL Action configuration.
|
||||
* @param repositoryNwo The name and owner of the repository.
|
||||
* @param features Information about enabled features.
|
||||
* @param jobStatus The status of the job, as reported by the Actions runtime environment.
|
||||
* @param env The environment to read variables from.
|
||||
* @param logger The logger to use.
|
||||
* @returns The results of uploading the SARIF file for the failure.
|
||||
*/
|
||||
@@ -333,11 +328,9 @@ export async function uploadFailureInfo(
|
||||
config: Config,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
env: ReadOnlyEnv,
|
||||
logger: Logger,
|
||||
): Promise<UploadFailedSarifResult> {
|
||||
await recordOverlayStatus(codeql, config, features, jobStatus, env, logger);
|
||||
await recordOverlayStatus(codeql, config, features, logger);
|
||||
|
||||
const uploadFailedSarifResult = await tryUploadSarifIfRunFailed(
|
||||
config,
|
||||
@@ -416,37 +409,6 @@ export async function uploadFailureInfo(
|
||||
return uploadFailedSarifResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether one of the CodeQL Actions reported an error for this job, which means the analysis
|
||||
* genuinely failed.
|
||||
*
|
||||
* Note that the converse does not hold: an Action that is terminated abruptly, or that fails before
|
||||
* it can gather telemetry, does not get to report anything.
|
||||
*/
|
||||
function didCodeQlReportError(env: ReadOnlyEnv): boolean {
|
||||
const jobStatus = env.getOptional(EnvVar.JOB_STATUS);
|
||||
return (
|
||||
jobStatus === JobStatus.FailureStatus ||
|
||||
jobStatus === JobStatus.ConfigErrorStatus
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the job status tells us anything about whether the analysis itself would have succeeded.
|
||||
*
|
||||
* We check for the statuses we know to be meaningful rather than excluding the ones that are not,
|
||||
* so that a status we do not recognise is treated as inconclusive.
|
||||
*/
|
||||
function isConclusiveJobStatus(jobStatus: string | undefined): boolean {
|
||||
switch (jobStatus?.trim().toLowerCase()) {
|
||||
case "failure":
|
||||
case "success":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If overlay base database creation was attempted but the analysis did not complete
|
||||
* successfully, save the failure status to the Actions cache so that subsequent runs
|
||||
@@ -456,30 +418,16 @@ async function recordOverlayStatus(
|
||||
codeql: CodeQL,
|
||||
config: Config,
|
||||
features: FeatureEnablement,
|
||||
jobStatus: string | undefined,
|
||||
env: ReadOnlyEnv,
|
||||
logger: Logger,
|
||||
) {
|
||||
if (
|
||||
config.overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase ||
|
||||
env.getOptional(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY) === "true" ||
|
||||
process.env[EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY] === "true" ||
|
||||
!(await features.getValue(Feature.OverlayAnalysisStatusSave))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only record a failure when the job outcome tells us something about the analysis. A cancelled
|
||||
// job, or a status we do not recognise, says nothing about whether the analysis would have
|
||||
// succeeded, so recording a failure would disable overlay analysis needlessly. We still record
|
||||
// one if a CodeQL Action reported an error before the job ended.
|
||||
if (!isConclusiveJobStatus(jobStatus) && !didCodeQlReportError(env)) {
|
||||
logger.info(
|
||||
"Not recording an improved incremental analysis failure for this job because the job " +
|
||||
`status (${jobStatus ?? "unset"}) does not tell us whether the analysis itself failed.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkRunIdInput = actionsUtil.getOptionalInput("check-run-id");
|
||||
const checkRunId =
|
||||
checkRunIdInput !== undefined ? parseInt(checkRunIdInput, 10) : undefined;
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as core from "@actions/core";
|
||||
|
||||
import {
|
||||
restoreInputs,
|
||||
getOptionalInput,
|
||||
getTemporaryDirectory,
|
||||
printDebugLogs,
|
||||
} from "./actions-util";
|
||||
@@ -21,7 +20,7 @@ import {
|
||||
DependencyCachingUsageReport,
|
||||
getDependencyCacheUsage,
|
||||
} from "./dependency-caching";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import { initFeatures } from "./feature-flags";
|
||||
import * as gitUtils from "./git-utils";
|
||||
import * as initActionPostHelper from "./init-action-post-helper";
|
||||
@@ -56,11 +55,6 @@ async function run(startedAt: Date) {
|
||||
| undefined;
|
||||
let dependencyCachingUsage: DependencyCachingUsageReport | undefined;
|
||||
try {
|
||||
// Read the job status before restoring inputs, since it is provided by the Actions runtime
|
||||
// environment for this step and would otherwise be overwritten by the value that the `init`
|
||||
// Action saw, which is always a success.
|
||||
const jobStatus = getOptionalInput("job-status");
|
||||
|
||||
// Restore inputs from `init` Action.
|
||||
restoreInputs();
|
||||
|
||||
@@ -81,7 +75,7 @@ async function run(startedAt: Date) {
|
||||
"Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.",
|
||||
);
|
||||
} else {
|
||||
const codeql = await getCodeQL(logger, config.codeQLCmd);
|
||||
const codeql = await getCodeQL(config.codeQLCmd);
|
||||
|
||||
uploadFailedSarifResult = await initActionPostHelper.uploadFailureInfo(
|
||||
debugArtifacts.tryUploadAllAvailableDebugArtifacts,
|
||||
@@ -90,8 +84,6 @@ async function run(startedAt: Date) {
|
||||
config,
|
||||
repositoryNwo,
|
||||
features,
|
||||
jobStatus,
|
||||
getEnv(),
|
||||
logger,
|
||||
);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -174,14 +175,6 @@ async function sendCompletedStatusReport(
|
||||
initToolsDownloadFields.tools_download_duration_ms =
|
||||
toolsDownloadStatusReport.downloadDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.extractionDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_extraction_duration_ms =
|
||||
toolsDownloadStatusReport.extractionDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.totalDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_total_duration_ms =
|
||||
toolsDownloadStatusReport.totalDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
@@ -262,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
|
||||
@@ -697,6 +695,7 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
|
||||
// To check custom query packs for compatibility with overlay analysis, we
|
||||
@@ -725,6 +724,7 @@ async function run(
|
||||
sourceRoot,
|
||||
"Runner.Worker.exe",
|
||||
qlconfigFile,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
logger: Logger,
|
||||
): Promise<void> {
|
||||
fs.mkdirSync(config.dbLocation, { recursive: true });
|
||||
await configUtils.wrapEnvironment(
|
||||
@@ -99,6 +100,7 @@ export async function runDatabaseInitCluster(
|
||||
sourceRoot,
|
||||
processName,
|
||||
qlconfigFile,
|
||||
logger,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function runResolveBuildEnvironment(
|
||||
) {
|
||||
logger.startGroup(`Attempting to resolve build environment for ${language}`);
|
||||
|
||||
const codeql = await getCodeQL(logger, cmd);
|
||||
const codeql = await getCodeQL(cmd);
|
||||
|
||||
if (workingDir !== undefined) {
|
||||
logger.info(`Using ${workingDir} as the working directory.`);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as core from "@actions/core";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { Action, ActionState, runInActions } from "./action-common";
|
||||
import {
|
||||
@@ -85,14 +86,6 @@ async function sendCompletedStatusReport(
|
||||
initToolsDownloadFields.tools_download_duration_ms =
|
||||
toolsDownloadStatusReport.downloadDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.extractionDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_extraction_duration_ms =
|
||||
toolsDownloadStatusReport.extractionDurationMs;
|
||||
}
|
||||
if (toolsDownloadStatusReport?.totalDurationMs !== undefined) {
|
||||
initToolsDownloadFields.tools_total_duration_ms =
|
||||
toolsDownloadStatusReport.totalDurationMs;
|
||||
}
|
||||
if (toolsFeatureFlagsValid !== undefined) {
|
||||
initToolsDownloadFields.tools_feature_flags_valid = toolsFeatureFlagsValid;
|
||||
}
|
||||
@@ -102,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.
|
||||
@@ -147,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",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
import * as github from "@actions/github";
|
||||
@@ -9,8 +7,7 @@ import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import * as diagnostics from "./diagnostics";
|
||||
import { ActionsEnvVars, EnvVar, ReadOnlyEnv } from "./environment";
|
||||
import { EnvVar } from "./environment";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
|
||||
@@ -30,7 +27,6 @@ import {
|
||||
setupActionsVars,
|
||||
setupTests,
|
||||
} from "./testing-utils";
|
||||
import * as toolsDownload from "./tools-download";
|
||||
import {
|
||||
getErrorMessage,
|
||||
GitHubVariant,
|
||||
@@ -124,42 +120,24 @@ test.serial(
|
||||
const LINKED_BUNDLE_TEST_CASES = [
|
||||
{
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
tarSupportsZstd: true,
|
||||
expectedBundleName: "codeql-bundle-linux64.tar.zst",
|
||||
expectedCompressionMethod: "zstd",
|
||||
},
|
||||
{
|
||||
platform: "linux",
|
||||
arch: "arm64",
|
||||
tarSupportsZstd: true,
|
||||
expectedBundleName: "codeql-bundle-linux-arm64.tar.zst",
|
||||
expectedCompressionMethod: "zstd",
|
||||
},
|
||||
{
|
||||
platform: "darwin",
|
||||
arch: "arm64",
|
||||
tarSupportsZstd: true,
|
||||
expectedBundleName: "codeql-bundle-osx64.tar.zst",
|
||||
expectedCompressionMethod: "zstd",
|
||||
},
|
||||
{
|
||||
platform: "darwin",
|
||||
arch: "x64",
|
||||
tarSupportsZstd: true,
|
||||
expectedBundleName: "codeql-bundle-osx64.tar.zst",
|
||||
expectedCompressionMethod: "zstd",
|
||||
},
|
||||
{
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
tarSupportsZstd: true,
|
||||
expectedBundleName: "codeql-bundle-win64.tar.gz",
|
||||
expectedCompressionMethod: "gzip",
|
||||
},
|
||||
{
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
tarSupportsZstd: false,
|
||||
expectedBundleName: "codeql-bundle-linux64.tar.gz",
|
||||
expectedCompressionMethod: "gzip",
|
||||
@@ -168,17 +146,15 @@ const LINKED_BUNDLE_TEST_CASES = [
|
||||
|
||||
for (const {
|
||||
platform,
|
||||
arch,
|
||||
tarSupportsZstd,
|
||||
expectedBundleName,
|
||||
expectedCompressionMethod,
|
||||
} of LINKED_BUNDLE_TEST_CASES) {
|
||||
test.serial(
|
||||
`getCodeQLSource selects ${expectedBundleName} for linked tools on ${platform}/${arch}`,
|
||||
`getCodeQLSource selects ${expectedBundleName} for linked tools`,
|
||||
async (t) => {
|
||||
const features = createFeatures([]);
|
||||
sinon.stub(process, "platform").value(platform);
|
||||
sinon.stub(process, "arch").value(arch);
|
||||
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
@@ -258,7 +234,6 @@ test.serial(
|
||||
codeqlFolder: "codeql",
|
||||
statusReport: {
|
||||
downloadDurationMs: 200,
|
||||
totalDurationMs: 300,
|
||||
},
|
||||
toolsVersion: LINKED_CLI_VERSION.cliVersion,
|
||||
});
|
||||
@@ -311,7 +286,6 @@ test.serial(
|
||||
codeqlFolder: "codeql",
|
||||
statusReport: {
|
||||
downloadDurationMs: 200,
|
||||
totalDurationMs: 300,
|
||||
},
|
||||
toolsVersion: expectedVersion,
|
||||
});
|
||||
@@ -943,616 +917,3 @@ test.serial(
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
/** The CLI version that the toolcache cleanup tests download. */
|
||||
const CLEANUP_CLI_VERSION = "2.21.0";
|
||||
/** The bundle version that the toolcache cleanup tests download. */
|
||||
const CLEANUP_BUNDLE_VERSION = "20240101";
|
||||
/** A version of the CodeQL tools that is already in the toolcache but that we are not going to use. */
|
||||
const CLEANUP_STALE_VERSION = "2.20.0";
|
||||
|
||||
/** Creates a directory in the toolcache that looks like a tool that `tool-cache` has cached. */
|
||||
function createToolcacheEntry(
|
||||
toolcacheRoot: string,
|
||||
tool: string,
|
||||
version: string,
|
||||
): string {
|
||||
const versionDirectory = path.join(toolcacheRoot, tool, version);
|
||||
const archDirectory = path.join(versionDirectory, os.arch());
|
||||
fs.mkdirSync(archDirectory, { recursive: true });
|
||||
fs.writeFileSync(path.join(archDirectory, "contents"), "x".repeat(1024));
|
||||
fs.writeFileSync(`${archDirectory}.complete`, "");
|
||||
return versionDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs out the download and the diagnostic sink, then downloads the CodeQL tools.
|
||||
*
|
||||
* @returns the extraction directory and the toolcache cleanup diagnostic, if emitted.
|
||||
*/
|
||||
async function runDownloadCodeQL(
|
||||
toolcacheRoot: string,
|
||||
features: Feature[],
|
||||
bundleVersion: string | undefined,
|
||||
): Promise<{
|
||||
codeqlFolder: string;
|
||||
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
|
||||
}> {
|
||||
sinon
|
||||
.stub(toolsDownload, "downloadAndExtract")
|
||||
.callsFake(async (_url, _compressionMethod, dest) => {
|
||||
// The real implementation creates the destination directory, which matters here because the
|
||||
// cleanup deletes it first and `writeToolcacheMarkerFile` writes into its parent afterwards.
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
return { totalDurationMs: 1 };
|
||||
});
|
||||
const addDiagnostic = sinon.stub(diagnostics, "addNoLanguageDiagnostic");
|
||||
|
||||
const { codeqlFolder } = await setupCodeql.downloadCodeQL(
|
||||
"https://example.com/codeql-bundle.tar.gz",
|
||||
"gzip",
|
||||
bundleVersion,
|
||||
CLEANUP_CLI_VERSION,
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
undefined, // tarVersion
|
||||
toolcacheRoot, // tempDir
|
||||
createFeatures(features),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
const diagnostic = addDiagnostic
|
||||
.getCalls()
|
||||
.map((call) => call.args[1])
|
||||
.find((d) => d.source?.id === "codeql-action/toolcache-bundle-cleanup");
|
||||
|
||||
return {
|
||||
codeqlFolder,
|
||||
cleanupDiagnostic: diagnostic?.attributes as
|
||||
| toolsDownload.ToolcacheCleanupResult
|
||||
| undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a toolcache containing the version of the CodeQL tools that we are about to download, a
|
||||
* different version of the CodeQL tools, and an unrelated tool, then downloads the CodeQL tools.
|
||||
*/
|
||||
async function testToolcacheCleanup(
|
||||
t: ExecutionContext<unknown>,
|
||||
{
|
||||
features,
|
||||
runnerEnvironment,
|
||||
setUp,
|
||||
}: {
|
||||
features: Feature[];
|
||||
runnerEnvironment: string | undefined;
|
||||
setUp?: () => void;
|
||||
},
|
||||
check: (context: {
|
||||
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
|
||||
destinationDirectory: string;
|
||||
staleDirectory: string;
|
||||
}) => void,
|
||||
) {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
if (runnerEnvironment === undefined) {
|
||||
delete process.env[ActionsEnvVars.RUNNER_ENVIRONMENT];
|
||||
} else {
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnvironment;
|
||||
}
|
||||
setUp?.();
|
||||
|
||||
// The extraction of the bundle would normally create this directory.
|
||||
const destinationDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_CLI_VERSION,
|
||||
);
|
||||
const staleDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
const otherToolDirectory = createToolcacheEntry(tmpDir, "Node", "20.0.0");
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
features,
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
t.true(
|
||||
fs.existsSync(otherToolDirectory),
|
||||
"Should never delete other tools from the toolcache.",
|
||||
);
|
||||
|
||||
check({ cleanupDiagnostic, destinationDirectory, staleDirectory });
|
||||
});
|
||||
}
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not clean up the toolcache when the feature flag is disabled",
|
||||
async (t) => {
|
||||
await testToolcacheCleanup(
|
||||
t,
|
||||
{ features: [], runnerEnvironment: "github-hosted" },
|
||||
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.true(fs.existsSync(destinationDirectory));
|
||||
t.is(cleanupDiagnostic, undefined);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not clean up the toolcache when the runner is not GitHub-hosted",
|
||||
async (t) => {
|
||||
await testToolcacheCleanup(
|
||||
t,
|
||||
{
|
||||
features: [Feature.CleanupToolcacheBundles],
|
||||
runnerEnvironment: "self-hosted",
|
||||
},
|
||||
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.true(fs.existsSync(destinationDirectory));
|
||||
t.is(cleanupDiagnostic, undefined);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not clean up the toolcache when the runner environment is unknown",
|
||||
async (t) => {
|
||||
// A runner that doesn't report its environment must be treated as not GitHub-hosted, since its
|
||||
// toolcache may well outlive the job.
|
||||
await testToolcacheCleanup(
|
||||
t,
|
||||
{
|
||||
features: [Feature.CleanupToolcacheBundles],
|
||||
runnerEnvironment: undefined,
|
||||
},
|
||||
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.true(fs.existsSync(destinationDirectory));
|
||||
t.is(cleanupDiagnostic, undefined);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL deletes other CodeQL bundles from the toolcache when enabled on a GitHub-hosted runner",
|
||||
async (t) => {
|
||||
await testToolcacheCleanup(
|
||||
t,
|
||||
{
|
||||
features: [Feature.CleanupToolcacheBundles],
|
||||
runnerEnvironment: "github-hosted",
|
||||
},
|
||||
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||
t.false(
|
||||
fs.existsSync(staleDirectory),
|
||||
"Should delete the version directory, including the `tool-cache` marker file it contains.",
|
||||
);
|
||||
t.false(
|
||||
fs.existsSync(path.join(destinationDirectory, os.arch(), "contents")),
|
||||
"Should also delete a partial entry for the version we are about to download, rather " +
|
||||
"than extracting over it.",
|
||||
);
|
||||
t.deepEqual(cleanupDiagnostic, {
|
||||
deletedVersions: [CLEANUP_STALE_VERSION, CLEANUP_CLI_VERSION].sort(),
|
||||
failed: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL reports no deleted versions when the toolcache has no CodeQL bundles",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
// A toolcache with other tools in it, but no CodeQL.
|
||||
const otherToolDirectory = createToolcacheEntry(tmpDir, "Node", "20.0.0");
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
t.true(fs.existsSync(otherToolDirectory));
|
||||
t.deepEqual(cleanupDiagnostic, { deletedVersions: [], failed: false });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL continues when deleting a CodeQL bundle from the toolcache fails",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
createToolcacheEntry(tmpDir, "CodeQL", CLEANUP_CLI_VERSION);
|
||||
const staleDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
|
||||
const rmStub = sinon
|
||||
.stub(fs.promises, "rm")
|
||||
.rejects(new Error("EACCES: permission denied"));
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
// Restore before `withTmpDir` cleans up after itself.
|
||||
rmStub.restore();
|
||||
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.deepEqual(
|
||||
cleanupDiagnostic,
|
||||
{ deletedVersions: [], failed: true },
|
||||
"Should not report versions that we failed to delete.",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"deleteToolcacheBundles reports a failure when the toolcache location is unknown",
|
||||
async (t) => {
|
||||
const messages: LoggedMessage[] = [];
|
||||
|
||||
const result = await toolsDownload.deleteToolcacheBundles({
|
||||
env: new ReadOnlyEnv({}),
|
||||
logger: getRecordingLogger(messages),
|
||||
});
|
||||
|
||||
t.deepEqual(
|
||||
result,
|
||||
{ deletedVersions: [], failed: true },
|
||||
"Should report a failure rather than throwing, so the download can continue.",
|
||||
);
|
||||
checkExpectedLogMessages(t, messages, [
|
||||
"Unable to determine toolcache directory: RUNNER_TOOL_CACHE environment variable must be set",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"deleteToolcacheBundles uses the supplied environment rather than process.env",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const ambientRoot = path.join(tmpDir, "ambient");
|
||||
const injectedRoot = path.join(tmpDir, "injected");
|
||||
setupActionsVars(tmpDir, ambientRoot);
|
||||
|
||||
const ambientVersion = createToolcacheEntry(
|
||||
ambientRoot,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
const injectedVersion = createToolcacheEntry(
|
||||
injectedRoot,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
const fileEntry = path.join(injectedRoot, "CodeQL", "not-a-directory");
|
||||
fs.writeFileSync(fileEntry, "keep");
|
||||
|
||||
const result = await toolsDownload.deleteToolcacheBundles({
|
||||
env: new ReadOnlyEnv({
|
||||
[ActionsEnvVars.RUNNER_TOOL_CACHE]: injectedRoot,
|
||||
}),
|
||||
logger: getRunnerLogger(true),
|
||||
});
|
||||
|
||||
t.deepEqual(result, {
|
||||
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||
failed: false,
|
||||
});
|
||||
t.false(fs.existsSync(injectedVersion));
|
||||
t.true(fs.existsSync(ambientVersion));
|
||||
t.is(fs.readFileSync(fileEntry, "utf8"), "keep");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"deleteToolcacheBundles reports a failure when the toolcache directory cannot be read",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const versionDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
const messages: LoggedMessage[] = [];
|
||||
const readdir = sinon
|
||||
.stub(fs.promises, "readdir")
|
||||
.rejects(new Error("permission denied"));
|
||||
|
||||
try {
|
||||
const result = await toolsDownload.deleteToolcacheBundles({
|
||||
env: new ReadOnlyEnv({
|
||||
[ActionsEnvVars.RUNNER_TOOL_CACHE]: tmpDir,
|
||||
}),
|
||||
logger: getRecordingLogger(messages),
|
||||
});
|
||||
|
||||
t.deepEqual(result, { deletedVersions: [], failed: true });
|
||||
t.true(fs.existsSync(versionDirectory));
|
||||
checkExpectedLogMessages(t, messages, [
|
||||
`Failed to clean up the CodeQL toolcache at '${path.join(tmpDir, "CodeQL")}': permission denied`,
|
||||
]);
|
||||
} finally {
|
||||
readdir.restore();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not follow a symlinked CodeQL toolcache directory",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const toolcacheRoot = path.join(tmpDir, "toolcache");
|
||||
setupActionsVars(tmpDir, toolcacheRoot);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
// Somewhere the toolcache cleanup must never reach.
|
||||
const outsideDirectory = path.join(tmpDir, "outside");
|
||||
createToolcacheEntry(outsideDirectory, "CodeQL", CLEANUP_STALE_VERSION);
|
||||
createToolcacheEntry(outsideDirectory, "CodeQL", CLEANUP_CLI_VERSION);
|
||||
|
||||
fs.mkdirSync(toolcacheRoot, { recursive: true });
|
||||
fs.symlinkSync(
|
||||
path.join(outsideDirectory, "CodeQL"),
|
||||
path.join(toolcacheRoot, "CodeQL"),
|
||||
);
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
toolcacheRoot,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
t.true(
|
||||
fs.existsSync(
|
||||
path.join(outsideDirectory, "CodeQL", CLEANUP_STALE_VERSION),
|
||||
),
|
||||
"Should not delete anything through a symlinked CodeQL directory.",
|
||||
);
|
||||
t.deepEqual(cleanupDiagnostic, { deletedVersions: [], failed: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not clean up the toolcache once a step has already set up CodeQL",
|
||||
async (t) => {
|
||||
// `.github/workflows/codeql.yml` sets up CodeQL twice and then runs both returned paths. If the
|
||||
// second setup downloads, it must not delete the bundle the first one handed out.
|
||||
await testToolcacheCleanup(
|
||||
t,
|
||||
{
|
||||
features: [Feature.CleanupToolcacheBundles],
|
||||
runnerEnvironment: "github-hosted",
|
||||
setUp: () => {
|
||||
process.env[EnvVar.HAS_SET_UP_CODEQL] = "true";
|
||||
},
|
||||
},
|
||||
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.true(fs.existsSync(destinationDirectory));
|
||||
t.is(cleanupDiagnostic, undefined);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"setupCodeQLBundle records that this job has set up CodeQL",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
|
||||
|
||||
sinon.stub(setupCodeql, "downloadCodeQL").resolves({
|
||||
codeqlFolder: "codeql",
|
||||
statusReport: { totalDurationMs: 1 },
|
||||
toolsVersion: LINKED_CLI_VERSION.cliVersion,
|
||||
});
|
||||
|
||||
await setupCodeql.setupCodeQLBundle(
|
||||
"linked",
|
||||
SAMPLE_DOTCOM_API_DETAILS,
|
||||
tmpDir,
|
||||
GitHubVariant.DOTCOM,
|
||||
SAMPLE_DEFAULT_CLI_VERSION,
|
||||
undefined, // rawLanguages
|
||||
false, // useOverlayAwareDefaultCliVersion
|
||||
createFeatures([]),
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
t.is(
|
||||
process.env[EnvVar.HAS_SET_UP_CODEQL],
|
||||
"true",
|
||||
"A later step must be able to tell that the toolcache is in use.",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL cleans up the toolcache even when the download will not be cached",
|
||||
async (t) => {
|
||||
// A `tools` URL we can't derive a bundle version from is extracted to a temporary directory
|
||||
// rather than the toolcache, but the toolcache is on the same filesystem, so emptying it still
|
||||
// frees up space for the analysis.
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
const staleDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
|
||||
const { codeqlFolder, cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
undefined, // bundleVersion
|
||||
);
|
||||
|
||||
t.is(path.dirname(codeqlFolder), tmpDir);
|
||||
t.not(codeqlFolder, path.join(tmpDir, "CodeQL"));
|
||||
t.true(fs.existsSync(codeqlFolder));
|
||||
t.false(fs.existsSync(`${codeqlFolder}.complete`));
|
||||
t.false(fs.existsSync(staleDirectory));
|
||||
t.deepEqual(cleanupDiagnostic, {
|
||||
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||
failed: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL reports a failure when the toolcache cannot be inspected",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
createToolcacheEntry(tmpDir, "CodeQL", CLEANUP_STALE_VERSION);
|
||||
|
||||
const lstatStub = sinon.stub(fs.promises, "lstat").rejects(
|
||||
Object.assign(new Error("permission denied"), {
|
||||
code: "EACCES",
|
||||
}),
|
||||
);
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
lstatStub.restore();
|
||||
|
||||
t.deepEqual(
|
||||
cleanupDiagnostic,
|
||||
{ deletedVersions: [], failed: true },
|
||||
"An error other than the toolcache being absent must not be reported as success.",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not clean up a toolcache on a different filesystem to the workspace",
|
||||
async (t) => {
|
||||
// Some runner images keep the toolcache on a different volume to the workspace, in which case
|
||||
// deleting the tools frees up disk space that the analysis cannot use.
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
const staleDirectory = createToolcacheEntry(
|
||||
tmpDir,
|
||||
"CodeQL",
|
||||
CLEANUP_STALE_VERSION,
|
||||
);
|
||||
|
||||
sinon
|
||||
.stub(toolsDownload, "isToolcacheOnWorkspaceFilesystem")
|
||||
.returns(false);
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
tmpDir,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
t.true(fs.existsSync(staleDirectory));
|
||||
t.is(cleanupDiagnostic, undefined);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"isToolcacheOnWorkspaceFilesystem assumes a different filesystem when it cannot tell",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
setupActionsVars(tmpDir, tmpDir);
|
||||
t.true(toolsDownload.isToolcacheOnWorkspaceFilesystem(logger));
|
||||
|
||||
// If we can't tell, we assume the toolcache is not somewhere we can reclaim space from.
|
||||
process.env[ActionsEnvVars.RUNNER_TOOL_CACHE] = path.join(
|
||||
tmpDir,
|
||||
"does-not-exist",
|
||||
);
|
||||
t.false(toolsDownload.isToolcacheOnWorkspaceFilesystem(logger));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.serial(
|
||||
"downloadCodeQL does not delete through a symlinked version directory",
|
||||
async (t) => {
|
||||
await withTmpDir(async (tmpDir) => {
|
||||
const toolcacheRoot = path.join(tmpDir, "toolcache");
|
||||
setupActionsVars(tmpDir, toolcacheRoot);
|
||||
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
|
||||
|
||||
createToolcacheEntry(toolcacheRoot, "CodeQL", CLEANUP_STALE_VERSION);
|
||||
|
||||
// Somewhere outside the toolcache that a version directory points at.
|
||||
const outsideDirectory = path.join(tmpDir, "outside");
|
||||
fs.mkdirSync(outsideDirectory, { recursive: true });
|
||||
fs.writeFileSync(path.join(outsideDirectory, "contents"), "x");
|
||||
fs.symlinkSync(
|
||||
outsideDirectory,
|
||||
path.join(toolcacheRoot, "CodeQL", "9.9.9"),
|
||||
);
|
||||
|
||||
const { cleanupDiagnostic } = await runDownloadCodeQL(
|
||||
toolcacheRoot,
|
||||
[Feature.CleanupToolcacheBundles],
|
||||
CLEANUP_BUNDLE_VERSION,
|
||||
);
|
||||
|
||||
t.true(
|
||||
fs.existsSync(path.join(outsideDirectory, "contents")),
|
||||
"Should not delete anything through a symlinked version directory.",
|
||||
);
|
||||
t.true(
|
||||
fs
|
||||
.lstatSync(path.join(toolcacheRoot, "CodeQL", "9.9.9"))
|
||||
.isSymbolicLink(),
|
||||
);
|
||||
t.deepEqual(cleanupDiagnostic, {
|
||||
deletedVersions: [CLEANUP_STALE_VERSION],
|
||||
failed: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2,17 +2,14 @@ import * as fs from "fs";
|
||||
import { OutgoingHttpHeaders } from "http";
|
||||
import * as path from "path";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import * as toolcache from "@actions/tool-cache";
|
||||
import { default as deepEqual } from "fast-deep-equal";
|
||||
import * as semver from "semver";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
|
||||
import { ActionState } from "./action-common";
|
||||
import {
|
||||
isAnalyzingPullRequest,
|
||||
isDynamicWorkflow,
|
||||
isGitHubHostedRunner,
|
||||
isRunningLocalAction,
|
||||
} from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
@@ -22,7 +19,6 @@ import {
|
||||
makeDiagnostic,
|
||||
makeTelemetryDiagnostic,
|
||||
} from "./diagnostics";
|
||||
import { EnvVar, getEnv } from "./environment";
|
||||
import {
|
||||
CODEQL_VERSION_ZSTD_BUNDLE,
|
||||
CodeQLDefaultVersionInfo,
|
||||
@@ -34,10 +30,8 @@ import { Logger } from "./logging";
|
||||
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
||||
import * as tar from "./tar";
|
||||
import {
|
||||
deleteToolcacheBundles,
|
||||
downloadAndExtract,
|
||||
getToolcacheDirectory,
|
||||
isToolcacheOnWorkspaceFilesystem,
|
||||
ToolsDownloadStatusReport,
|
||||
writeToolcacheMarkerFile,
|
||||
} from "./tools-download";
|
||||
@@ -81,7 +75,7 @@ export function getCodeQLBundleName(
|
||||
if (process.platform === "win32") {
|
||||
platform = "win64";
|
||||
} else if (process.platform === "linux") {
|
||||
platform = process.arch === "arm64" ? "linux-arm64" : "linux64";
|
||||
platform = "linux64";
|
||||
} else if (process.platform === "darwin") {
|
||||
platform = "osx64";
|
||||
} else {
|
||||
@@ -790,7 +784,6 @@ export const downloadCodeQL = async function (
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
tarVersion: tar.TarVersion | undefined,
|
||||
tempDir: string,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
): Promise<{
|
||||
codeqlFolder: string;
|
||||
@@ -824,8 +817,6 @@ export const downloadCodeQL = async function (
|
||||
const extractedBundlePath =
|
||||
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
|
||||
|
||||
await tryDeleteToolcacheBundles({ env: getEnv(), features, logger });
|
||||
|
||||
const statusReport = await downloadAndExtract(
|
||||
codeqlURL,
|
||||
compressionMethod,
|
||||
@@ -878,48 +869,6 @@ function getToolcacheDestinationInfo(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reclaims disk space by deleting the CodeQL tools from the toolcache, if enabled.
|
||||
*
|
||||
* On GitHub-hosted runners the toolcache shares a filesystem with the workspace, so tools left in
|
||||
* the toolcache take up space that the analysis could use instead. This holds wherever we extract
|
||||
* the tools we are obtaining, since the toolcache is on that filesystem either way.
|
||||
*/
|
||||
async function tryDeleteToolcacheBundles({
|
||||
env,
|
||||
features,
|
||||
logger,
|
||||
}: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>): Promise<void> {
|
||||
// A step that has already set up CodeQL may hand out a path into the toolcache that a later step
|
||||
// runs, so only the first step to set it up can know that nothing else relies on the toolcache.
|
||||
if (env.getOptional(EnvVar.HAS_SET_UP_CODEQL) !== undefined) {
|
||||
logger.debug(
|
||||
"Not deleting the CodeQL tools from the toolcache since a previous step in this job has " +
|
||||
"already set up CodeQL.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!isGitHubHostedRunner() ||
|
||||
!isToolcacheOnWorkspaceFilesystem(logger) ||
|
||||
!(await features.getValue(Feature.CleanupToolcacheBundles))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteToolcacheBundles({ env, logger });
|
||||
|
||||
addNoLanguageDiagnostic(
|
||||
undefined,
|
||||
makeTelemetryDiagnostic(
|
||||
"codeql-action/toolcache-bundle-cleanup",
|
||||
"Toolcache CodeQL bundle cleanup",
|
||||
{ ...result },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodeQLURLVersion(url: string): string {
|
||||
const match = url.match(/\/codeql-bundle-(.*)\//);
|
||||
if (match === null || match.length < 2) {
|
||||
@@ -1029,7 +978,6 @@ export async function setupCodeQLBundle(
|
||||
apiDetails,
|
||||
zstdAvailability.version,
|
||||
tempDir,
|
||||
features,
|
||||
logger,
|
||||
);
|
||||
toolsVersion = result.toolsVersion;
|
||||
@@ -1041,11 +989,6 @@ export async function setupCodeQLBundle(
|
||||
default:
|
||||
util.assertNever(source);
|
||||
}
|
||||
|
||||
// Record that this job now has a copy of the CodeQL tools, so that a later step doesn't delete
|
||||
// the toolcache out from under the path we are about to return.
|
||||
core.exportVariable(EnvVar.HAS_SET_UP_CODEQL, "true");
|
||||
|
||||
return {
|
||||
codeqlFolder,
|
||||
toolsDownloadStatusReport,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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,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,
|
||||
@@ -14,26 +12,21 @@ import {
|
||||
isSelfHostedRunner,
|
||||
} from "./actions-util";
|
||||
import { getAnalysisKey, getApiClient } from "./api-client";
|
||||
import {
|
||||
getCachedCodeQlVersion,
|
||||
getCommandCacheFilePath,
|
||||
} from "./cli/output-cache";
|
||||
import type { Config } from "./config/action-config";
|
||||
import type { ComputedInput, InputName } from "./config/inputs";
|
||||
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
|
||||
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,
|
||||
getCachedCodeQlVersion,
|
||||
isInTestMode,
|
||||
GITHUB_DOTCOM_URL,
|
||||
DiskUsage,
|
||||
@@ -66,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.
|
||||
*
|
||||
@@ -190,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. */
|
||||
@@ -299,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.
|
||||
*
|
||||
@@ -379,10 +298,7 @@ export async function createStatusReportBase(
|
||||
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
|
||||
}
|
||||
const runnerOs = getRequiredEnvParam("RUNNER_OS");
|
||||
const codeQlCliVersion = getCachedCodeQlVersion(
|
||||
logger,
|
||||
getCommandCacheFilePath(getEnv()),
|
||||
);
|
||||
const codeQlCliVersion = getCachedCodeQlVersion();
|
||||
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
|
||||
const testingEnvironment = getTestingEnvironment();
|
||||
// re-export the testing environment variable so that it is available to subsequent steps,
|
||||
@@ -408,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,
|
||||
@@ -626,21 +541,8 @@ export interface InitWithConfigStatusReport extends InitStatusReport {
|
||||
|
||||
/** Fields of the init status report populated when the tools source is `download`. */
|
||||
export interface InitToolsDownloadFields {
|
||||
/**
|
||||
* Time taken to download the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
/** Time taken to download the bundle, in milliseconds. */
|
||||
tools_download_duration_ms?: number;
|
||||
/**
|
||||
* Time taken to extract the bundle, in milliseconds. Not populated when the bundle is downloaded
|
||||
* and extracted concurrently.
|
||||
*/
|
||||
tools_extraction_duration_ms?: number;
|
||||
/**
|
||||
* Total time taken to make the bundle available on disk, in milliseconds. This includes any time
|
||||
* spent on a streaming attempt that failed and fell back to downloading before extracting.
|
||||
*/
|
||||
tools_total_duration_ms?: number;
|
||||
/**
|
||||
* Whether the relevant tools dotcom feature flags have been misconfigured.
|
||||
* Only populated if we attempt to determine the default version based on the dotcom feature flags. */
|
||||
|
||||
@@ -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/,
|
||||
});
|
||||
});
|
||||
});
|
||||
13
src/tar.ts
13
src/tar.ts
@@ -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}`),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user