Compare commits

..
Author SHA1 Message Date
Mario Campos 3ea37a44b6 Merge pull request #4217 from github/mario-campos/warn-on-changelog-write
Add `post-comment` Action
2026-11-11 04:38:32 +00:00
Henry Mercer 592d162ba3 Merge pull request #4312 from github/henrymercer/all-platform-bundle-checks
Make the "All-platform bundle" PR check use the all-platform bundle
2026-11-10 18:07:39 +00:00
Henry MercerandCopilot App 4e3aa4135f Remove the CodeQL Bundle All PR check, which duplicates all-platform-bundle
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-09 17:13:44 +00:00
Henry MercerandCopilot App 91bae3ac68 Check that the all-platform-bundle PR check uses an all-platform bundle
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-09 17:13:32 +00:00
Henry MercerandCopilot App cfb3940966 Respect use-all-platform-bundle for the latest nightly in prepare-test
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-09 17:13:08 +00:00
Mario CamposandCopilot App 05870b623f Defer workflow activation to a follow-up PR
Keep the post-comment implementation independent of the workflow that loads it from main.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-03 11:13:53 -06:00
Mario Campos 9d1378fb6e Use new case expression for evaluating conditional --update-only flag 2026-11-02 09:23:54 -06:00
Mario Campos 4094755f8c Polish post-comment.ts usage comment 2026-11-02 09:19:54 -06:00
Mario Campos d71593936c Replace --create-if-missing to --update-only
`--update-only` is disabled by default, whereas `--create-if-missing` should've been enabled by default but was not.
2026-10-30 10:41:05 -05:00
Mario Campos 9de5eacd23 Make error a bit clearer and more actionable 2026-10-30 09:56:15 -05:00
Mario Campos 97a381d7b6 Split long line 2026-10-30 09:51:41 -05:00
Mario Campos eec64423e6 Make precedence explicit with parens 2026-10-30 09:50:21 -05:00
Mario Campos 8ba2b06960 Use parseRepositoryNwo instead of splitting manually 2026-10-30 09:49:42 -05:00
Mario Campos b25bc2e8e6 Change RepositoryNwo import to type 2026-10-30 09:43:29 -05:00
Mario Campos 0bc39f9ffc Prefer last comment when there are multiple to choose from 2026-10-30 09:35:55 -05:00
Mario Campos a5878670a9 Update actions/setup-node action to version 7.1.0 2026-10-30 09:28:58 -05:00
Mario Campos 1a18e9047f Merge branch 'main' into mario-campos/warn-on-changelog-write 2026-10-30 09:27:10 -05:00
Mario Campos 68a9dcbb35 Prefer == true over != false 2026-10-29 19:46:00 -05:00
Mario CamposandCopilot Autofix powered by AI 1304cc4df1 Potential fix for pull request finding 'CodeQL / Missing space in string concatenation'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-10-29 11:37:40 -05:00
Mario CamposandCopilot App 94f0998581 Avoid creating resolved comments without an existing warning
Use a boolean --create-if-missing flag, supplied by the composite action by default and omitted for resolution updates.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-29 11:32:57 -05:00
Mario Campos e9651489af Avoid non-null assertion operators by asserting arguments 2026-10-29 10:44:10 -05:00
Mario Campos 9359b6b571 Wrap --marker argument in HTML comment tags automatically 2026-10-29 10:34:12 -05:00
Mario Campos 19e1ab5452 Update pr-checks/post-comment.ts description comment 2026-10-29 10:28:26 -05:00
Mario Campos f13bcf383f Warn if more than one marked comment was found 2026-10-29 10:20:21 -05:00
Mario Campos 5bb3554330 Make input-validation assertions more granular 2026-10-29 10:13:06 -05:00
Mario Campos 87cac24cdd Avoid sparse checkout
This codebase is a tad coupled; it could end up biting us.
2026-10-29 10:03:32 -05:00
Mario Campos 2902eeab4d Checkout main instead of PR's base for the post-pr-comments job 2026-10-29 10:01:27 -05:00
Mario Campos 56fa196def Use YAML multiline string for the Action's inputs' descriptions 2026-10-29 09:58:48 -05:00
Mario Campos 3fb676f638 Use existing RepositoryNwo type 2026-10-29 09:56:18 -05:00
Mario Campos afaf324646 Move resolveToken to pr-checks/api-client.ts 2026-10-29 09:51:48 -05:00
Mario CamposandCopilot App c069d49e81 Test sticky comments using observable state
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 16:46:41 -05:00
Mario CamposandCopilot App 36f220a96c Remove unused post-comment step IDs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 16:08:22 -05:00
Mario CamposandCopilot App 03137fc68d Rename changelog outputs to modified
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 16:06:08 -05:00
Mario CamposandCopilot App 94488778fd Simplify post-comment to a single set-comment mode
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 16:00:37 -05:00
Mario CamposandCopilot App ae23865548 Move comment branching into workflow steps
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 15:53:36 -05:00
Mario CamposandCopilot App 90d1bc983c Remove bespoke post-comment argument validation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 15:39:43 -05:00
Mario CamposandCopilot App ae9f97bade Make post-comment marker configurable
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-10-28 15:30:49 -05:00
Mario Campos e7dadca872 Refactor comment handling logic into performAction for improved testability 2026-10-09 17:35:09 -05:00
Mario Campos f1419b8c76 Reject empty --marker 2026-10-09 17:11:18 -05:00
Mario CamposandCopilot Autofix powered by AI 3c452a182d Use HTTP link to unreleased-change-notes/README.md
Update link in PR checks workflow to point to the correct README.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-09 10:36:48 -05:00
Mario CamposandCopilot Autofix powered by AI 4d464b0697 Improve issue ID parsing and validation logic
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-09 10:35:34 -05:00
Mario Campos 14cefaa03c Replace bash/run step with local Action post-comment 2026-10-08 23:03:55 -05:00
Mario Campos 8c8b0827d5 Add script and Action for managing PR comments 2026-10-08 23:02:25 -05:00
Mario Campos 9fe741e77d Simplify if condition of CHANGELOG-comment-post step 2026-10-08 15:26:56 -05:00
Mario CamposandMichael B. Gale 27d37d97e2 Format all file references as inline Markdown code spans
Co-authored-by: Michael B. Gale <mbg@github.com>
2026-10-08 15:19:38 -05:00
Mario Campos 0707124ab8 Delete CHANGELOG.md-modification comment if CHANGELOG.md no longer modified 2026-10-08 13:11:34 -05:00
Mario CamposandCopilot Autofix powered by AI 928548a094 Refine exclusion conditions for CHANGELOG-modification comment
Refactor conditions for PR title checks in workflow.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-08 12:57:52 -05:00
Mario CamposandCopilot Autofix powered by AI ccdf69089a Escape backticks PR comment body
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-10-08 12:55:10 -05:00
Mario Campos d97a656f70 Update CHANGELOG.md warning message to reference change-notes directory 2026-10-08 12:41:52 -05:00
Mario Campos 6e4d3fb63d Use a sticky comment to warn about CHANGELOG.md changes 2026-10-08 12:31:10 -05:00
Mario Campos ae75d466ce Exclude "Mergeback" and "Update default bundle" PRs from CHANGELOG.md change warning 2026-10-08 12:11:29 -05:00
Mario Campos 780e947861 Skip CHANGELOG.md modification warning for release and backport PRs 2026-10-08 11:48:09 -05:00
Mario Campos f7aa9dc1ba Move post-comment step to post-pr-comments job 2026-10-08 11:40:38 -05:00
Mario Campos 43ac64c267 Use gh api instead of third-party Action 2026-10-08 11:16:19 -05:00
Mario Campos fbd33ac6fc Gate comment-post step on successful fetch-base 2026-10-08 10:50:34 -05:00
Mario Campos b2b77a81a2 Add CHANGELOG.md modification check and warning comment 2026-10-08 10:47:46 -05:00
14 changed files with 743 additions and 171 deletions

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
name: "Post comment"
description: "Create or update a sticky PR comment identified by a caller-provided marker."
inputs:
marker:
description: >
Non-empty plain-text identifier for the comment. Use a unique, stable value
for each distinct comment; it is automatically wrapped in a hidden HTML comment.
required: true
body:
description: "Full comment markdown."
required: true
update-only:
description: Do nothing if no comment with the specified marker exists.
required: false
default: "false"
issue-id:
description: >
The issue or pull request number to post/update the comment on.
Defaults to the current pull request if not provided.
required: false
token:
description: "The GitHub token to authenticate with."
required: true
runs:
using: composite
steps:
- name: Post comment
shell: bash
env:
BODY: ${{ inputs.body }}
MARKER: ${{ inputs.marker }}
UPDATE_ONLY: ${{ case(inputs.update-only == 'true', '--update-only', '') }}
GH_TOKEN: ${{ inputs.token }}
ISSUE_ID: ${{ inputs.issue-id || github.event.pull_request.number }}
run: |
npx tsx ./pr-checks/post-comment.ts \
--body "$BODY" \
--marker "$MARKER" \
$UPDATE_ONLY \
--issue-id "$ISSUE_ID" \
--repository "$GITHUB_REPOSITORY"
+8 -1
View File
@@ -36,7 +36,14 @@ runs:
set -e # Fail this Action if `gh release list` fails.
if [[ "$VERSION" == "nightly" || "$VERSION" == "nightly-latest" ]]; then
echo "tools-url=nightly" >> "$GITHUB_OUTPUT"
if [[ "$USE_ALL_PLATFORM_BUNDLE" == "true" ]]; then
# With `tools: nightly`, the Action downloads the bundle for the runner's platform, so look
# up the latest nightly release here to get its all-platform bundle instead.
tag=$(gh release list --repo dsp-testing/codeql-cli-nightlies --limit 1 --json tagName --jq '.[0].tagName')
echo "tools-url=https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/$tag/codeql-bundle.tar.gz" >> "$GITHUB_OUTPUT"
else
echo "tools-url=nightly" >> "$GITHUB_OUTPUT"
fi
exit 0
elif [[ "$VERSION" == "linked" ]]; then
echo "tools-url=linked" >> "$GITHUB_OUTPUT"
-5
View File
@@ -1,5 +0,0 @@
paths-ignore:
- lib
- tests
queries:
- uses: security-and-quality
+12
View File
@@ -92,6 +92,18 @@ jobs:
# Swift is not supported on Ubuntu so we manually exclude it from the list here
languages: cpp,csharp,go,java,javascript,python,ruby
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains tools for all platforms
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# Platform-specific bundles only contain the tools for their own platform.
tools_dir="$(dirname "$CODEQL_PATH")/tools"
for platform in linux64 osx64 win64; do
if [[ ! -d "$tools_dir/$platform" ]]; then
echo "::error::Expected an all-platform CodeQL bundle, but '$tools_dir' does not contain a '$platform' directory."
exit 1
fi
done
- name: Build code
run: ./build.sh
- uses: ./../action/analyze
@@ -3,7 +3,7 @@
# pr-checks/sync.sh
# to regenerate this file.
name: PR Check - Runner images
name: PR Check - Linux Arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GO111MODULE: auto
@@ -47,38 +47,16 @@ defaults:
shell: bash
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: runner-images-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
jobs:
runner-images:
linux-arm64:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
version: linked
- os: ubuntu-22.04-arm
version: linked
- os: ubuntu-24.04
version: linked
- os: ubuntu-24.04-arm
version: linked
- os: ubuntu-26.04
version: linked
- os: ubuntu-26.04-arm
version: linked
- os: windows-2022
version: linked
- os: windows-2025
version: linked
- os: macos-15-large
version: linked
- os: macos-15-xlarge
version: linked
- os: macos-26-large
version: linked
- os: macos-26-xlarge
version: linked
name: Runner images
version: nightly-latest
name: Linux Arm64
if: github.triggering_actor != 'dependabot[bot]'
permissions:
contents: read
@@ -124,5 +102,5 @@ jobs:
echo "Found database for $lang"
done
env:
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby${{ startsWith(matrix.os, 'macos') && ',swift' || '' }}
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
CODEQL_ACTION_TEST_MODE: true
+94 -7
View File
@@ -19,16 +19,69 @@ env:
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
jobs:
analyze:
# Identify the CodeQL tool versions to use in the analysis job.
check-codeql-versions:
if: github.triggering_actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
outputs:
versions: ${{ steps.compare.outputs.versions }}
permissions:
contents: read
# We currently need `security-events: read` to access feature flags.
security-events: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up default CodeQL bundle
id: setup-default
uses: ./setup-codeql
- name: Set up linked CodeQL bundle
id: setup-linked
uses: ./setup-codeql
with:
tools: linked
- name: Compare default and linked CodeQL bundle versions
id: compare
env:
CODEQL_DEFAULT: ${{ steps.setup-default.outputs.codeql-path }}
CODEQL_LINKED: ${{ steps.setup-linked.outputs.codeql-path }}
run: |
CODEQL_VERSION_DEFAULT="$("$CODEQL_DEFAULT" version --format terse)"
CODEQL_VERSION_LINKED="$("$CODEQL_LINKED" version --format terse)"
echo "Default CodeQL bundle version is $CODEQL_VERSION_DEFAULT"
echo "Linked CodeQL bundle version is $CODEQL_VERSION_LINKED"
# If we're running on a pull request, run with both bundles, even if `tools: linked` would
# be the same as `tools: null`. This allows us to make the job for each of the bundles a
# required status check.
#
# If we're running on push or schedule, then we can skip running with `tools: linked` when it would be
# the same as running with `tools: null`.
if [[ "$GITHUB_EVENT_NAME" != "pull_request" && "$GITHUB_EVENT_NAME" != "merge_group" && "$CODEQL_VERSION_DEFAULT" == "$CODEQL_VERSION_LINKED" ]]; then
VERSIONS_JSON='[null]'
else
VERSIONS_JSON='[null, "linked"]'
fi
# Output a JSON-encoded list with the distinct versions to test against.
echo "Suggested matrix config for analysis job: $VERSIONS_JSON"
echo "versions=${VERSIONS_JSON}" >> $GITHUB_OUTPUT
analyze-javascript:
if: github.triggering_actor != 'dependabot[bot]'
needs: [check-codeql-versions]
strategy:
fail-fast: false
matrix:
language:
- actions
- javascript
os:
- ubuntu-22.04
- ubuntu-24.04
- windows-2022
- windows-2025
- macos-15-xlarge
tools: ${{ fromJson(needs.check-codeql-versions.outputs.versions) }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
@@ -41,15 +94,49 @@ jobs:
uses: ./init
id: init
with:
languages: ${{ matrix.language }}
build-mode: none
config-file: ./.github/codeql/codeql-config-${{ matrix.language }}.yml
languages: javascript
config-file: ./.github/codeql/codeql-config-javascript.yml
tools: ${{ matrix.tools }}
# confirm steps.init.outputs.codeql-path points to the codeql binary
- name: Print CodeQL Version
run: >
"$CODEQL" version --format=json
env:
CODEQL: ${{steps.init.outputs.codeql-path}}
- name: Perform CodeQL Analysis
uses: ./analyze
with:
category: "/language:javascript"
upload: ${{ (matrix.os == 'ubuntu-24.04' && !matrix.tools && github.event_name != 'merge_group' && 'always' ) || 'never' }}
analyze-other:
if: github.triggering_actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
strategy:
fail-fast: false
matrix:
include:
- language: actions
permissions:
contents: read
security-events: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize CodeQL
uses: ./init
with:
languages: ${{ matrix.language }}
build-mode: none
config: >
paths-ignore:
- lib
- tests
queries:
- uses: security-and-quality
- name: Perform CodeQL Analysis
uses: ./analyze
with:
-2
View File
@@ -31,7 +31,6 @@ on:
- "PR Check - C/C\\+\\+: autoinstalling dependencies is skipped (macOS)"
- "PR Check - C/C\\+\\+: disabling autoinstalling dependencies (Linux)"
- "PR Check - Clean up database cluster directory"
- "PR Check - CodeQL Bundle All"
- "PR Check - Config export"
- "PR Check - Config input"
- "PR Check - Custom source root"
@@ -61,7 +60,6 @@ on:
- "PR Check - Remote config file"
- "PR Check - Resolve environment"
- "PR Check - Ruby analysis"
- "PR Check - Runner images"
- "PR Check - Rust analysis"
- "PR Check - Split workflow"
- "PR Check - Start proxy"
@@ -1,62 +0,0 @@
name: "PR Check - CodeQL Bundle All"
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:
concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
group: ${{ github.workflow }}-${{ github.ref }}
defaults:
run:
shell: bash
jobs:
test-codeql-bundle-all:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-26.04
version: nightly-latest
name: "CodeQL Bundle All"
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: true
- name: Install .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "9.x"
- id: init
uses: ./../action/init
with:
# We manually exclude Swift from the languages list here, as it is not supported on Ubuntu
languages: cpp,csharp,go,java,javascript,python,ruby
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Build code
run: ./build.sh
- uses: ./../action/analyze
env:
CODEQL_ACTION_TEST_MODE: true
+11
View File
@@ -8,3 +8,14 @@ export function getApiClient(token: string): ApiClient {
const opts = githubUtils.getOctokitOptions(token);
return new githubUtils.GitHub(opts);
}
/** Resolves the GitHub API token to use, from `GH_TOKEN` or `GITHUB_TOKEN`. */
export function resolveToken(env: NodeJS.ProcessEnv): string {
const token = env.GH_TOKEN?.trim() || env.GITHUB_TOKEN?.trim();
if (!token) {
throw new Error(
"Missing authentication token. Set GH_TOKEN or GITHUB_TOKEN.",
);
}
return token;
}
+12
View File
@@ -17,6 +17,18 @@ steps:
# Swift is not supported on Ubuntu so we manually exclude it from the list here
languages: cpp,csharp,go,java,javascript,python,ruby
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Check that the bundle contains tools for all platforms
env:
CODEQL_PATH: ${{ steps.init.outputs.codeql-path }}
run: |
# Platform-specific bundles only contain the tools for their own platform.
tools_dir="$(dirname "$CODEQL_PATH")/tools"
for platform in linux64 osx64 win64; do
if [[ ! -d "$tools_dir/$platform" ]]; then
echo "::error::Expected an all-platform CodeQL bundle, but '$tools_dir' does not contain a '$platform' directory."
exit 1
fi
done
- name: Build code
run: ./build.sh
- uses: ./../action/analyze
+35
View File
@@ -0,0 +1,35 @@
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
-66
View File
@@ -1,66 +0,0 @@
name: "Runner images"
description: "An end-to-end integration test checking that CodeQL can analyze interpreted and compiled code on the GitHub-hosted runner images"
# This check runs on each image listed in
# https://github.com/actions/runner-images#available-images, except for:
# - `ubuntu-slim`, which is intended for lightweight jobs rather than builds like this one.
# - `windows-11-arm`, since CodeQL doesn't support Windows on Arm64.
# - `xcode-27`, a preview image that the `unsupported-macos` check runs on.
operatingSystems:
# The Ubuntu 22.04 images have brownouts from 23 March 2027 and are retired on 17 April 2027, so
# remove them before the brownouts start.
# See https://github.com/actions/runner-images/issues/14254.
- os: ubuntu
runner-image: ubuntu-22.04
- os: ubuntu
runner-image: ubuntu-22.04-arm
- os: ubuntu
runner-image: ubuntu-24.04
- os: ubuntu
runner-image: ubuntu-24.04-arm
- os: ubuntu
runner-image: ubuntu-26.04
- os: ubuntu
runner-image: ubuntu-26.04-arm
- os: windows
runner-image: windows-2022
- os: windows
runner-image: windows-2025
# Use larger runners for macOS to keep these jobs off the critical path.
- os: macos
runner-image: macos-15-large
- os: macos
runner-image: macos-15-xlarge
- os: macos
runner-image: macos-26-large
- os: macos
runner-image: macos-26-xlarge
# Other PR checks cover the supported CodeQL versions, so this check only tests the linked version.
# Linux Arm64 bundles are only available for CodeQL 2.27.0 and later, so older stable versions
# can't run on the Arm64 images.
versions:
- linked
installGo: true
installDotNet: true
# The languages to build and analyze. CodeQL only supports Swift on macOS.
env:
LANGUAGES: "cpp,csharp,go,java,javascript,python,ruby${{ startsWith(matrix.os, 'macos') && ',swift' || '' }}"
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
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env npx tsx
/*
Tests for post-comment.ts.
*/
import * as assert from "node:assert/strict";
import { describe, it } from "node:test";
import { type ApiClient, resolveToken } from "./api-client";
import {
findExistingComment,
type Options,
parseOptions,
setComment,
} from "./post-comment";
/** A baseline set of valid CLI flags, as an array, for use in parseOptions tests. */
const validFlags = [
"--body",
"hello",
"--marker",
"marker",
"--issue-id",
"42",
"--repository",
"github/codeql-action",
];
/** Returns a copy of `validFlags` with the value following `flag` replaced by `value`. */
function withFlag(flag: string, value: string): string[] {
const flags = [...validFlags];
const index = flags.indexOf(flag);
flags[index + 1] = value;
return flags;
}
/** Returns a copy of `validFlags` with `flag` (and its value) removed entirely. */
function withoutFlag(flag: string): string[] {
const flags = [...validFlags];
const index = flags.indexOf(flag);
flags.splice(index, 2);
return flags;
}
describe("parseOptions", async () => {
await it("parses valid flags", () => {
const options = parseOptions(validFlags);
assert.deepEqual(options, {
body: "hello",
marker: "marker",
updateOnly: false,
issueId: 42,
repository: { owner: "github", repo: "codeql-action" },
});
});
await it("enables update-only mode when --update-only is supplied", () => {
const options = parseOptions([...validFlags, "--update-only"]);
assert.equal(options.updateOnly, true);
});
await it("rejects a missing --body", () => {
assert.throws(
() => parseOptions(withoutFlag("--body")),
/Missing required flag --body/,
);
});
await it("rejects an empty --body", () => {
assert.throws(
() => parseOptions(withFlag("--body", "")),
/The value for --body cannot be empty/,
);
});
await it("rejects a whitespace-only --body", () => {
assert.throws(
() => parseOptions(withFlag("--body", " \t\n")),
/The value for --body cannot be empty/,
);
});
await it("rejects a missing --marker", () => {
assert.throws(
() => parseOptions(withoutFlag("--marker")),
/Missing required flag --marker/,
);
});
await it("rejects an empty --marker", () => {
assert.throws(
() => parseOptions(withFlag("--marker", "")),
/The value for --marker cannot be empty/,
);
});
await it("rejects a whitespace-only --marker", () => {
assert.throws(
() => parseOptions(withFlag("--marker", " \t\n")),
/The value for --marker cannot be empty/,
);
});
await it("rejects a missing --repository", () => {
assert.throws(
() => parseOptions(withoutFlag("--repository")),
/Missing required flag --repository/,
);
});
await it("rejects an empty --repository", () => {
assert.throws(
() => parseOptions(withFlag("--repository", "")),
/The value for --repository cannot be empty/,
);
});
await it("rejects a whitespace-only --repository", () => {
assert.throws(
() => parseOptions(withFlag("--repository", " \t\n")),
/The value for --repository cannot be empty/,
);
});
await it("rejects an invalid --repository value", () => {
assert.throws(
() => parseOptions(withFlag("--repository", "github/codeql/action")),
/Invalid --repository value/,
);
});
});
describe("resolveToken", async () => {
await it("reads the token from GH_TOKEN", () => {
assert.equal(
resolveToken({ GH_TOKEN: "gh-token", GITHUB_TOKEN: "gha-token" }),
"gh-token",
);
});
await it("falls back to GITHUB_TOKEN", () => {
assert.equal(resolveToken({ GITHUB_TOKEN: "gha-token" }), "gha-token");
});
await it("throws when no token is set", () => {
assert.throws(() => resolveToken({}), /Missing authentication token/);
});
});
/** A minimal fake of the subset of `ApiClient` used by post-comment.ts. */
function fakeClient(existingComments: Array<{ id: number; body?: string }>) {
const comments = existingComments.map((comment) => ({ ...comment }));
let nextId =
comments.reduce((maxId, comment) => Math.max(maxId, comment.id), 0) + 1;
const client = {
paginate: async () => comments,
rest: {
issues: {
listComments: () => {},
createComment: async ({ body }: { body: string }) => {
comments.push({ id: nextId++, body });
},
updateComment: async ({
comment_id,
body,
}: {
comment_id: number;
body: string;
}) => {
const comment = comments.find(({ id }) => id === comment_id);
assert.ok(comment, `Comment ${comment_id} does not exist.`);
comment.body = body;
},
},
},
};
return { client: client as unknown as ApiClient, comments };
}
describe("findExistingComment", async () => {
await it("finds the id of the first comment containing the marker", async () => {
const { client } = fakeClient([
{ id: 1, body: "unrelated" },
{ id: 2, body: "<!-- marker -->\nhello" },
{ id: 3, body: "<!-- marker -->\nanother" },
]);
const id = await findExistingComment(
client,
{ owner: "owner", repo: "repo" },
1,
"marker",
);
assert.equal(id, 3);
});
await it("returns undefined when no comment matches", async () => {
const { client } = fakeClient([{ id: 1, body: "unrelated" }]);
const id = await findExistingComment(
client,
{ owner: "owner", repo: "repo" },
1,
"marker",
);
assert.equal(id, undefined);
});
});
/** A baseline set of `Options`, for use in `setComment` tests. */
const baseOptions: Options = {
body: "hello",
marker: "marker",
updateOnly: false,
issueId: 42,
repository: { owner: "owner", repo: "repo" },
};
function assertCommentContent(
comment: { body?: string },
marker: string,
body: string,
): void {
assert.ok(comment.body?.startsWith(`<!-- ${marker} -->`));
assert.ok(comment.body?.endsWith(body));
}
describe("setComment", async () => {
await it("does not create a comment in update-only mode when none exists", async () => {
const { client, comments } = fakeClient([]);
await setComment(client, { ...baseOptions, updateOnly: true });
// Resolution does not introduce a warning that was never posted.
assert.deepEqual(comments, []);
});
await it("leaves unrelated comments unchanged in update-only mode", async () => {
const originalComments = [{ id: 7, body: "unrelated" }];
const { client, comments } = fakeClient(originalComments);
await setComment(client, { ...baseOptions, updateOnly: true });
// Unrelated comments neither trigger resolution nor get overwritten.
assert.deepEqual(comments, originalComments);
});
await it("uses the supplied marker without affecting other comments", async () => {
const marker = "custom-warning";
const { client, comments } = fakeClient([
{ id: 1, body: "<!-- marker -->\nunrelated warning" },
{ id: 2, body: "<!-- custom-warning -->\nold warning" },
]);
const options = parseOptions(withFlag("--marker", marker));
await setComment(client, options);
// Only the comment with the supplied marker changes.
assert.equal(comments.length, 2);
assert.deepEqual(comments[0], {
id: 1,
body: "<!-- marker -->\nunrelated warning",
});
assert.equal(comments[1].id, 2);
assertCommentContent(comments[1], marker, options.body);
});
await it("creates a new comment with the marker prepended", async () => {
const { client, comments } = fakeClient([]);
// No comment has the marker before posting.
assert.equal(comments.length, 0);
assert.ok(
!comments.some(({ body }) => body?.startsWith("<!-- marker -->")),
);
await setComment(client, baseOptions);
// Posting adds exactly one comment with the marker before the body.
assert.equal(comments.length, 1);
assertCommentContent(comments[0], baseOptions.marker, baseOptions.body);
});
await it("creates a new comment when no existing comment matches", async () => {
const originalComments = [
{ id: 7, body: "unrelated" },
{ id: 2, body: "another unrelated comment" },
];
const { client, comments } = fakeClient(originalComments);
// Existing comments do not match the marker.
assert.ok(
!comments.some(({ body }) => body?.startsWith("<!-- marker -->")),
);
await setComment(client, baseOptions);
// A new comment is added without changing unrelated comments.
assert.equal(comments.length, originalComments.length + 1);
assert.deepEqual(
comments.slice(0, originalComments.length),
originalComments,
);
const markedComments = comments.filter(({ body }) =>
body?.startsWith("<!-- marker -->"),
);
// Exactly one comment contains the expected marker and body.
assert.equal(markedComments.length, 1);
assertCommentContent(
markedComments[0],
baseOptions.marker,
baseOptions.body,
);
// The new comment's ID does not collide with existing IDs.
assert.equal(new Set(comments.map(({ id }) => id)).size, comments.length);
});
await it("updates the existing comment when one is found", async () => {
const { client, comments } = fakeClient([
{ id: 7, body: "<!-- marker -->\nold" },
]);
await setComment(client, baseOptions);
// Updating preserves the comment's ID without creating a duplicate.
assert.equal(comments.length, 1);
assert.equal(comments[0].id, 7);
assertCommentContent(comments[0], baseOptions.marker, baseOptions.body);
});
await it("replaces a warning with the resolved state using the same marker", async () => {
const { client, comments } = fakeClient([
{ id: 9, body: "<!-- marker -->\nold" },
]);
const body =
"`CHANGELOG.md` is not modified in this PR. No changelog action is needed.";
await setComment(client, { ...baseOptions, body, updateOnly: true });
// The resolved state replaces the warning on the same marked comment.
assert.equal(comments.length, 1);
assert.equal(comments[0].id, 9);
assertCommentContent(comments[0], baseOptions.marker, body);
});
});
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env npx tsx
/*
Creates or updates a pull-request comment identified by a marker.
The marker is supplied via `--marker` and must be a non-empty plain-text identifier.
The CLI wraps the marker in a hidden HTML comment, prepends it to `--body`,
and uses it to find the comment on later runs.
Supply `--update-only` to suppress creation of comment; omit it
to update or create comments.
Example:
```sh
GH_TOKEN=<token> npx tsx ./pr-checks/post-comment.ts \
--body "The changelog needs updating." \
--marker "my-bot" \
--issue-id 123 \
--repository github/codeql-action
```
*/
import assert from "node:assert/strict";
import { parseArgs } from "node:util";
import { parseRepositoryNwo, type RepositoryNwo } from "../src/repository";
import { type ApiClient, getApiClient, resolveToken } from "./api-client";
/** Represents the command-line options. */
export interface Options {
/** The full comment body. */
body: string;
/** Plain-text identifier wrapped in a hidden HTML comment. */
marker: string;
/** Whether to only update a comment if a matching marker exists. */
updateOnly: boolean;
/** The issue/pull request number to post/update the comment on. */
issueId: number;
/** The repository to operate on. */
repository: RepositoryNwo;
}
function hiddenMarker(marker: string): string {
return `<!-- ${marker} -->`;
}
/** Parses CLI arguments supplied by the composite action into `Options`. */
export function parseOptions(argv?: string[]): Options {
const { values } = parseArgs({
args: argv,
options: {
body: { type: "string" },
marker: { type: "string" },
"update-only": { type: "boolean", default: false },
"issue-id": { type: "string" },
repository: { type: "string" },
},
strict: true,
});
assert(values.body !== undefined, "Missing required flag --body.");
assert(values.body.trim() !== "", "The value for --body cannot be empty.");
assert(values.marker !== undefined, "Missing required flag --marker.");
assert(
values.marker.trim() !== "",
"The value for --marker cannot be empty.",
);
assert(
values.repository !== undefined,
"Missing required flag --repository.",
);
assert(
values.repository.trim() !== "",
"The value for --repository cannot be empty.",
);
assert(
/^[\w.-]+\/[\w.-]+$/.test(values.repository),
"Invalid --repository value.",
);
return {
body: values.body,
marker: values.marker,
updateOnly: values["update-only"],
issueId: Number(values["issue-id"]),
repository: parseRepositoryNwo(values.repository),
};
}
/**
* Finds the first comment on the given issue/pull request whose body starts with `marker`, if any.
*/
export async function findExistingComment(
client: ApiClient,
repository: RepositoryNwo,
issueNumber: number,
marker: string,
): Promise<number | undefined> {
const formattedMarker = hiddenMarker(marker);
const comments = await client.paginate(client.rest.issues.listComments, {
...repository,
issue_number: issueNumber,
per_page: 100,
});
const markedComments = comments.filter((comment) =>
comment.body?.startsWith(formattedMarker),
);
if (markedComments.length > 1) {
console.warn(
`Found ${markedComments.length} comments with marker "${formattedMarker}". ` +
"Updating the first one and leaving the others unchanged. " +
"Use a unique marker for each distinct managed comment to avoid this situation.",
);
}
return markedComments.at(-1)?.id;
}
/**
* Creates or updates a marked comment using `client` to talk to the GitHub API.
*/
export async function setComment(
client: ApiClient,
options: Options,
): Promise<void> {
const { issueId, marker, repository } = options;
const body = `${hiddenMarker(marker)}\n${options.body}`;
const existingCommentId = await findExistingComment(
client,
repository,
issueId,
marker,
);
if (existingCommentId === undefined) {
if (options.updateOnly) {
console.info(
"No existing comment found; skipping creation because --update-only was specified.",
);
return;
}
console.info("No existing comment found; creating a new comment.");
await client.rest.issues.createComment({
...repository,
issue_number: issueId,
body,
});
} else {
console.info(`Updating existing comment ${existingCommentId}.`);
await client.rest.issues.updateComment({
...repository,
comment_id: existingCommentId,
body,
});
}
}
async function main(): Promise<number> {
const options = parseOptions();
const token = resolveToken(process.env);
const client = getApiClient(token);
await setComment(client, options);
return 0;
}
async function run(): Promise<void> {
try {
process.exit(await main());
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
}
if (import.meta.main) {
void run();
}