mirror of
https://github.com/github/codeql-action
synced 2026-11-12 00:02:14 +03:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6827a39de | ||
|
|
97d0cc029d | ||
|
|
b5a3632119 | ||
|
|
3a7f564e33 | ||
|
|
02e8a5ded2 | ||
|
|
9d4eb28961 |
No files matched your search
@@ -1,41 +0,0 @@
|
||||
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"
|
||||
@@ -36,14 +36,7 @@ runs:
|
||||
set -e # Fail this Action if `gh release list` fails.
|
||||
|
||||
if [[ "$VERSION" == "nightly" || "$VERSION" == "nightly-latest" ]]; then
|
||||
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
|
||||
echo "tools-url=nightly" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
elif [[ "$VERSION" == "linked" ]]; then
|
||||
echo "tools-url=linked" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
paths-ignore:
|
||||
- lib
|
||||
- tests
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
-12
@@ -92,18 +92,6 @@ 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
|
||||
|
||||
+28
-6
@@ -3,7 +3,7 @@
|
||||
# pr-checks/sync.sh
|
||||
# to regenerate this file.
|
||||
|
||||
name: PR Check - Linux Arm64
|
||||
name: PR Check - Runner images
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GO111MODULE: auto
|
||||
@@ -47,16 +47,38 @@ defaults:
|
||||
shell: bash
|
||||
concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || false }}
|
||||
group: linux-arm64-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
|
||||
group: runner-images-${{github.ref}}-${{inputs.dotnet-version}}-${{inputs.go-version}}
|
||||
jobs:
|
||||
linux-arm64:
|
||||
runner-images:
|
||||
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: nightly-latest
|
||||
name: Linux Arm64
|
||||
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
|
||||
if: github.triggering_actor != 'dependabot[bot]'
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -102,5 +124,5 @@ jobs:
|
||||
echo "Found database for $lang"
|
||||
done
|
||||
env:
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby
|
||||
LANGUAGES: cpp,csharp,go,java,javascript,python,ruby${{ startsWith(matrix.os, 'macos') && ',swift' || '' }}
|
||||
CODEQL_ACTION_TEST_MODE: true
|
||||
@@ -19,69 +19,16 @@ env:
|
||||
CODEQL_ACTION_TESTING_ENVIRONMENT: codeql-action-pr-checks
|
||||
|
||||
jobs:
|
||||
# Identify the CodeQL tool versions to use in the analysis job.
|
||||
check-codeql-versions:
|
||||
analyze:
|
||||
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:
|
||||
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 }}
|
||||
language:
|
||||
- actions
|
||||
- javascript
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -94,49 +41,15 @@ jobs:
|
||||
uses: ./init
|
||||
id: init
|
||||
with:
|
||||
languages: javascript
|
||||
config-file: ./.github/codeql/codeql-config-javascript.yml
|
||||
tools: ${{ matrix.tools }}
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
config-file: ./.github/codeql/codeql-config-${{ matrix.language }}.yml
|
||||
# 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:
|
||||
|
||||
@@ -31,6 +31,7 @@ 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"
|
||||
@@ -60,6 +61,7 @@ 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"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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
|
||||
@@ -8,14 +8,3 @@ 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;
|
||||
}
|
||||
@@ -17,18 +17,6 @@ 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
|
||||
@@ -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
|
||||
@@ -0,0 +1,66 @@
|
||||
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
|
||||
@@ -1,344 +0,0 @@
|
||||
#!/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);
|
||||
});
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/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();
|
||||
}
|
||||
Reference in new issue
Block a user