docs: add guide for adding a CodeQL data extension #350
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |