-
Notifications
You must be signed in to change notification settings - Fork 25
346 lines (300 loc) · 13.6 KB
/
Copy pathci.yml
File metadata and controls
346 lines (300 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
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@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d
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@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d
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@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d
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@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d
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 }}"