1919#
2020# A shared run timestamp keeps results under the same tests/runs/<timestamp>/
2121# tree; the publish job republishes failure artifacts from all test legs.
22+ #
23+ # The builds set EDG_BUILD_IDENTIFIER so that identical sources produce
24+ # byte-identical build products. Each test leg keys a cache entry on the
25+ # SHA-256 of the build products it downloaded plus the git trees of
26+ # everything else that affects the results (tests/, dev_tools/, bases/, and
27+ # this workflow, computed once by the prepare job). When a leg passes, it
28+ # saves that entry along with its coverage report; a later run on the same
29+ # ref (a re-run, or a push that leaves the build products and test inputs
30+ # unchanged) restores it, skips edgy, and reuses the coverage report.
31+ # actions/cache scopes entries to the ref that saved them (plus the default
32+ # branch), so one pull request can never vouch for another's build.
33+ #
2234# The bench job runs the cachegrind benchmarks the way
2335# `edg-docker-bench --posture=release` does and uploads them as bench-results
2436# (c/ and cpp/ runs, for comparison against the checked-in
3446name : Build & Test CI
3547
3648on :
37- push :
38- branches :
39- - main
40- # The nightly collect-changes job's commits only move Changes entries.
41- paths-ignore :
42- - ' **/Changes'
43- - ' **/pending_changes/**'
4449 pull_request :
4550 merge_group :
4651
@@ -61,19 +66,41 @@ jobs:
6166 timeout-minutes : 5
6267 outputs :
6368 run_timestamp : ${{ steps.timestamp.outputs.run_timestamp }}
69+ test_inputs : ${{ steps.test-inputs.outputs.test_inputs }}
6470 steps :
6571 - name : Declare shared edgy run timestamp
6672 id : timestamp
6773 run : |
6874 # Match edgutil.RUN_TIMESTAMP_FORMAT (%Y.%m.%d-%H.%M.%S).
6975 echo "run_timestamp=$(date -u +'%Y.%m.%d-%H.%M.%S')" >> "${GITHUB_OUTPUT}"
7076
77+ # Only the git trees are needed, so skip blobs and most of the
78+ # working tree.
79+ - name : Check out repository metadata
80+ uses : actions/checkout@v7
81+ with :
82+ persist-credentials : false
83+ filter : blob:none
84+ sparse-checkout : .github
85+
86+ - name : Hash test inputs
87+ id : test-inputs
88+ run : |
89+ # Everything besides the build products that can change a test
90+ # leg's results. The workflow file covers the container image and
91+ # the edgy options.
92+ hash="$(git rev-parse HEAD:tests HEAD:dev_tools HEAD:bases \
93+ HEAD:include_c++ \
94+ HEAD:.github/workflows/build-and-test-ci.yml |
95+ sha256sum | cut -c1-64)"
96+ echo "test_inputs=${hash}" >> "${GITHUB_OUTPUT}"
97+
7198 build-gcc :
7299 name : Build linux-gcc-debug
73100 runs-on : ubuntu-latest
74101 timeout-minutes : 120
75102 container :
76- image : ghcr.io/edgcpp/ci-runner@sha256:14b3948c2ce12cfa5472e5a6c468393d15426f3a67685cc35d830ea5db2acdf8
103+ image : ghcr.io/edgcpp/ci-runner@sha256:e83cc8c97556cdcb42353915a5164a051ba9c219d5b822f6859c875e07e9851d
77104 defaults :
78105 run :
79106 shell : edg-container-exec "bash -eo pipefail {0}"
87114 - name : Configure linux-gcc-debug
88115 env :
89116 EDG_BASE : ${{ github.workspace }}/bases/docker/dev-env/gcc
90- run : cmake --preset linux-gcc-debug -DEDG_CPFE_USE_GCOV=ON
117+ run : >-
118+ cmake --preset linux-gcc-debug -DEDG_CPFE_USE_GCOV=ON
119+ -DEDG_BUILD_IDENTIFIER=ci
91120
92121 - name : Build linux-gcc-debug
93122 env :
@@ -118,7 +147,7 @@ jobs:
118147 runs-on : ubuntu-latest
119148 timeout-minutes : 120
120149 container :
121- image : ghcr.io/edgcpp/ci-runner@sha256:14b3948c2ce12cfa5472e5a6c468393d15426f3a67685cc35d830ea5db2acdf8
150+ image : ghcr.io/edgcpp/ci-runner@sha256:e83cc8c97556cdcb42353915a5164a051ba9c219d5b822f6859c875e07e9851d
122151 defaults :
123152 run :
124153 shell : edg-container-exec "bash -eo pipefail {0}"
@@ -132,7 +161,9 @@ jobs:
132161 - name : Configure linux-i686-gcc-debug
133162 env :
134163 EDG_BASE : ${{ github.workspace }}/bases/docker/dev-env/gcc-i686
135- run : cmake --preset linux-i686-gcc-debug -DEDG_CPFE_USE_GCOV=ON
164+ run : >-
165+ cmake --preset linux-i686-gcc-debug -DEDG_CPFE_USE_GCOV=ON
166+ -DEDG_BUILD_IDENTIFIER=ci
136167
137168 - name : Build linux-i686-gcc-debug
138169 env :
@@ -168,7 +199,7 @@ jobs:
168199 - edg_x86_64
169200 - edg_x86_64_cp
170201 container :
171- image : ghcr.io/edgcpp/ci-runner@sha256:14b3948c2ce12cfa5472e5a6c468393d15426f3a67685cc35d830ea5db2acdf8
202+ image : ghcr.io/edgcpp/ci-runner@sha256:e83cc8c97556cdcb42353915a5164a051ba9c219d5b822f6859c875e07e9851d
172203 # Required for setarch ... -R (disable ASLR) used by the test harness.
173204 options : --privileged
174205 defaults :
@@ -194,7 +225,38 @@ jobs:
194225 # Artifact round-trip does not reliably preserve +x.
195226 chmod -R a+x build/gcc/bin
196227
228+ - name : Compute test cache key
229+ id : key
230+ env :
231+ CONFIG : ${{ matrix.config }}
232+ TEST_INPUTS : ${{ needs.prepare.outputs.test_inputs }}
233+ run : |
234+ # Hash the names and contents of the downloaded build products.
235+ build="$(cd build/gcc &&
236+ find . -type f -print0 | LC_ALL=C sort -z |
237+ xargs -0 sha256sum | sha256sum | cut -c1-64)"
238+ echo "Build products: ${build}"
239+ echo "key=tested-${CONFIG}-${TEST_INPUTS}-${build}" \
240+ >> "${GITHUB_OUTPUT}"
241+
242+ - name : Look for an earlier passing run of this build
243+ id : tested
244+ uses : actions/cache/restore@v6
245+ with :
246+ key : ${{ steps.key.outputs.key }}
247+ path : .tmp/tested
248+
249+ - name : Reuse earlier results
250+ if : ${{ steps.tested.outputs.cache-hit == 'true' }}
251+ run : |
252+ echo "::notice::This build already passed with the same test" \
253+ "inputs: $(cat .tmp/tested/ci-meta.json)"
254+ mkdir -p coverage
255+ cp .tmp/tested/coverage/*.json coverage/ || true
256+
197257 - name : Run edgy (${{ matrix.config }})
258+ id : edgy
259+ if : ${{ steps.tested.outputs.cache-hit != 'true' }}
198260 env :
199261 RUN_TIMESTAMP : ${{ needs.prepare.outputs.run_timestamp }}
200262 CONFIG : ${{ matrix.config }}
@@ -230,7 +292,7 @@ jobs:
230292 # Coverage problems must never fail the test leg, hence
231293 # continue-on-error.
232294 - name : Install gcovr
233- if : ${{ !cancelled() }}
295+ if : ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }}
234296 continue-on-error : true
235297 run : |
236298 python3 -m venv "${RUNNER_TEMP}/gcovr"
@@ -242,7 +304,7 @@ jobs:
242304 # hot loops exceed gcovr's "suspicious hits" threshold; warn about them
243305 # instead of failing.
244306 - name : Generate coverage report
245- if : ${{ !cancelled() }}
307+ if : ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }}
246308 continue-on-error : true
247309 env :
248310 CONFIG : ${{ matrix.config }}
@@ -269,6 +331,37 @@ jobs:
269331 if-no-files-found : warn
270332 retention-days : 1
271333
334+ # Record the pass (with its coverage report, so a run that skips edgy
335+ # still contributes coverage). Without a report, don't record it.
336+ - name : Prepare record of this passing run
337+ id : record
338+ if : >-
339+ ${{ steps.edgy.outcome == 'success' &&
340+ hashFiles(format('coverage/{0}.json', matrix.config)) != '' }}
341+ env :
342+ CONFIG : ${{ matrix.config }}
343+ RUN_ID : ${{ github.run_id }}
344+ RUN_ATTEMPT : ${{ github.run_attempt }}
345+ TESTED_SHA : ${{ github.sha }}
346+ run : |
347+ mkdir -p .tmp/tested/coverage
348+ cp "coverage/${CONFIG}.json" .tmp/tested/coverage/
349+ python3 - > .tmp/tested/ci-meta.json <<'EOF'
350+ import json, os
351+ print(json.dumps({
352+ 'tested_sha': os.environ['TESTED_SHA'],
353+ 'run_id': int(os.environ['RUN_ID']),
354+ 'run_attempt': int(os.environ['RUN_ATTEMPT']),
355+ }))
356+ EOF
357+
358+ - name : Save record of this passing run
359+ if : ${{ steps.record.outcome == 'success' }}
360+ uses : actions/cache/save@v6
361+ with :
362+ key : ${{ steps.key.outputs.key }}
363+ path : .tmp/tested
364+
272365 test-i686 :
273366 name : Test ${{ matrix.config }}
274367 needs :
@@ -283,7 +376,7 @@ jobs:
283376 - edg_x86_32
284377 - edg_x86_32_cp
285378 container :
286- image : ghcr.io/edgcpp/ci-runner@sha256:14b3948c2ce12cfa5472e5a6c468393d15426f3a67685cc35d830ea5db2acdf8
379+ image : ghcr.io/edgcpp/ci-runner@sha256:e83cc8c97556cdcb42353915a5164a051ba9c219d5b822f6859c875e07e9851d
287380 # Required for setarch ... -R (disable ASLR) used by the test harness.
288381 options : --privileged
289382 defaults :
@@ -309,7 +402,38 @@ jobs:
309402 # Artifact round-trip does not reliably preserve +x.
310403 chmod -R a+x build/gcc-i686/bin
311404
405+ - name : Compute test cache key
406+ id : key
407+ env :
408+ CONFIG : ${{ matrix.config }}
409+ TEST_INPUTS : ${{ needs.prepare.outputs.test_inputs }}
410+ run : |
411+ # Hash the names and contents of the downloaded build products.
412+ build="$(cd build/gcc-i686 &&
413+ find . -type f -print0 | LC_ALL=C sort -z |
414+ xargs -0 sha256sum | sha256sum | cut -c1-64)"
415+ echo "Build products: ${build}"
416+ echo "key=tested-${CONFIG}-${TEST_INPUTS}-${build}" \
417+ >> "${GITHUB_OUTPUT}"
418+
419+ - name : Look for an earlier passing run of this build
420+ id : tested
421+ uses : actions/cache/restore@v6
422+ with :
423+ key : ${{ steps.key.outputs.key }}
424+ path : .tmp/tested
425+
426+ - name : Reuse earlier results
427+ if : ${{ steps.tested.outputs.cache-hit == 'true' }}
428+ run : |
429+ echo "::notice::This build already passed with the same test" \
430+ "inputs: $(cat .tmp/tested/ci-meta.json)"
431+ mkdir -p coverage
432+ cp .tmp/tested/coverage/*.json coverage/ || true
433+
312434 - name : Run edgy (${{ matrix.config }})
435+ id : edgy
436+ if : ${{ steps.tested.outputs.cache-hit != 'true' }}
313437 env :
314438 RUN_TIMESTAMP : ${{ needs.prepare.outputs.run_timestamp }}
315439 CONFIG : ${{ matrix.config }}
@@ -345,7 +469,7 @@ jobs:
345469 # Coverage problems must never fail the test leg, hence
346470 # continue-on-error.
347471 - name : Install gcovr
348- if : ${{ !cancelled() }}
472+ if : ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }}
349473 continue-on-error : true
350474 run : |
351475 python3 -m venv "${RUNNER_TEMP}/gcovr"
@@ -357,7 +481,7 @@ jobs:
357481 # hot loops exceed gcovr's "suspicious hits" threshold; warn about them
358482 # instead of failing.
359483 - name : Generate coverage report
360- if : ${{ !cancelled() }}
484+ if : ${{ !cancelled() && steps.tested.outputs.cache-hit != 'true' }}
361485 continue-on-error : true
362486 env :
363487 CONFIG : ${{ matrix.config }}
@@ -384,6 +508,37 @@ jobs:
384508 if-no-files-found : warn
385509 retention-days : 1
386510
511+ # Record the pass (with its coverage report, so a run that skips edgy
512+ # still contributes coverage). Without a report, don't record it.
513+ - name : Prepare record of this passing run
514+ id : record
515+ if : >-
516+ ${{ steps.edgy.outcome == 'success' &&
517+ hashFiles(format('coverage/{0}.json', matrix.config)) != '' }}
518+ env :
519+ CONFIG : ${{ matrix.config }}
520+ RUN_ID : ${{ github.run_id }}
521+ RUN_ATTEMPT : ${{ github.run_attempt }}
522+ TESTED_SHA : ${{ github.sha }}
523+ run : |
524+ mkdir -p .tmp/tested/coverage
525+ cp "coverage/${CONFIG}.json" .tmp/tested/coverage/
526+ python3 - > .tmp/tested/ci-meta.json <<'EOF'
527+ import json, os
528+ print(json.dumps({
529+ 'tested_sha': os.environ['TESTED_SHA'],
530+ 'run_id': int(os.environ['RUN_ID']),
531+ 'run_attempt': int(os.environ['RUN_ATTEMPT']),
532+ }))
533+ EOF
534+
535+ - name : Save record of this passing run
536+ if : ${{ steps.record.outcome == 'success' }}
537+ uses : actions/cache/save@v6
538+ with :
539+ key : ${{ steps.key.outputs.key }}
540+ path : .tmp/tested
541+
387542 publish-test-results :
388543 name : Publish aggregated test results
389544 needs :
@@ -508,7 +663,7 @@ jobs:
508663 runs-on : ubuntu-latest
509664 timeout-minutes : 120
510665 container :
511- image : ghcr.io/edgcpp/ci-runner@sha256:14b3948c2ce12cfa5472e5a6c468393d15426f3a67685cc35d830ea5db2acdf8
666+ image : ghcr.io/edgcpp/ci-runner@sha256:e83cc8c97556cdcb42353915a5164a051ba9c219d5b822f6859c875e07e9851d
512667 defaults :
513668 run :
514669 shell : edg-container-exec "bash -eo pipefail {0}"
0 commit comments