Skip to content

docs: add guide for adding a CodeQL data extension #350

docs: add guide for adding a CodeQL data extension

docs: add guide for adding a CodeQL data extension #350

Workflow file for this run

name: Build CodeQL Packs
on:
pull_request:
branches: [ main ]
workflow_dispatch:
# Least-privilege default; jobs that need to comment on PRs override this below.
permissions:
contents: read
packages: read
jobs:
compile-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # required by pr-suites-packs.sh (gh pr comment)
issues: write # required by pr-suites-packs.sh (gh pr comment)
packages: read # required by pr-suites-packs.sh (gh api packages)
strategy:
fail-fast: false
matrix:
language: [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
steps:
- uses: actions/checkout@v7
# Conditionally run actions based on files modified by PR, feature branch or pushed commits
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
id: changes
with:
filters: |
src:
- '${{ matrix.language }}/**'
- '.github/**'
- '.codeqlversion'
- '.release.yml'
- name: Setup CodeQL
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
id: install-codeql
uses: ./.github/actions/install-codeql
- name: Install Packs
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
CODEQL_CLI_VERSION: ${{ steps.install-codeql.outputs.codeql-cli-version }}
run: |
gh repo clone github/codeql -- -b codeql-cli-${CODEQL_CLI_VERSION} # to make stubs available for tests
codeql pack install "${{ matrix.language }}/lib"
codeql pack install "${{ matrix.language }}/src"
codeql pack install "${{ matrix.language }}/test"
- name: Compile Queries
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
./.github/scripts/pr-compile.sh "${{ github.event.number }}" "${{ matrix.language }}"
- name: Test Queries
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
RUNNER_TEMP: ${{ runner.temp }}
shell: python
run: |
import os
import subprocess
import sys
from pathlib import Path
def print_error(fmt, *args):
print(f"::error::{fmt}", *args)
def print_error_and_fail(fmt, *args):
print_error(fmt, *args)
sys.exit(1)
runner_temp = os.environ['RUNNER_TEMP']
language = "${{ matrix.language }}"
pr_number = "${{ github.event.number }}"
test_root = Path('${{ github.workspace }}', language, 'test')
def get_pr_changed_files():
# No PR context (e.g. workflow_dispatch run directly on a branch) - there is
# no PR file list to walk, so the caller always runs the full test suite.
if not pr_number:
return None
result = subprocess.run(
["gh", "pr", "view", pr_number, "--json", "files", "--jq", ".files.[].path"],
capture_output=True, text=True, check=True,
)
return [line for line in result.stdout.splitlines() if line]
def find_targeted_qlrefs(changed_files):
# If this PR only touches leaf .ql file(s) in `language` (no shared library,
# pack metadata/lockfile, or CLI/dependency version-bump changes), return the
# .qlref file(s) that test those exact quer(y/ies) - which may be empty if none
# of the touched queries have test coverage. Otherwise return None, meaning
# "run the full test suite" (some other kind of change could affect any test).
touched_queries = []
for f in changed_files:
if f in (".codeqlversion", ".release.yml") or f.startswith(".github/"):
return None
if f.startswith(f"{language}/"):
if f.endswith(".ql"):
touched_queries.append(f)
else:
return None
if not touched_queries:
return None
src_prefix = f"{language}/src/"
touched_rel = set()
for q in touched_queries:
if not q.startswith(src_prefix):
return None
touched_rel.add(q[len(src_prefix):])
# .qlref files declare the query they test via a `query: <path-relative-to-src>`
# line - this is the authoritative src<->test mapping (test folder names are
# only a loose, unreliable convention, not a formal one).
matches = []
for qlref in test_root.rglob("*.qlref"):
for line in qlref.read_text().splitlines():
line = line.strip()
if line.startswith("query:"):
if line.split(":", 1)[1].strip() in touched_rel:
matches.append(qlref)
break
return matches
changed_files = get_pr_changed_files()
targeted_qlrefs = find_targeted_qlrefs(changed_files) if changed_files is not None else None
files_to_close = []
try:
if targeted_qlrefs is not None:
if not targeted_qlrefs:
print(f"[+] No tests reference the changed .ql file(s) in {language} - nothing to run")
report_path = os.path.join(runner_temp, language, "test_report_slice_1_of_1.json")
os.makedirs(os.path.dirname(report_path), exist_ok=True)
Path(report_path).write_text("[]")
sys.exit(0)
print(f"[+] PR only touches leaf .ql file(s) in {language} - running {len(targeted_qlrefs)} targeted test(s) instead of the full suite")
slices = [(1, 1, [str(p) for p in targeted_qlrefs])]
else:
print(f"Executing tests found (recursively) in the directory '{test_root}'")
# Runners have 4 cores, so split the tests into 4 "slices", and run one per thread
num_slices = 4
slices = [(n, num_slices, [f"--slice={n}/{num_slices}", str(test_root)]) for n in range(1, num_slices+1)]
procs = []
for slice_num, total_slices, extra_args in slices:
test_report_path = os.path.join(runner_temp, language, f"test_report_slice_{slice_num}_of_{total_slices}.json")
test_log_path = os.path.join(runner_temp, language, f"test_log_slice_{slice_num}_of_{total_slices}.txt")
os.makedirs(os.path.dirname(test_report_path), exist_ok=True)
test_report_file = open(test_report_path, 'w')
test_log_file = open(test_log_path, 'w')
files_to_close.append(test_report_file)
files_to_close.append(test_log_file)
procs.append((subprocess.Popen(["codeql", "test", "run", "--failing-exitcode=122", "--verbosity=progress", "--ram=2048", "--format=json", *extra_args], stdout=test_report_file, stderr=test_log_file), test_log_path))
for p, test_log_path in procs:
p.wait()
# Progress output goes to stderr by default - previously this was only
# printed on failure, so a normal passing run looked completely silent.
# Always surface it now.
log_text = Path(test_log_path).read_text()
if log_text:
print(log_text)
if p.returncode != 0 and p.returncode != 122:
# 122 just means a test case failed - validate-test-results will catch
# that from the JSON report. Anything else is a real crash - fail fast.
print_error_and_fail(f"Failed to run tests with return code {p.returncode}")
finally:
for file in files_to_close:
file.close()
- name: Upload test results
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.language }}-test-results
path: |
${{ runner.temp }}/${{ matrix.language }}/test_report_slice_*.json
${{ runner.temp }}/${{ matrix.language }}/test_log_slice_*.txt
if-no-files-found: error
- name: Compile / Check Suites & Packs
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
./.github/scripts/pr-suites-packs.sh "${{ github.event.number }}" "${{ matrix.language }}"
validate-test-results:
name: Validate test results
needs: compile-and-test
runs-on: ubuntu-latest
steps:
- name: Check if compile-and-test job failed to complete, if so fail
if: ${{ needs.compile-and-test.result == 'failure' }}
uses: actions/github-script@v9
with:
script: |
core.setFailed('Test run job failed')
- name: Collect test results
uses: actions/download-artifact@v8
- name: Validate test results
run: |
mapfile -t test_reports < <(find . -name 'test_report_*.json')
if [[ ${#test_reports[@]} -eq 0 ]]; then
echo "No test results found"
exit 0
fi
for json_report in "${test_reports[@]}"
do
jq --raw-output '"PASS \(map(select(.pass == true)) | length)/\(length)"' "$json_report"
done
FAILING_TESTS=$(jq --slurp --raw-output '.[][] | select(.pass == false)' "${test_reports[@]}")
if [[ ! -z "$FAILING_TESTS" ]]; then
echo "ERROR: The following tests failed:"
echo $FAILING_TESTS | jq .
exit 1
fi
extensions:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'csharp', 'go', 'java', 'python' ]
steps:
- uses: actions/checkout@v7
with:
submodules: true
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
id: changes
with:
filters: |
src:
- '${{ matrix.language }}/ext/**'
- name: Setup CodeQL
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
uses: ./.github/actions/install-codeql
- name: Install Packs
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
gh extension install github/gh-codeql
# NOTE: deliberately no `gh codeql pack install` here. `ext` is a CodeQL model/extension
# pack (`library: true`, `extensionTargets`, no `dependencies`) - installing it just
# (re)writes a `codeql-pack.lock.yml` with an empty `dependencies: {}` map, and a
# checked-in lock file in that state makes a subsequent `codeql pack create` emit a
# bogus `addsTo.pack '...' is not an extension target of '...'` warning for every data
# extension in the pack (a known CodeQL CLI bug, see
# https://github.com/github/codeql/issues/20211). See CONTRIBUTING.md.
gh codeql pack create "${{ matrix.language }}/ext/"
library-sources:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'csharp', 'java' ]
steps:
- uses: actions/checkout@v7
with:
submodules: true
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
id: changes
with:
filters: |
src:
- '${{ matrix.language }}/ext-library-sources/**'
- name: Setup CodeQL
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
uses: ./.github/actions/install-codeql
- name: Install CodeQL
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
# NOTE: deliberately no `codeql pack install` here - see the matching comment in the
# `extensions` job above.
codeql pack create "${{ matrix.language }}/ext-library-sources/"
configs:
runs-on: ubuntu-latest
needs: compile-and-test
permissions:
contents: read
packages: read # required by codeql pack install (GHCR rate limiting)
pull-requests: read # required by pr-configs.sh (gh pr view)
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
id: changes
with:
filters: |
src:
- 'configs/**'
- name: Setup CodeQL
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
uses: ./.github/actions/install-codeql
- name: Install Packs
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
for lang in cpp csharp go java javascript python ruby; do
if [[ -d "${lang}/src" ]]; then
codeql pack install "${lang}/src"
fi
done
- name: "Check Configurations"
if: steps.changes.outputs.src == 'true' || github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
./.github/scripts/pr-configs.sh "${{ github.event.number }}"