Compare commits

..

6 Commits

Author SHA1 Message Date
Michael B. Gale
0560247397 Use getComputedInput for getConfigFileInput 2026-07-28 12:08:27 +01:00
Michael B. Gale
e6801c5a55 Add nonEmptyStringProperty 2026-07-28 12:08:26 +01:00
Michael B. Gale
479e61c602 Make FF explicit in ComputedInputOptions 2026-07-24 17:18:06 +01:00
Michael B. Gale
c5d7d6b3ac Add allowForcedRepositoryPropertyValue option 2026-07-24 17:05:43 +01:00
Michael B. Gale
6e76342b54 Generalise getToolsInput into getComputedInput 2026-07-24 17:02:08 +01:00
Michael B. Gale
c54d34d000 Return ComputedInput from getConfigFileInput and include in telemetry 2026-07-24 16:42:40 +01:00
167 changed files with 16295 additions and 48812 deletions

View File

@@ -92,7 +92,8 @@ runs:
Please do the following:
- [ ] Approve running the full set of PR checks.
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is selected rather than "Squash and merge" or "Rebase and merge".
- [ ] Approve and merge the PR. When merging the PR, make sure "Create a merge commit" is
selected rather than "Squash and merge" or "Rebase and merge".
EOF
)

View File

@@ -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"

View File

@@ -1,33 +1,14 @@
# CodeQL Action - Copilot Instructions
The CodeQL Action is used in GitHub Actions workflows to run CodeQL scans using the CodeQL CLI.
## Overview
- The repository contains two TypeScript projects.
- The main TypeScript codebase is in the `src` directory, with accompanying unit tests in `.test.ts` files in the same directory.
- The main codebase is compiled to bundled JavaScript code, which is also contained in the repository in the `lib` directory.
- A secondary TypeScript codebase with scripts that are only used for development purposes or by CI is in the `pr-checks` directory. This codebase is not compiled to bundled JavaScript. It is executed directly with `tsx`, which handles compilation internally.
## Review instructions
- When wording review comments, be helpful and friendly. Assume that the PR author has written the code with the best of intentions. Word your comments constructively as suggestions for improvements. Do not word suggestions as commands.
- If you want to comment on a change that you believe will fail a CI check, do not present the CI failure you expect as a fact. Instead, write that you think a change "may" lead to a failure in CI. Suggest that, if such a failure manifests, the changes you are commenting on may be the place responsible for the failure and are worth looking at.
- If a suggestion you make is suitable for a follow-up, such as a refactoring that doesn't change the behaviour or fixing a typo in a comment, mention that it can be addressed in a later PR rather than blocking this one.
- If a change is a net improvement, for example because it improves on an existing limitation of existing code, do not complain about pre-existing problems that remain. You may comment on them, but you should make it clear that the thing you are commenting on is not new by writing e.g. "Not new in this PR, but [..]" followed by your description of the issue and a suggestion that it could be improved at the same time with e.g. "Consider whether this is worth addressing as part of this PR as well."
## Generated code
The main codebase of the CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from. The secondary TypeScript codebase has sources in the `pr-checks` directory, which are executed directly with `tsx` and not compiled to JavaScript in the `lib` folder.
The CodeQL Action is written in TypeScript and compiled to JavaScript. Both the TypeScript sources and the **generated** JavaScript code are contained in this repository. The TypeScript sources are contained in the `src` directory and the JavaScript code is contained in the `lib` directory. A GitHub Actions workflow checks that the JavaScript code in `lib` is up-to-date. Therefore, you should not review any changes to the contents of the `lib` folder and it is expected that the JavaScript code in `lib` closely mirrors the TypeScript code it is generated from.
GitHub Actions workflows in the `.github/workflows` directory whose filenames start with two underscores (e.g. `__all-platform-bundle.yml`) are automatically generated using the `pr-checks/sync.sh` script from template files in the `pr-checks/checks` directory. Therefore, you do not need to review files in the `.github/workflows` directory that start with two underscores. However, you should review changes to the `pr-checks` directory as well as workflows in the `.github/workflows` directory that do not start with underscores.
## Release process
Changes that are merged to the `main` branch are not used in production straight away. Instead, changes from `main` must first be released using the release process, which has to be manually triggered by a staff member. Therefore, it is possible for further changes to be made to the code after some changes have been merged to `main` but before they are released. Once the release process automation is started, a series of PRs are automatically generated:
- A release-PR is a PR which matches all of the following patterns:
- PR title starts with the exact prefix: `Merge main into releases/v`
- Equivalent regex: `^Merge main into releases\/v`
- AND head (source) branch matches the update-branch pattern:
@@ -36,5 +17,5 @@ Changes that are merged to the `main` branch are not used in production straight
- Regex: `^releases\/v` (e.g. `releases/v3`)
- Consider all files in release-PRs to have already been reviewed and do not review them, except for the following two files:
- `CHANGELOG.md`
- `package.json`
- `CHANGELOG.md`
- `package.json`

View File

@@ -8,6 +8,8 @@ updates:
interval: weekly
cooldown:
default-days: 7
exclude:
- "@actions/*"
labels:
- Rebuild
# Ignore incompatible dependency updates
@@ -31,6 +33,8 @@ updates:
interval: weekly
cooldown:
default-days: 7
exclude:
- "actions/*"
labels:
- Rebuild
groups:

View File

@@ -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`.

View File

@@ -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

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin

View File

@@ -63,7 +63,7 @@ jobs:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Java
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with:
java-version: ${{ inputs.java-version || '17' }}
distribution: temurin

View File

@@ -80,8 +80,7 @@ jobs:
- id: init
uses: ./../action/init
with:
# Request multiple languages so this check uses the combined bundle.
languages: javascript,python
languages: javascript
tools: ${{ steps.prepare-test.outputs.tools-url }}
- uses: ./../action/analyze
with:

View File

@@ -51,8 +51,6 @@ jobs:
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test

View File

@@ -124,5 +124,4 @@ jobs:
env:
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
CODEQL_ACTION_TEST_MODE: true

View File

@@ -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
View File

@@ -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

View File

@@ -116,14 +116,13 @@ jobs:
version: ${{ matrix.version }}
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Install Python 3.13.15 for older CLI versions
# Older CLI versions don't work with Python 3.13.16 or newer because their Python extractor
# imports `importlib._bootstrap._ERR_MSG`, which those Python versions no longer define.
- name: Install Python 3.13 for older CLI versions
# We need Python 3.13 for older CLI versions because they are not compatible with Python 3.14 or newer.
# See https://github.com/github/codeql-action/pull/3212
if: matrix.version != 'nightly-latest' && matrix.version != 'linked'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13.15'
python-version: '3.13'
- name: Use Xcode 16
# Only the older CodeQL CLI versions need Xcode 16, and these run on macOS 15.
@@ -192,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

View File

@@ -84,8 +84,6 @@ jobs:
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test

View File

@@ -84,8 +84,6 @@ jobs:
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test

View File

@@ -84,8 +84,6 @@ jobs:
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test

View File

@@ -84,8 +84,6 @@ jobs:
with:
node-version: 20.x
cache: npm
- name: Install newer npm
run: npm install -g npm@11.19.1
- name: Install dependencies
run: npm ci
- name: Prepare test

View File

@@ -1,164 +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 - Per-language bundles
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: {}
workflow_call:
inputs: {}
defaults:
run:
shell: bash
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: per-language-bundle-validation-${{github.ref}}
jobs:
per-language-bundle-validation:
strategy:
fail-fast: false
matrix:
include:
- language: actions
os: ubuntu-latest
version: nightly-latest
expected-extractors: actions javascript
- language: cpp
os: ubuntu-latest
version: nightly-latest
build-mode: manual
build-command: gcc -o main main.c
- language: csharp
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: go
os: ubuntu-latest
version: nightly-latest
build-mode: autobuild
- language: java
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: javascript
os: ubuntu-latest
version: nightly-latest
- language: python
os: ubuntu-latest
version: nightly-latest
- language: ruby
os: ubuntu-latest
version: nightly-latest
- language: rust
os: ubuntu-latest
version: nightly-latest
- language: swift
os: macos-latest-xlarge
version: nightly-latest
build-mode: autobuild
name: Per-language bundles
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: 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
id: init
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix['build-mode'] }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains only the expected extractors
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
LANGUAGE: ${{ matrix.language }}
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
run: |
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
echo "Extractors in the bundle:"
echo "$extractors"
echo "Expected: $EXPECTED_EXTRACTORS"
for expected in $EXPECTED_EXTRACTORS; do
if ! echo "$extractors" | grep -qx "$expected"; then
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
exit 1
fi
done
# If the bundle contained extractors beyond those the language needs, then it would not
# have been trimmed, and this job would be silently validating the combined bundle.
for other in actions cpp csharp go java javascript python ruby rust swift; do
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
continue
fi
if echo "$extractors" | grep -qx "$other"; then
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
exit 1
fi
done
- name: Check that the bundle was not added to the toolcache
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# A bundle that is missing most of its extractors must never be left in the toolcache,
# where a later job analyzing a different language could pick it up. The runner image
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
# rather than whether the toolcache contains CodeQL at all.
echo "CodeQL is at $CODEQL_PATH"
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
exit 1
fi
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
exit 1
fi
- name: Build code
if: matrix['build-command']
run: ${{ matrix['build-command'] }}
- uses: ./../action/analyze
id: analysis
with:
upload-database: false
- name: Check that a database was created for the language
env:
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
LANGUAGE: ${{ matrix.language }}
run: |
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
if [ -z "$database" ] || [ ! -d "$database" ]; then
echo "::error::No CodeQL database was created for ${LANGUAGE}."
echo "Databases: $DB_LOCATIONS"
exit 1
fi
echo "Created a ${LANGUAGE} database at ${database}."
env:
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
CODEQL_ACTION_TEST_MODE: true

View File

@@ -54,7 +54,7 @@ jobs:
use-all-platform-bundle: 'false'
setup-kotlin: 'true'
- name: Set up Ruby
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -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]'

View File

@@ -113,6 +113,7 @@ jobs:
matrix:
include:
- language: actions
- language: python
permissions:
contents: read

View File

@@ -75,8 +75,7 @@ jobs:
uses: ./../action/.github/actions/check-codescanning-config
with:
expected-config-file-contents: "{}"
# Request multiple languages so later checks can reuse the combined bundle.
languages: javascript,python
languages: javascript
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Packs from input

View File

@@ -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

View File

@@ -45,13 +45,7 @@ jobs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
# Install a new enough version of `npm` to understand `min-release-age`
# that is still compatible with Node 20.
- name: Install newer npm
if: matrix.node-version == 20
run: npm install -g npm@11.19.1
cache: 'npm'
- name: Install dependencies
run: |
@@ -73,12 +67,7 @@ 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
@@ -101,19 +90,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check for incorrect addresses in package-lock.json
run: |
if git grep -nE '(pkgs\.visualstudio\.com|pkgs\.dev\.azure\.com|packagefeedproxy\.microsoft\.io)' -- \
'package-lock.json'; then
echo "::error::package-lock.json contains internal package feed URLs. Replace them with public registry URLs."
exit 1
fi
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: "npm"
cache: 'npm'
- name: Install dependencies
id: install-deps
@@ -128,10 +109,6 @@ jobs:
working-directory: pr-checks
run: npx tsx --test
- name: Run `pr-checks/changenotes.ts` to ensure that all unreleased change notes are valid
if: ${{ !cancelled() && steps.install-deps.outcome == 'success' }}
run: npx tsx pr-checks/changenotes.ts validate
- name: Verify all Actions use the same Node version
id: head-version
run: |
@@ -181,14 +158,14 @@ jobs:
path: ${{ runner.temp }}/repo-size/
if-no-files-found: error
- name: "Backport: Check out base ref"
- name: 'Backport: Check out base ref'
id: checkout-base
if: ${{ startsWith(github.head_ref, 'backport-') }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.base_ref }}
- name: "Backport: Verify Node versions unchanged"
- name: 'Backport: Verify Node versions unchanged'
if: steps.checkout-base.outcome == 'success'
env:
HEAD_VERSION: ${{ steps.head-version.outputs.node_version }}

View File

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

View File

@@ -54,27 +54,32 @@ jobs:
run: |
git fetch origin "$BASE_BRANCH"
# Allow merge conflicts in `lib`, since rebuilding should resolve them. Conflicts leave the
# merge in progress, so check for `MERGE_HEAD` to tell them apart from failures that don't.
if git merge "origin/$BASE_BRANCH"; then
# Allow merge conflicts in `lib`, since rebuilding should resolve them.
git merge "origin/$BASE_BRANCH"
MERGE_RESULT=$?
if [ "$MERGE_RESULT" -eq 0 ]; then
echo "Merge succeeded cleanly."
elif git rev-parse --verify MERGE_HEAD >/dev/null 2>&1; then
echo "Merge conflicts detected, continuing."
elif [ "$MERGE_RESULT" -eq 1 ]; then
echo "Merge conflicts detected (exit code $MERGE_RESULT), continuing."
else
echo "git merge failed with unexpected exit code $MERGE_RESULT."
exit 1
fi
if [ "$MERGE_RESULT" -ne 0 ]; then
echo "merge-in-progress=true" >> $GITHUB_OUTPUT
# Check for merge conflicts outside of `lib`.
CONFLICTS_OUTSIDE_LIB=$(git diff --name-only --diff-filter=U | grep --invert-match '^lib/' || true)
if [ -n "$CONFLICTS_OUTSIDE_LIB" ]; then
# Check for merge conflicts outside of `lib`. Disable git diff's trailing whitespace check
# since `node_modules/@types/semver/README.md` fails it.
if git -c core.whitespace=-trailing-space diff --check | grep --invert-match '^lib/'; then
echo "Merge conflicts were detected outside of the lib directory. Please resolve them manually."
echo "$CONFLICTS_OUTSIDE_LIB"
git -c core.whitespace=-trailing-space diff --check | grep --invert-match '^lib/' || true
exit 1
fi
echo "No merge conflicts found outside the lib directory. We should be able to resolve all of" \
"these by rebuilding the Action."
else
echo "git merge failed for a reason other than merge conflicts."
exit 1
fi
- name: Compile TypeScript

View File

@@ -10,7 +10,8 @@ on:
required: true
# Only for dry-runs of changes to the workflow.
push:
# Don't run dry-run on release branches, since that's unnecessary.
# Don't run dry-run on release branches, to avoid an issue where the
# "new" tag determined by the "Prepare release" job already exists.
branches-ignore:
- releases/v*
paths:
@@ -23,7 +24,7 @@ defaults:
jobs:
prepare:
name: "Prepare"
name: "Prepare release"
if: github.repository == 'github/codeql-action'
permissions:
@@ -92,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
@@ -106,10 +107,8 @@ jobs:
# We usually expect to checkout `inputs.rollback-tag` (required for `workflow_dispatch`),
# but use `v0.0.0` for testing.
ROLLBACK_TAG: ${{ inputs.rollback-tag || 'v0.0.0' }}
# Use `needs.prepare.outputs.version` for actual runs and `v0.0.1` for testing.
RELEASE_TAG: ${{ case(github.event_name == 'workflow_dispatch', needs.prepare.outputs.version, 'v0.0.1') }}
# Use `needs.prepare.outputs.major_version` for actual runs and `v0` for testing.
MAJOR_VERSION_TAG: ${{ case(github.event_name == 'workflow_dispatch', needs.prepare.outputs.major_version, 'v0') }}
RELEASE_TAG: ${{ needs.prepare.outputs.version }}
MAJOR_VERSION_TAG: ${{ needs.prepare.outputs.major_version }}
run: |
git checkout "refs/tags/${ROLLBACK_TAG}"
git tag --annotate "${RELEASE_TAG}" --message "${RELEASE_TAG}"
@@ -129,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
@@ -185,3 +182,4 @@ jobs:
# Setting this to `true` for non-workflow_dispatch events will
# still push the `branch`, but won't create a corresponding PR
dry-run: "${{ github.event_name != 'workflow_dispatch' }}"

23
.github/workflows/script/bundle_changelog.py vendored Executable file
View 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
View 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))

View 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)

View File

@@ -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: |

View File

@@ -16,15 +16,15 @@ defaults:
shell: bash
jobs:
prepare:
name: "Prepare"
name: "Prepare release"
permissions:
contents: read
uses: ./.github/workflows/prepare-release.yml
update:
name: "Update release branch"
timeout-minutes: 45
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
@@ -77,7 +77,6 @@ jobs:
--conductor ${GITHUB_ACTOR}
backport:
name: "Create backport"
timeout-minutes: 45
runs-on: ubuntu-latest
environment: Automation

View File

@@ -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

1
.npmrc
View File

@@ -1,2 +1 @@
lockfile-version=3
min-release-age=7

View File

@@ -7,10 +7,6 @@
// transpiled JavaScript
"build": true,
"lib": true,
// exclude "tests" by default because it causes VSCode to start language-specific extensions
// that are not typically needed during development (or indeed may not work correctly)
"tests": true
},
"search.exclude": {
"**/node_modules": true,
@@ -22,7 +18,7 @@
"git.ignoreLimitWarning": true,
// Use the vendored TypeScript version to have a consistent development experience across
// machines.
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},

View File

@@ -4,52 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th
## [UNRELEASED]
No user facing changes.
## 4.38.3 - 08 Oct 2026
- _Upcoming breaking change_: CodeQL version 2.21.2 and earlier were discontinued on 24 September 2026 alongside GitHub Enterprise Server 3.17, and will be unsupported by the next minor release of the CodeQL Action. Added a deprecation warning for customers using these versions of CodeQL. [#4188](https://github.com/github/codeql-action/pull/4188)
- Update default CodeQL bundle version to [2.27.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.2). [#4203](https://github.com/github/codeql-action/pull/4203)
- Fixed a bug where the decision of whether to use a per-language bundle did not account for custom configurations that reference queries outside of compiled CodeQL packs. This issue was caught during internal testing and did not affect any customer repositories. We will resume the roll out of per-language bundles in the coming weeks. [#4184](https://github.com/github/codeql-action/pull/4184)
## 4.38.2 - 24 Sept 2026
- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)
## 4.38.1 - 18 Sept 2026
- The CodeQL Action now has experimental support for CodeQL releases for which per-language bundles are available. Per-language bundles support analysis for a single language and are therefore smaller than the combined bundles that allow analysis for all supported languages. As a result, per-language bundles take up less space on disk and are faster to download. We expect to roll this change out to everyone in the coming weeks. [#4146](https://github.com/github/codeql-action/pull/4146)
## 4.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)
## 4.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)
## 4.37.8 - 21 Aug 2026
No user facing changes.
## 4.37.7 - 13 Aug 2026
- Update default CodeQL bundle version to [2.26.3](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.3). [#4085](https://github.com/github/codeql-action/pull/4085)
## 4.37.6 - 04 Aug 2026
- Changed the default filepath for the new remote file address format that was introduced in CodeQL Action 4.37.0 / 3.37.0 to `.github/codeql-config.yml` to align it with the suggested path that is used elsewhere. [#4070](https://github.com/github/codeql-action/pull/4070)
## 4.37.5 - 03 Aug 2026
- Fixed a bug where a network error while streaming the download of the CodeQL bundle could terminate the `init` Action instead of falling back to downloading the bundle before extracting it. [#4061](https://github.com/github/codeql-action/pull/4061)
## 4.37.4 - 29 Jul 2026
- This version of the CodeQL Action adds support for the `tools` input for the `codeql-action/init` step to be specified using a `github-codeql-tools` [repository property](https://docs.github.com/en/organizations/managing-organization-settings/managing-custom-properties-for-repositories-in-your-organization). This feature will gradually be rolled out following the release of this version. Once rolled out, this allows for the CodeQL CLI version that is used in GitHub-managed workflows, such as Default Setup, to be set to a custom value. For example, customers who run into issues with rate limits when a new CodeQL CLI version is released can set the value to `toolcache` to always use the CodeQL CLI version that is available in the runner toolcache. For Advanced Setup workflows, the value provided for `tools` in the workflow definition always takes precedence unless the value of the repository property starts with `!`. [#4037](https://github.com/github/codeql-action/pull/4037)
- Update default CodeQL bundle version to [2.26.2](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.26.2). [#4051](https://github.com/github/codeql-action/pull/4051)
## 4.37.3 - 22 Jul 2026

View File

@@ -53,7 +53,6 @@ Here are a few things you can do that will increase the likelihood of your pull
- Write tests.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
- For user-facing changes, add a change-note file. See [unreleased-change-notes/README.md](unreleased-change-notes/README.md).
## Releasing (write access required)
@@ -61,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.

View File

@@ -72,11 +72,12 @@ We typically release new minor versions of the CodeQL Action and Bundle when a n
| Minimum CodeQL Action | Minimum CodeQL Bundle Version | GitHub Environment | Notes |
|-----------------------|-------------------------------|--------------------|-------|
| `v4.36.2` | `2.25.6` | Enterprise Server 3.22 | |
| `v4.33.0` | `2.24.3` | Enterprise Server 3.21 | |
| `v4.31.10` | `2.23.9` | Enterprise Server 3.20 | |
| `v3.29.11` | `2.22.4` | Enterprise Server 3.19 | |
| `v3.28.21` | `2.21.3` | Enterprise Server 3.18 | |
| `v3.28.12` | `2.20.7` | Enterprise Server 3.17 | |
| `v3.28.6` | `2.20.3` | Enterprise Server 3.16 | |
See the full list of GHES release and deprecation dates at [GitHub Enterprise Server releases](https://docs.github.com/en/enterprise-server/admin/all-releases#releases-of-github-enterprise-server).

View File

@@ -1,4 +1,3 @@
import pkg from "./package.json" with { type: "json" };
globalThis.__CODEQL_ACTION_VERSION__ = pkg.version;
globalThis.__CODEQL_ACTION_TEST_ENV__ = "unit-test";

View File

@@ -78,7 +78,7 @@ const UPLOAD_LIB_SRC = "./src/upload-lib";
*
* The virtual module additionally re-exports `upload-lib` under the `uploadLib` namespace so that
* external consumers can access it via the small `lib/upload-lib.js` stub emitted below.
*
*
* A tiny stub file is emitted for each Action entrypoint, and one for `upload-lib`. Each stub
* imports the shared bundle and calls/re-exports from the respective entry point.
*
@@ -212,7 +212,6 @@ const context = await esbuild.context({
target: ["node20"],
define: {
__CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
__CODEQL_ACTION_TEST_ENV__: JSON.stringify(""),
},
metafile: true,
});

View File

@@ -140,17 +140,6 @@ export default [
"no-async-foreach/no-async-foreach": "error",
"no-sequences": "error",
"no-shadow": "off",
// A basic check that we don't use `exportVariable` from `@actions/core`.
"no-restricted-syntax": [
"error",
{
selector: "MemberExpression[property.name='exportVariable']",
message:
"Use the `export` method of an `Env` instance or `exportEnvVar` from `environment.ts` instead.",
},
],
// This is overly restrictive with unsetting `EnvVar`s
"@typescript-eslint/no-dynamic-delete": "off",
"@typescript-eslint/no-shadow": "error",
@@ -168,15 +157,6 @@ export default [
],
},
},
{
files: ["src/environment.ts"],
// We allow `exportVariable` from `@actions/core` to be used in this file
// since it defines the wrapper around it that other modules use.
rules: {
"no-restricted-syntax": "off",
},
},
{
files: ["**/*.ts", "**/*.js"],

View File

@@ -164,13 +164,6 @@ 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

View File

@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.27.2",
"cliVersion": "2.27.2",
"priorBundleVersion": "codeql-bundle-v2.27.1",
"priorCliVersion": "2.27.1"
"bundleVersion": "codeql-bundle-v2.26.1",
"cliVersion": "2.26.1",
"priorBundleVersion": "codeql-bundle-v2.26.0",
"priorCliVersion": "2.26.0"
}

53756
lib/entry-points.js generated

File diff suppressed because one or more lines are too long

1787
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "codeql",
"version": "4.38.4",
"version": "4.37.4",
"private": true,
"description": "CodeQL action",
"scripts": {
@@ -30,50 +30,50 @@
"@actions/http-client": "^3.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "^3.0.1",
"@octokit/core": "^7.0.8",
"@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.2",
"js-yaml": "^5.2.1",
"jsonschema": "1.5.0",
"long": "^5.3.2",
"node-forge": "^1.4.0",
"semver": "^7.8.5",
"undici": "^6.28.0",
"uuid": "^14.0.2"
"uuid": "^14.0.1",
"undici": "^6.24.0"
},
"devDependencies": {
"@ava/typescript": "6.0.0",
"@eslint/compat": "^2.1.1",
"@eslint/compat": "^2.1.0",
"@microsoft/eslint-formatter-sarif": "^3.1.0",
"@octokit/types": "^18.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": "^65.0.1",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-no-async-foreach": "^0.1.1",
"glob": "^13.0.6",
"globals": "^17.12.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.70.1"
"typescript-eslint": "^8.64.0"
},
"overrides": {
"@actions/tool-cache": {
@@ -95,6 +95,6 @@
"semver": ">=6.3.1"
},
"glob": "^13.0.6",
"undici": "^6.28.0"
"undici": "^6.24.0"
}
}

View File

@@ -1 +0,0 @@
24

View File

@@ -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 {

View File

@@ -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));
});
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));
});
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);
});
});

View File

@@ -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 (import.meta.main) {
process.exit(main());
}

View File

@@ -43,6 +43,6 @@ async function main() {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
void main();
}

View File

@@ -5,50 +5,17 @@
*/
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import { describe, it } from "node:test";
import {
addBodyLinesToUnreleasedSection,
ChangelogSection,
EMPTY_CHANGELOG,
getHeader,
getReleaseDateString,
NO_CHANGES_STR,
parseChangelog,
processChangelogForBackports,
renderChangelog,
setVersionAndDate,
UNRELEASED_PLACEHOLDER,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";
const testDate = new Date(2026, 7, 14);
describe("getHeader", async () => {
function Section(headerLine: string): ChangelogSection {
return {
headerLine,
bodyLines: [],
};
}
await it("returns non-headers unchanged", () => {
assert.equal("foo", getHeader(Section("foo")));
assert.equal("- bar", getHeader(Section("- bar")));
});
await it("strips octothorpes", async () => {
assert.equal("foo", getHeader(Section("# foo")));
assert.equal("foo", getHeader(Section("## foo")));
assert.equal("foo", getHeader(Section("### foo")));
assert.equal("foo", getHeader(Section("#### foo")));
assert.equal("foo", getHeader(Section("##### foo")));
assert.equal("foo", getHeader(Section("###### foo")));
});
await it("strips whitespace", async () => {
assert.equal("foo", getHeader(Section("# foo ")));
});
});
describe("getReleaseDateString", async () => {
await it("formats dates as expected", async () => {
assert.equal(getReleaseDateString(testDate), "14 Aug 2026");
@@ -70,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
@@ -99,73 +58,3 @@ describe("processChangelogForBackports", async () => {
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
});
});
describe("addBodyLinesToUnreleasedSection", async () => {
function newChangelogWithSections(sections: ChangelogSection[]) {
return {
preamble: [],
sections,
};
}
await it("throws error if '[UNRELEASED]' section is not first", async () => {
const invalidChangelog = newChangelogWithSections([
{
headerLine: "## Release 1.0.0",
bodyLines: [],
},
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: [],
},
]);
assert.throws(() =>
addBodyLinesToUnreleasedSection(invalidChangelog, ["foo"]),
);
});
await it("overwrites 'No user facing changes.'", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
addBodyLinesToUnreleasedSection(changelog, ["- foo"]);
assert.equal(changelog.sections[0].bodyLines.length, 3);
assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]);
});
await it("does nothing if lines is empty", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
const changelogClone = structuredClone(changelog);
addBodyLinesToUnreleasedSection(changelog, []);
assert.deepEqual(changelog, changelogClone);
});
await it("inserts a line", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", "- Added a new dependency.", ""],
},
]);
const lineToInsert = "- foo";
addBodyLinesToUnreleasedSection(changelog, [lineToInsert]);
assert.equal(changelog.sections[0].bodyLines.length, 4);
assert.ok(
changelog.sections[0].bodyLines.some((line) => line === lineToInsert),
);
});
});

View File

@@ -2,44 +2,15 @@ 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.";
/** 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 the text of the header (without the '## ' prefix) of the given section.
* */
export function getHeader(section: ChangelogSection): string {
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
}
/** Returns `date` formatted as `DD Mon YYYY`. */
export function getReleaseDateString(today: Date = new Date()): string {
return today.toLocaleDateString("en-GB", {
@@ -82,112 +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 };
}
/**
* Inserts the changenotes `lines` in the `[UNRELEASED]` section of `changelog`.
* If the section contains the stock message {@link NO_CHANGES_STR}, then
* `lines` will be inserted in place and the stock message will be deleted.
*
* @throws Error -- if the [UNRELEASED] section does not exist.
*
* @param changelog The CHANGELOG object to modify.
* @param lines The changenotes to insert.
*/
export function addBodyLinesToUnreleasedSection(
changelog: Changelog,
lines: string[],
) {
// Do nothing if there is nothing to insert.
if (lines.length === 0) return;
const unreleasedSection = changelog.sections[0];
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
throw Error(
`'${UNRELEASED_PLACEHOLDER}' is not the first section of 'CHANGELOG.md'`,
);
}
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
unreleasedSection.bodyLines = ["", ...lines, ""];
return;
}
// The last body line should be a blank line (for spacing).
// Remove it so that we can add `lines` and then add the blank line back.
unreleasedSection.bodyLines.pop();
unreleasedSection.bodyLines.push(...lines);
unreleasedSection.bodyLines.push("");
}
/**
* 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);
}
/**
@@ -200,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);
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;
}

View File

@@ -1,166 +0,0 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { withTmpFile } from "../../src/util";
import {
hasValidChangenoteCategory,
isValidChangenoteContent,
isValidChangenoteFile,
isValidChangenoteFilename,
VALID_CHANGE_NOTE_CATEGORIES,
} from "./validate";
await describe("isValidChangenoteContent", async () => {
await it("recognizes an unordered Markdown list", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
for (const category of Object.keys(VALID_CHANGE_NOTE_CATEGORIES)) {
const frontmatter = { category };
assert.equal(hasValidChangenoteCategory(frontmatter), true);
}
});
await it("rejects invalid categories", () => {
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", () => {
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",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), true);
},
);
});
await it("rejects a non-existent path", async () => {
assert.equal(isValidChangenoteFile("non-existent-file.md"), false);
});
await it("rejects invalid Markdown", async () => {
await withTmpFile(
"2026-01-01-fix-bug.md",
"---\ncategory: fix\n---\n* Fixed a bug\n",
(filePath) => {
assert.equal(isValidChangenoteFile(filePath), false);
},
);
});
});

View File

@@ -1,106 +0,0 @@
import * as fs from "node:fs";
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 { content } = matter(fileData);
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;
}

View File

@@ -1,134 +0,0 @@
#!/usr/bin/env npx tsx
import * as fs from "node:fs";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import path from "path";
import { ExitCode } from "@actions/core";
import { matter } from "lite-matter";
import {
addBodyLinesToUnreleasedSection,
parseChangelog,
renderChangelog,
withChangelog,
} from "./changelog";
import { isValidChangenoteFile } from "./changelog/validate";
import { CHANGENOTES_DIR } from "./config";
/**
* Describes a changenote file, including its file path, frontmatter, and content.
*/
interface ChangenoteFile {
absolutePath: string;
data: Record<string, any>;
content: string;
}
/**
* Returns the absolute file paths of all files in
* {@link CHANGENOTES_DIR} (except ".gitkeep" and "README.md").
* */
function listUnreleasedChangenoteDir(): string[] {
return fs
.readdirSync(CHANGENOTES_DIR)
.filter((name) => ![".gitkeep", "README.md"].includes(name))
.map((name) => path.join(CHANGENOTES_DIR, name));
}
/**
* Scans the {@link CHANGENOTES_DIR} directory for changenote files
* and returns a parsed listing of those changenote files.
*/
function getChangenotes(): ChangenoteFile[] {
return listUnreleasedChangenoteDir().map((absolutePath) => {
return {
absolutePath,
...matter(fs.readFileSync(absolutePath, "utf-8")),
};
});
}
const entryPoint = process.argv[1];
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
try {
process.exit(main());
} catch (error) {
console.error(error);
process.exit(ExitCode.Failure);
}
}
function main(): ExitCode {
const { positionals } = parseArgs({
allowPositionals: true,
strict: true,
});
const [command] = positionals;
switch (command) {
case undefined:
case "help":
return usage();
case "assemble":
return assemble();
case "validate":
return validate();
default:
console.error(`Unknown command: ${command}`);
return ExitCode.Failure;
}
}
function usage(): ExitCode {
const message =
"Usage: changenotes.ts assemble\n" +
" changenotes.ts validate\n" +
" changenotes.ts help";
console.log(message);
return ExitCode.Success;
}
function assemble(): ExitCode {
try {
const changenotes = getChangenotes();
const changenoteBodies = changenotes.map((c) => c.content);
const changenotePaths = changenotes.map((c) => c.absolutePath);
withChangelog((contents) => {
const changelog = parseChangelog(contents);
addBodyLinesToUnreleasedSection(changelog, changenoteBodies);
return renderChangelog(changelog);
}, {});
// Delete changenotes only after successful processing.
for (const p of changenotePaths) {
fs.unlinkSync(p);
}
return ExitCode.Success;
} catch (e) {
console.error("Failed to assemble changenotes to 'CHANGELOG.md'", e);
}
return ExitCode.Failure;
}
function validate(): ExitCode {
try {
const allChangenotesValid = getChangenotes().reduce(
(r, changenote) => r && isValidChangenoteFile(changenote.absolutePath),
true,
);
if (allChangenotesValid) {
console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`);
return ExitCode.Success;
}
} catch (error) {
console.error(
`Failed to read changenotes directory '${CHANGENOTES_DIR}'`,
error,
);
}
return ExitCode.Failure;
}

View File

@@ -218,6 +218,6 @@ async function run(): Promise<void> {
}
}
if (import.meta.main) {
if (require.main === module) {
void run();
}

View File

@@ -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

View File

@@ -30,8 +30,7 @@ steps:
- id: init
uses: ./../action/init
with:
# Request multiple languages so this check uses the combined bundle.
languages: javascript,python
languages: javascript
tools: ${{ steps.prepare-test.outputs.tools-url }}
- uses: ./../action/analyze
with:

View File

@@ -11,10 +11,6 @@ installDotNet: true
env:
CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false
CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true
# To balance speed and coverage, we analyze only a single language (JavaScript), but use the
# combined bundle so we can test that baseline information is reported for each language in the
# multi-language source directory.
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false
steps:
- uses: ./../action/init
id: init

View File

@@ -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'."

View File

@@ -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

View File

@@ -15,19 +15,17 @@ 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
steps:
- name: Install Python 3.13.15 for older CLI versions
# Older CLI versions don't work with Python 3.13.16 or newer because their Python extractor
# imports `importlib._bootstrap._ERR_MSG`, which those Python versions no longer define.
- name: Install Python 3.13 for older CLI versions
# We need Python 3.13 for older CLI versions because they are not compatible with Python 3.14 or newer.
# See https://github.com/github/codeql-action/pull/3212
if: matrix.version != 'nightly-latest' && matrix.version != 'linked'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13.15"
python-version: "3.13"
- name: Use Xcode 16
# Only the older CodeQL CLI versions need Xcode 16, and these run on macOS 15.

View File

@@ -1,117 +0,0 @@
name: Per-language bundles
description: Validates extraction and analysis using each per-language CodeQL bundle.
# TODO: Use a released bundle once releases include per-language bundles.
matrix:
include:
- language: actions
os: ubuntu-latest
version: nightly-latest
# Actions also needs the JavaScript extractor.
expected-extractors: actions javascript
- language: cpp
os: ubuntu-latest
version: nightly-latest
build-mode: manual
build-command: gcc -o main main.c
- language: csharp
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: go
os: ubuntu-latest
version: nightly-latest
build-mode: autobuild
- language: java
os: ubuntu-latest
version: nightly-latest
build-mode: none
- language: javascript
os: ubuntu-latest
version: nightly-latest
- language: python
os: ubuntu-latest
version: nightly-latest
- language: ruby
os: ubuntu-latest
version: nightly-latest
- language: rust
os: ubuntu-latest
version: nightly-latest
- language: swift
os: macos-latest-xlarge
version: nightly-latest
build-mode: autobuild
env:
CODEQL_ACTION_PER_LANGUAGE_BUNDLES: true
steps:
- uses: ./../action/init
id: init
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix['build-mode'] }}
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains only the expected extractors
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
LANGUAGE: ${{ matrix.language }}
EXPECTED_EXTRACTORS: ${{ matrix['expected-extractors'] || matrix.language }}
run: |
extractors="$("$CODEQL_PATH" resolve languages --format=json | jq -r 'keys[]')"
echo "Extractors in the bundle:"
echo "$extractors"
echo "Expected: $EXPECTED_EXTRACTORS"
for expected in $EXPECTED_EXTRACTORS; do
if ! echo "$extractors" | grep -qx "$expected"; then
echo "::error::The ${LANGUAGE} bundle does not contain the ${expected} extractor."
exit 1
fi
done
# If the bundle contained extractors beyond those the language needs, then it would not
# have been trimmed, and this job would be silently validating the combined bundle.
for other in actions cpp csharp go java javascript python ruby rust swift; do
if echo "$EXPECTED_EXTRACTORS" | grep -qw "$other"; then
continue
fi
if echo "$extractors" | grep -qx "$other"; then
echo "::error::The ${LANGUAGE} bundle also contains the ${other} extractor, so it is not trimmed."
exit 1
fi
done
- name: Check that the bundle was not added to the toolcache
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# A bundle that is missing most of its extractors must never be left in the toolcache,
# where a later job analyzing a different language could pick it up. The runner image
# ships with its own CodeQL in the toolcache, so check where this bundle was extracted to
# rather than whether the toolcache contains CodeQL at all.
echo "CodeQL is at $CODEQL_PATH"
if [[ "$CODEQL_PATH" == "$RUNNER_TOOL_CACHE"/* ]]; then
echo "::error::The per-language bundle was added to the toolcache at $CODEQL_PATH."
exit 1
fi
if [[ "$CODEQL_PATH" != "$RUNNER_TEMP"/* ]]; then
echo "::error::Expected the per-language bundle to be extracted under $RUNNER_TEMP, but found it at $CODEQL_PATH."
exit 1
fi
- name: Build code
if: matrix['build-command']
run: ${{ matrix['build-command'] }}
- uses: ./../action/analyze
id: analysis
with:
upload-database: false
- name: Check that a database was created for the language
env:
DB_LOCATIONS: ${{ steps.analysis.outputs.db-locations }}
LANGUAGE: ${{ matrix.language }}
run: |
database="$(echo "$DB_LOCATIONS" | jq -r --arg lang "$LANGUAGE" '.[$lang] // empty')"
if [ -z "$database" ] || [ ! -d "$database" ]; then
echo "::error::No CodeQL database was created for ${LANGUAGE}."
echo "Databases: $DB_LOCATIONS"
exit 1
fi
echo "Created a ${LANGUAGE} database at ${database}."

View File

@@ -5,7 +5,7 @@ versions:
- default
steps:
- name: Set up Ruby
uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
uses: ruby/setup-ruby@003a5c4d8d6321bd302e38f6f0ec593f77f06600 # v1.319.0
with:
ruby-version: 2.6
- name: Install Code Scanning integration

View File

@@ -5,8 +5,7 @@ versions:
- default
- nightly-latest
operatingSystems:
- os: macos
runner-image: macos-latest-xlarge
- macos
installGo: true
installDotNet: true
env:

View File

@@ -1,9 +1,4 @@
import path from "path";
import { fileURLToPath } from "url";
// For backwards-compatibility.
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/** The oldest supported major version of the CodeQL Action. */
export const OLDEST_SUPPORTED_MAJOR_VERSION = 3;
@@ -23,9 +18,6 @@ export const PACKAGE_JSON = path.join(REPO_ROOT, "package.json");
/** The path of the changelog. */
export const CHANGELOG_FILE = path.join(REPO_ROOT, "CHANGELOG.md");
/** The path to the unreleased change-notes directory. */
export const CHANGENOTES_DIR = path.join(REPO_ROOT, "unreleased-change-notes");
/** The path to the esbuild metadata file. */
export const BUNDLE_METADATA_FILE = path.join(REPO_ROOT, "meta.json");
@@ -45,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. */

View File

@@ -5,8 +5,6 @@ contains:
- "test-setup-python-scripts"
- "update"
- "Update"
# Matrix-ed job; the name starts with this
- "Create backport"
is:
- "Agent"
- "check-expected-release-files"
@@ -17,6 +15,4 @@ is:
- "Label PR with size"
- "Post repo size comment"
- "Prepare"
- "Release info"
- "Upload results"
- "Update release branch"

View File

@@ -1,20 +1,17 @@
{
"private": true,
"description": "Dependencies for codeql-action scripts",
"type": "module",
"description": "Dependencies for the sync.ts",
"dependencies": {
"@actions/core": "^2.0.3",
"@actions/github": "^8.0.1",
"@octokit/core": "^7.0.8",
"@octokit/plugin-paginate-rest": ">=15.0.0",
"@octokit/plugin-rest-endpoint-methods": "^18.0.0",
"lite-matter": "^0.1.2",
"mdast-util-from-markdown": "^2.0.3",
"@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.1"
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/node": "^24.19.0",
"tsx": "^4.23.15"
"@types/node": "^20.19.43",
"tsx": "^4.23.1"
}
}

View File

@@ -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, "");
});
});

View File

@@ -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 (import.meta.main) {
process.exit(main());
}

View File

@@ -116,6 +116,6 @@ async function main() {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
void main();
}

View File

@@ -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.`,
);
});
});

View File

@@ -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 (import.meta.main) {
process.exit(main());
}

View File

@@ -21,11 +21,10 @@ import * as fs from "fs";
import { parseArgs } from "node:util";
import * as path from "path";
import { PR_CHECKS_DIR, REPO_ROOT } from "./config";
const CHECKS_DIR = path.join(PR_CHECKS_DIR, "checks");
const WORKFLOW_DIR = path.join(REPO_ROOT, ".github", "workflows");
const SYNC_TS_PATH = path.join(PR_CHECKS_DIR, "sync.ts");
const THIS_DIR = __dirname;
const CHECKS_DIR = path.join(THIS_DIR, "checks");
const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows");
const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts");
/**
* Scan generated workflow files to extract the latest action versions.
@@ -233,6 +232,6 @@ function main(): number {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
process.exit(main());
}

View File

@@ -342,6 +342,6 @@ async function main(): Promise<void> {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
void main();
}

View File

@@ -4,19 +4,12 @@ set -e
cd "$(dirname "$0")"
# Run `npm ci` in CI or `npm install` otherwise.
#
# `pr-checks` is an npm workspace of the repository root and the two share a single hoisted
# `node_modules` directory. Running npm from this directory puts it in workspace mode, where it
# ignores the root project's own dependencies by default. `npm ci` would then rebuild the shared
# `node_modules` with only this workspace's dependencies, removing the root's ones, which breaks
# anything that imports from `src` (such as `sync.ts` itself). `--include-workspace-root` keeps the
# root project's dependencies in the installed tree.
if [ "$GITHUB_ACTIONS" = "true" ]; then
echo "In Actions, running 'npm ci' for 'sync.ts'..."
npm ci --include-workspace-root
npm ci
else
echo "Running 'npm install' for 'sync.ts'..."
npm install --no-audit --no-fund --include-workspace-root
npm install --no-audit --no-fund
fi
npx tsx sync.ts

View File

@@ -7,8 +7,6 @@ import * as yaml from "yaml";
import { BuiltInLanguage } from "../src/languages";
import { PR_CHECKS_DIR, REPO_ROOT } from "./config";
/**
* Returns a `uses` value for `action` pinned to a commit SHA, with the
* human-readable version recorded in a trailing comment.
@@ -81,8 +79,6 @@ interface Specification extends JobSpecification {
useAllPlatformBundle?: string;
/** Values for the `analysis-kinds` matrix dimension. */
analysisKinds?: string[];
/** Overrides the generated job matrix using GitHub Actions matrix syntax. */
matrix?: Record<string, unknown>;
/** Container image configuration for the job. */
container?: any;
@@ -223,12 +219,6 @@ const languageSetups: LanguageSetups = {
cache: "npm",
},
},
// Install a new enough version of `npm` to understand `min-release-age`
// that is still compatible with Node 20.
{
name: "Install newer npm",
run: "npm install -g npm@11.19.1",
},
{
name: "Install dependencies",
run: "npm ci",
@@ -263,8 +253,8 @@ const languageSetups: LanguageSetups = {
name: "Install Java",
uses: pinnedUses(
"actions/setup-java",
"de7274f081f381c8f8158605e0321c36c376e2e6",
"v6.0.1",
"03ad4de0992f5dab5e18fcb136590ce7c4a0ac95",
"v5.6.0",
),
with: {
"java-version": `\${{ inputs.java-version || '${defaultLanguageVersions.java}' }}`,
@@ -314,8 +304,9 @@ const languageSetups: LanguageSetups = {
// See https://github.com/github/codeql-action/pull/3423
const YQ_VERSION = "v4.50.1";
const CHECKS_DIR = path.join(PR_CHECKS_DIR, "checks");
const OUTPUT_DIR = path.join(REPO_ROOT, ".github", "workflows");
const THIS_DIR = __dirname;
const CHECKS_DIR = path.join(THIS_DIR, "checks");
const OUTPUT_DIR = path.join(THIS_DIR, "..", ".github", "workflows");
/**
* Loads and parses a YAML file.
@@ -521,6 +512,9 @@ function generateJob(
specDocument: yaml.Document,
checkSpecification: Specification,
) {
const matrix: Array<Record<string, any>> =
generateJobMatrix(checkSpecification);
const useAllPlatformBundle = checkSpecification.useAllPlatformBundle
? checkSpecification.useAllPlatformBundle
: "false";
@@ -573,8 +567,8 @@ function generateJob(
const checkJob: Record<string, any> = {
strategy: {
"fail-fast": false,
matrix: checkSpecification.matrix ?? {
include: generateJobMatrix(checkSpecification),
matrix: {
include: matrix,
},
},
name: checkSpecification.name,

View File

@@ -6,8 +6,8 @@
"module": "preserve",
"rootDir": "..",
"sourceMap": false,
"noEmit": true
"noEmit": true,
},
"include": ["./**/*.ts", "../src/**/*.ts"],
"include": ["./*.ts", "../src/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -238,6 +238,6 @@ function main() {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
main();
}

View File

@@ -835,6 +835,6 @@ async function main(): Promise<void> {
}
// Only call `main` if this script was run directly.
if (import.meta.main) {
if (require.main === module) {
void main();
}

View File

@@ -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);
}

View File

@@ -23,8 +23,7 @@ predicate isSafeForDefaultSetup(string envVar) {
"GITHUB_BASE_REF", "GITHUB_EVENT_NAME", "GITHUB_JOB", "GITHUB_RUN_ATTEMPT", "GITHUB_RUN_ID",
"GITHUB_SHA", "GITHUB_REPOSITORY", "GITHUB_SERVER_URL", "GITHUB_TOKEN", "GITHUB_WORKFLOW",
"GITHUB_WORKSPACE", "GOFLAGS", "ImageVersion", "JAVA_TOOL_OPTIONS", "RUNNER_ARCH",
"RUNNER_ENVIRONMENT", "RUNNER_NAME", "RUNNER_OS", "RUNNER_TEMP", "RUNNER_TOOL_CACHE",
"NODE_ENV"
"RUNNER_ENVIRONMENT", "RUNNER_NAME", "RUNNER_OS", "RUNNER_TEMP", "RUNNER_TOOL_CACHE"
]
}

View File

@@ -21,16 +21,19 @@ inputs:
required: false
languages:
description: >-
A comma-separated list of CodeQL languages that the installed CodeQL CLI will be used to
analyze. If specified, the Action may use this list to select a CodeQL CLI version that is
best suited to analyzing those languages, for example by preferring a version that has a
cached overlay-base database for the specified languages.
A comma-separated list of CodeQL languages that will be analyzed in subsequent
`github/codeql-action/init` and `github/codeql-action/analyze` invocations. If specified, the
Action may use this list to select a CodeQL CLI version that is best suited to analyzing those
languages, for example by preferring a version that has a cached overlay-base database for the
specified languages. This input is not remembered and must also be passed to
`github/codeql-action/init`.
required: false
analysis-kinds:
description: >-
[Internal] A comma-separated list of analysis kinds that the installed CodeQL CLI will be used
for. If specified, the Action may use this list to select a CodeQL CLI version that is best
suited to those analysis kinds.
[Internal] A comma-separated list of analysis kinds that subsequent
`github/codeql-action/init` invocations will enable. If specified, the Action may use this
list to select a CodeQL CLI version that is best suited to those analysis kinds. This input is
not remembered and must also be passed to `github/codeql-action/init`.
Available options are the same as for the `analysis-kinds` input on the `init` Action.
default: 'code-scanning'

View File

@@ -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",
});

View File

@@ -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 {
@@ -19,10 +18,6 @@ export interface BaseState {
name: ActionName;
/** When the Action was started. */
startedAt: Date;
/** The platform the Action is running on. */
platform: NodeJS.Platform;
/** The architecture of the host. */
arch: NodeJS.Architecture;
}
/** Describes different state features that an Action may have. */
@@ -56,29 +51,6 @@ export interface FeatureState {
/** Identifies a type of state an Action may have. */
export type StateFeature = keyof FeatureState;
/**
* The `Env` feature implies the availability of the `ReadOnlyEnv` feature.
*
* If `T` is `Env`, this returns `Env | ReadOnlyEnv`.
* Otherwise, it is the identity and returns T.
*/
type ImpliedFeatures<T extends StateFeature> = T extends "Env"
? "Env" | "ReadOnlyEnv"
: T;
/**
* Given an object type `Obj`, this tries to lookup a corresponding `StateFeature`
* to which the object type belongs in `FeatureState`. Resolves to `never` if there
* is no match.
*/
type FeatureNameFor<Obj extends object> = {
[K in StateFeature]: [Obj] extends [FeatureState[K]]
? [FeatureState[K]] extends [Obj]
? K
: never
: never;
}[StateFeature];
/** Constructs the intersection of all state types identifies by `Fs`. */
export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
? Record<never, never>
@@ -89,54 +61,8 @@ export type FieldsOf<Fs extends readonly StateFeature[]> = Fs extends []
? FeatureState[Head] & FieldsOf<Tail>
: never;
/**
* Symbol used for a field in `ActionState` that carries the type array of state features.
* This is a Symbol so that it doesn't clash with any property names we might want to have.
*/
const stateFeatures = Symbol();
/** Describes the state of an Action that has access to the state corresponding to `Fs`. */
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs> & {
/**
* When given a chance, TypeScript will simplify an `ActionState<Fs>` type as much as possible,
* which results in a concrete object type that doesn't mention `Fs`.
*
* That causes problems for functions which accept `ActionState<Fs>` values, but need to know the
* feature keys `Fs`. This property here explicitly captures `Fs` in the concrete object type
* that results from simplifying `ActionState<Fs>`.
*
* This is a function rather than a field, because we want to be able to provide values of type
* `ActionState<Fs>` to functions expecting `ActionState<As>` where `As` is a subset of `Fs`.
*
* Since function types are contravariant in the types of their parameters, using a function
* type here allows that to happen.
*
* Because the field is optional, we don't have to explicitly provide a value
* for it anywhere while the type is still inferred.
*
* `Fs[number]` returns the union of all features in `Fs`. We wrap it in `ImpliedFeatures`
* so that `Env` is expanded into `Env | ReadOnlyEnv`, allowing functions that expect the
* `ReadOnlyEnv` feature to be provided with an `ActionState` that has the `Env` feature
* without requiring this to be made explicit.
*/
readonly [stateFeatures]?: (ts: ImpliedFeatures<Fs[number]>) => void;
};
/** Extends `state` with an `extra` feature. */
export function extendActionState<
// In first position, so that it can be explicitly provided if `FeatureNameFor`
// should not work on `extra`.
F extends StateFeature,
Fs extends readonly StateFeature[],
E extends FeatureState[F],
>(
state: ActionState<Fs>,
extra: E,
): ActionState<[...Fs, FeatureNameFor<E> & F]> {
return { ...state, ...extra } as unknown as ActionState<
[...Fs, FeatureNameFor<E> & F]
>;
}
export type ActionState<Fs extends readonly StateFeature[]> = FieldsOf<Fs>;
/** The type of an Action's main entry point. This is a function that is provided
* with a basic `ActionState` object with features that are always available.
@@ -152,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. */
@@ -168,34 +88,17 @@ export async function runInActions(action: Action) {
const actionsEnv = getActionsEnv();
try {
const actionState = {
await action.run({
name: action.name,
startedAt,
platform: process.platform,
arch: process.arch,
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);
}
}

View File

@@ -7,14 +7,13 @@ import * as github from "@actions/github";
import * as io from "@actions/io";
import type { Config } from "./config-utils";
import { Env, EnvVar, ActionsEnvVars, ReadOnlyEnv } from "./environment";
import { Env, EnvVar, ActionsEnvVars } from "./environment";
import { Logger } from "./logging";
import {
doesDirectoryExist,
getCodeQLDatabasePath,
ConfigurationError,
getEnv,
getErrorMessage,
} from "./util";
/**
@@ -28,7 +27,6 @@ declare const __CODEQL_ACTION_VERSION__: string;
* global functions in tests.
*/
export interface ActionsEnv {
getRequiredInput: (name: string) => string;
getOptionalInput: (name: string) => string | undefined;
}
@@ -36,10 +34,7 @@ export interface ActionsEnv {
* Gets the real `ActionsEnv` used by production code.
*/
export function getActionsEnv(): ActionsEnv {
return {
getRequiredInput,
getOptionalInput,
};
return { getOptionalInput };
}
/**
@@ -282,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: ReadOnlyEnv = 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";
@@ -411,23 +393,14 @@ export const persistInputs = function (env: Env = getEnv()) {
/**
* Restores all inputs to the action from the persisted state.
*/
export function restoreInputs(logger: Logger) {
try {
const persistedInputsValue = core.getState(persistedInputsKey);
if (persistedInputsValue) {
const persistedInputs = JSON.parse(persistedInputsValue);
for (const [name, value] of persistedInputs) {
process.env[name] = value;
}
export const restoreInputs = function () {
const persistedInputs = core.getState(persistedInputsKey);
if (persistedInputs) {
for (const [name, value] of JSON.parse(persistedInputs)) {
process.env[name] = value;
}
} catch (err) {
logger.error(`Unable to restore inputs: ${getErrorMessage(err)}`);
throw new Error(
"Failed to restore inputs from the state set by this action's main execution.",
);
}
}
};
export interface PullRequestBranches {
base: string;

View File

@@ -25,8 +25,8 @@ export async function runWrapper() {
// possible, and only use safe functions outside.
try {
actionsUtil.restoreInputs();
const logger = getActionsLogger();
actionsUtil.restoreInputs(logger);
const gitHubVersion = await getGitHubVersion();
checkGitHubVersionInRange(gitHubVersion, logger);
@@ -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,

View File

@@ -212,12 +212,7 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) {
await runAutobuild(config, BuiltInLanguage.go, logger);
}
async function run({
startedAt,
env,
logger,
actions,
}: ActionState<["Base", "Env", "Logger", "Actions"]>) {
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -260,7 +255,7 @@ async function run({
);
}
const codeql = await getCodeQL(logger, config.codeQLCmd);
const codeql = await getCodeQL(config.codeQLCmd);
if (hasBadExpectErrorInput()) {
throw new util.ConfigurationError(
@@ -288,7 +283,7 @@ async function run({
const apiDetails = getApiDetails();
const outputDir = actionsUtil.getRequiredInput("output");
env.export(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
core.exportVariable(EnvVar.SARIF_RESULTS_OUTPUT_DIR, outputDir);
const threads = util.getThreadsFlag(
actionsUtil.getOptionalInput("threads") || process.env["CODEQL_THREADS"],
logger,
@@ -312,13 +307,8 @@ async function run({
logger,
);
const checkoutPath = actions.getRequiredInput("checkout_path");
// Setup diff informed analysis if needed (based on whether init created the file)
const diffRangePackDir = await setupDiffInformedQueryRun(
logger,
checkoutPath,
);
const diffRangePackDir = await setupDiffInformedQueryRun(logger);
await warnIfGoInstalledAfterInit(config, logger);
await runAutobuildIfLegacyGoWorkflow(config, logger);
@@ -364,6 +354,7 @@ async function run({
actionsUtil.getOptionalInput("upload"),
);
if (runStats) {
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
const category = actionsUtil.getOptionalInput("category");
uploadResults = await postProcessAndUploadSarif(
@@ -397,23 +388,18 @@ async function run({
// Possibly upload the overlay-base database to actions cache.
// Note: Take care with the ordering of this call since databases may be cleaned up
// at the `overlay` level.
await cleanupAndUploadOverlayBaseDatabaseToCache(
codeql,
config,
logger,
checkoutPath,
);
await cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger);
// Possibly upload the database bundles for remote queries.
// Note: Take care with the ordering of this call since databases may be cleaned up
// at the `overlay` or `clear` level.
databaseUploadResults = await cleanupAndUploadDatabases(
{ logger, features },
repositoryNwo,
codeql,
config,
apiDetails,
checkoutPath,
features,
logger,
);
// Possibly upload the TRAP caches for later re-use
@@ -457,7 +443,7 @@ async function run({
`expect-error input was set to true but no error was thrown.`,
);
}
env.export(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
core.exportVariable(EnvVar.ANALYZE_DID_COMPLETE_SUCCESSFULLY, "true");
} catch (unwrappedError) {
const error = util.wrapError(unwrappedError);
if (

View File

@@ -7,12 +7,12 @@ import * as sinon from "sinon";
import { CodeQuality, CodeScanning, RiskAssessment } from "./analyses";
import {
runQueries,
defaultSuites,
resolveQuerySuiteAlias,
addSarifExtension,
diffRangeExtensionPackContents,
} from "./analyze";
import { createStubCodeQL } from "./codeql";
import { defaultSuites } from "./config/db-config";
import { Feature } from "./feature-flags";
import { BuiltInLanguage } from "./languages";
import { getRunnerLogger } from "./logging";

View File

@@ -5,11 +5,10 @@ import { performance } from "perf_hooks";
import * as io from "@actions/io";
import * as yaml from "js-yaml";
import { getTemporaryDirectory } from "./actions-util";
import { getTemporaryDirectory, getRequiredInput } from "./actions-util";
import * as analyses from "./analyses";
import { setupCppAutobuild } from "./autobuild";
import { type CodeQL } from "./codeql";
import { defaultSuites } from "./config/db-config";
import * as configUtils from "./config-utils";
import {
getCsharpTempDependencyDir,
@@ -234,7 +233,6 @@ async function finalizeDatabaseCreation(
*/
export async function setupDiffInformedQueryRun(
logger: Logger,
checkoutPath: string,
): Promise<string | undefined> {
return await withGroupAsync(
"Generating diff range extension pack",
@@ -247,6 +245,7 @@ export async function setupDiffInformedQueryRun(
return undefined;
}
const checkoutPath = getRequiredInput("checkout_path");
const packDir = writeDiffRangeDataExtensionPack(
logger,
diffRanges,
@@ -358,6 +357,15 @@ dataExtensions:
return diffRangeDir;
}
// A set of default query suite names that are understood by the CLI.
export const defaultSuites: Set<string> = new Set([
"security-experimental",
"security-extended",
"security-and-quality",
"code-quality",
"code-scanning",
]);
/**
* If `maybeSuite` is the name of a default query suite, it is resolved into the corresponding
* query suite name for the given `language`. Otherwise, `maybeSuite` is returned as is.

View File

@@ -111,115 +111,103 @@ test.serial("getGitHubVersion for GHEC-DR", async (t) => {
t.deepEqual({ type: util.GitHubVariant.GHEC_DR }, gheDotcom);
});
test("wrapApiConfigurationError doesn't wrap errors it isn't supposed to", (t) => {
const unwrappedErrors = [
test.serial(
"wrapApiConfigurationError correctly wraps specific configuration errors",
(t) => {
// We don't reclassify arbitrary errors
new Error("arbitrary error"),
// Same goes for arbitrary strings
"arbitrary error",
// If an HTTP error doesn't contain a specific error message, we don't wrap it.
new util.HTTPError("arbitrary HTTP error", 456),
];
const arbitraryError = new Error("arbitrary error");
let res = api.wrapApiConfigurationError(arbitraryError);
t.is(res, arbitraryError);
for (const unwrappedError of unwrappedErrors) {
const res = api.wrapApiConfigurationError(unwrappedError);
t.is(
res,
unwrappedError,
`${util.getErrorMessage(unwrappedError)} should not be wrapped by wrapApiConfigurationError`,
// Same goes for arbitrary errors
const configError = new util.ConfigurationError("arbitrary error");
res = api.wrapApiConfigurationError(configError);
t.is(res, configError);
// If an HTTP error doesn't contain a specific error message, we don't
// wrap is an an API error.
const httpError = new util.HTTPError("arbitrary HTTP error", 456);
res = api.wrapApiConfigurationError(httpError);
t.is(res, httpError);
// For other HTTP errors, we wrap them as Configuration errors if they contain
// specific error messages.
const httpNotFoundError = new util.HTTPError("commit not found", 404);
res = api.wrapApiConfigurationError(httpNotFoundError);
t.deepEqual(res, new util.ConfigurationError("commit not found"));
const refNotFoundError = new util.HTTPError(
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
404,
);
res = api.wrapApiConfigurationError(refNotFoundError);
t.deepEqual(
res,
new util.ConfigurationError(
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
),
);
}
});
test("wrapApiConfigurationError correctly wraps specific configuration errors", (t) => {
// For other HTTP errors, we wrap them as Configuration errors if they contain
// specific error messages.
const httpNotFoundError = new util.HTTPError("commit not found", 404);
const refNotFoundError = new util.HTTPError(
"ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest",
404,
);
const apiRateLimitError = new util.HTTPError(
"API rate limit exceeded for installation",
403,
);
const resourceNotAccessibleError = new util.HTTPError(
"Resource not accessible by integration",
403,
);
const errorsToWrap = [
httpNotFoundError,
refNotFoundError,
apiRateLimitError,
resourceNotAccessibleError,
];
const apiRateLimitError = new util.HTTPError(
"API rate limit exceeded for installation",
403,
);
res = api.wrapApiConfigurationError(apiRateLimitError);
t.deepEqual(
res,
new util.ConfigurationError("API rate limit exceeded for installation"),
);
for (const errorToWrap of errorsToWrap) {
const res = api.wrapApiConfigurationError(errorToWrap);
t.deepEqual(res, new util.ConfigurationError(errorToWrap.message));
}
});
test("wrapApiConfigurationError wraps token errors", async (t) => {
const tokenSuggestionMessage =
"Please check that your token is valid and has the required permissions: contents: read, security-events: write";
const badCredentialsError = new util.HTTPError("Bad credentials", 401);
const notFoundError = new util.HTTPError("Not Found", 404);
const errorsToWrap = [badCredentialsError, notFoundError];
for (const errorToWrap of errorsToWrap) {
const res = api.wrapApiConfigurationError(errorToWrap);
const tokenSuggestionMessage =
"Please check that your token is valid and has the required permissions: contents: read, security-events: write";
const badCredentialsError = new util.HTTPError("Bad credentials", 401);
res = api.wrapApiConfigurationError(badCredentialsError);
t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage));
}
});
test("wrapApiConfigurationError wraps enablement errors", async (t) => {
// Enablement errors.
const enablementErrorMessages = [
"Code Security must be enabled for this repository to use code scanning",
"Advanced Security must be enabled for this repository to use code scanning",
"Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.",
"Code quality is not enabled for this repository. Please enable code quality in the repository settings.",
];
const transforms = [
(msg: string) => msg,
(msg: string) => msg.toLowerCase(),
(msg: string) => msg.toLocaleUpperCase(),
];
const notFoundError = new util.HTTPError("Not Found", 404);
res = api.wrapApiConfigurationError(notFoundError);
t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage));
for (const enablementErrorMessage of enablementErrorMessages) {
for (const transform of transforms) {
const enablementError = new util.HTTPError(
transform(enablementErrorMessage),
403,
);
const res = api.wrapApiConfigurationError(enablementError);
t.deepEqual(
res,
new util.ConfigurationError(
api.getFeatureEnablementError(enablementError.message),
),
);
const resourceNotAccessibleError = new util.HTTPError(
"Resource not accessible by integration",
403,
);
res = api.wrapApiConfigurationError(resourceNotAccessibleError);
t.deepEqual(
res,
new util.ConfigurationError("Resource not accessible by integration"),
);
// Enablement errors.
const enablementErrorMessages = [
"Code Security must be enabled for this repository to use code scanning",
"Advanced Security must be enabled for this repository to use code scanning",
"Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.",
"Code quality is not enabled for this repository. Please enable code quality in the repository settings.",
];
const transforms = [
(msg: string) => msg,
(msg: string) => msg.toLowerCase(),
(msg: string) => msg.toLocaleUpperCase(),
];
for (const enablementErrorMessage of enablementErrorMessages) {
for (const transform of transforms) {
const enablementError = new util.HTTPError(
transform(enablementErrorMessage),
403,
);
res = api.wrapApiConfigurationError(enablementError);
t.deepEqual(
res,
new util.ConfigurationError(
api.getFeatureEnablementError(enablementError.message),
),
);
}
}
}
});
test("wrapApiConfigurationError doesn't double-wrap errors", async (t) => {
// This test checks that errors don't get wrapped a second time if `wrapApiConfigurationError`
// is called on an error that was already wrapped by a previous call to `wrapApiConfigurationError`.
// Start by calling `wrapApiConfigurationError` on an unwrapped error that should be wrapped:
const unwrappedError = new util.HTTPError("commit not found", 404);
const wrappedError = api.wrapApiConfigurationError(unwrappedError);
// Sanity-check that it was wrapped, as expected.
t.deepEqual(
wrappedError,
new util.ConfigurationError(unwrappedError.message),
);
// The result of the second call should be exactly `wrappedError`:
t.is(api.wrapApiConfigurationError(wrappedError), wrappedError);
});
},
);
test("getRegistryProxy - returns undefined if the proxy is not configured", async (t) => {
const target = callee(api.getRegistryProxy).withArgs();

View File

@@ -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 {
@@ -17,7 +20,6 @@ import {
ReadOnlyEnv,
RegistryProxyVars,
getEnv,
exportEnvVar,
} from "./environment";
import { Logger } from "./logging";
import { getRepositoryNwo, RepositoryNwo } from "./repository";
@@ -126,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 {
@@ -220,31 +222,25 @@ export async function getGitHubVersionFromApi(
return { type: GitHubVariant.DOTCOM };
}
try {
// Doesn't strictly have to be the meta endpoint as we're only
// using the response headers which are available on every request.
//
// See https://docs.github.com/en/rest/meta/meta#get-github-meta-information.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const response = await apiClient.rest.meta.get();
// Doesn't strictly have to be the meta endpoint as we're only
// using the response headers which are available on every request.
//
// See https://docs.github.com/en/rest/meta/meta#get-github-meta-information.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const response = await apiClient.rest.meta.get();
// This happens on dotcom, although we expect to have already returned in that
// case. This can also serve as a fallback in cases we haven't foreseen.
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) {
return { type: GitHubVariant.DOTCOM };
}
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") {
return { type: GitHubVariant.GHEC_DR };
}
const version = response.headers[
GITHUB_ENTERPRISE_VERSION_HEADER
] as string;
return { type: GitHubVariant.GHES, version };
} catch (err) {
throw wrapApiConfigurationError(err);
// This happens on dotcom, although we expect to have already returned in that
// case. This can also serve as a fallback in cases we haven't foreseen.
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) {
return { type: GitHubVariant.DOTCOM };
}
if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") {
return { type: GitHubVariant.GHEC_DR };
}
const version = response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] as string;
return { type: GitHubVariant.GHES, version };
}
/**
@@ -256,10 +252,9 @@ export async function getGitHubVersionFromApi(
*/
export async function getGitHubVersion(): Promise<GitHubVersion> {
if (cachedGitHubVersion === undefined) {
const apiDetails = getApiDetails();
cachedGitHubVersion = await getGitHubVersionFromApi(
createApiClientWithDetails(apiDetails),
apiDetails,
getApiClient(),
getApiDetails(),
);
}
return cachedGitHubVersion;
@@ -317,7 +312,7 @@ export async function getAnalysisKey(): Promise<string> {
const jobName = getRequiredEnvParam("GITHUB_JOB");
analysisKey = `${workflowPath}:${jobName}`;
exportEnvVar(EnvVar.ANALYSIS_KEY, analysisKey);
core.exportVariable(EnvVar.ANALYSIS_KEY, analysisKey);
return analysisKey;
}
@@ -422,14 +417,7 @@ export function getFeatureEnablementError(message: string): string {
return `Please verify that the necessary features are enabled: ${message}`;
}
/**
* Decides whether `e` is a known error returned by the GitHub API that we should
* classify as a `ConfigurationError`.
*
* @param e The error to classify.
* @returns Either `e` or a corresponding `ConfigurationError`.
*/
export function wrapApiConfigurationError<T>(e: T): T | ConfigurationError {
export function wrapApiConfigurationError(e: unknown) {
const httpError = asHTTPError(e);
if (httpError !== undefined) {
if (

View File

@@ -1 +1 @@
{"maximumVersion":"3.23","minimumVersion":"3.18"}
{"maximumVersion": "3.22", "minimumVersion": "3.17"}

View File

@@ -68,11 +68,7 @@ async function sendCompletedStatusReport(
}
}
async function run({
startedAt,
env,
logger,
}: ActionState<["Base", "Env", "Logger"]>) {
async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -103,7 +99,7 @@ async function run({
);
}
const codeql = await getCodeQL(logger, config.codeQLCmd);
const codeql = await getCodeQL(config.codeQLCmd);
languages = await determineAutobuildLanguages(codeql, config, logger);
if (languages !== undefined) {
@@ -139,7 +135,7 @@ async function run({
return;
}
env.export(EnvVar.AUTOBUILD_DID_COMPLETE_SUCCESSFULLY, "true");
core.exportVariable(EnvVar.AUTOBUILD_DID_COMPLETE_SUCCESSFULLY, "true");
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
}

View File

@@ -1,9 +1,11 @@
import * as core from "@actions/core";
import { getTemporaryDirectory, getWorkflowEventName } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { DocUrl } from "./doc-url";
import { ActionsEnvVars, EnvVar, exportEnvVar } from "./environment";
import { ActionsEnvVars, EnvVar } from "./environment";
import { Feature, featureConfig, initFeatures } from "./feature-flags";
import { BuiltInLanguage, Language } from "./languages";
import { Logger } from "./logging";
@@ -134,16 +136,16 @@ export async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
: ""
}`,
);
exportEnvVar(envVar, "false");
core.exportVariable(envVar, "false");
} else {
logger.info(
`Enabling ${featureName}. This can be disabled by setting the ${envVar} environment variable to 'false'. See ${DocUrl.DEFINE_ENV_VARIABLES} for more information.`,
);
exportEnvVar(envVar, "true");
core.exportVariable(envVar, "true");
}
} else {
logger.info(`Disabling ${featureName}.`);
exportEnvVar(envVar, "false");
core.exportVariable(envVar, "false");
}
}
@@ -153,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);
}
@@ -163,7 +165,7 @@ export async function runAutobuild(
await codeQL.runAutobuild(config, language);
}
if (language === BuiltInLanguage.go) {
exportEnvVar(EnvVar.DID_AUTOBUILD_GOLANG, "true");
core.exportVariable(EnvVar.DID_AUTOBUILD_GOLANG, "true");
}
logger.endGroup();
}

View File

@@ -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;

View File

@@ -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(

View File

@@ -8,7 +8,6 @@ import { ConfigurationError } from "./util";
const SUPPORTED_PLATFORMS = [
["linux", "x64"],
["linux", "arm64"],
["win32", "x64"],
["darwin", "x64"],
["darwin", "arm64"],

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