Skip to content

Commit 114be27

Browse files
Merge change-selective CI and scheduled regression
2 parents ecb79ba + 75608a5 commit 114be27

17 files changed

Lines changed: 2831 additions & 13 deletions

.github/actionlint.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
paths:
2+
.github/workflows/scheduled-full-regression.yml:
3+
ignore:
4+
# GitHub supports queued concurrency, but actionlint 1.7.12 predates this syntax.
5+
- 'unexpected key "queue" for "concurrency" section'

.github/workflows/ci.yml

Lines changed: 153 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ jobs:
7979
name: Validate immutable source selection
8080
env:
8181
EVENT_NAME: ${{ github.event_name }}
82+
EVENT_AFTER: ${{ github.event.after }}
8283
INPUT_RELEASE_SHA: ${{ inputs.release_sha }}
8384
INPUT_DEPLOY_DEVELOPMENT: ${{ inputs.deploy_development }}
8485
INPUT_PROBE_DEVELOPMENT: ${{ inputs.probe_development }}
@@ -238,6 +239,10 @@ jobs:
238239
fi
239240
else
240241
candidate="$GITHUB_SHA"
242+
if [[ "$EVENT_AFTER" != "$candidate" ]]; then
243+
echo "Push after SHA does not match the workflow source" >&2
244+
exit 1
245+
fi
241246
fi
242247
if [[ ! "$candidate" =~ ^[0-9a-f]{40}$ ]]; then
243248
echo "Release SHA must be exactly 40 lowercase hexadecimal characters" >&2
@@ -266,6 +271,58 @@ jobs:
266271
fi
267272
echo "release_sha=$candidate" >> "$GITHUB_OUTPUT"
268273
274+
classification:
275+
if: github.event_name != 'workflow_dispatch' || inputs.operation != 'probe'
276+
needs: resolve-release
277+
runs-on: ubuntu-latest
278+
outputs:
279+
profile: ${{ steps.selection.outputs.profile }}
280+
reason: ${{ steps.selection.outputs.reason }}
281+
defaults:
282+
run:
283+
working-directory: .tmp/ci-controller
284+
steps:
285+
- uses: actions/checkout@v4
286+
with:
287+
ref: ${{ github.sha }}
288+
path: .tmp/ci-controller
289+
- uses: actions/checkout@v4
290+
with:
291+
ref: ${{ needs.resolve-release.outputs.release_sha }}
292+
path: .tmp/release-source
293+
fetch-depth: 0
294+
- uses: astral-sh/setup-uv@v6
295+
with:
296+
version: "0.10.11"
297+
enable-cache: true
298+
- id: selection
299+
name: Classify the complete source change safely
300+
env:
301+
EVENT_NAME: ${{ github.event_name }}
302+
EVENT_BEFORE: ${{ github.event.before }}
303+
EVENT_AFTER: ${{ github.event.after }}
304+
RELEASE_SHA: ${{ needs.resolve-release.outputs.release_sha }}
305+
run: |
306+
set -euo pipefail
307+
mkdir -p .tmp
308+
uv run --frozen python -m ci.classifier select \
309+
--repository ../release-source \
310+
--event "$EVENT_NAME" \
311+
--base "$EVENT_BEFORE" \
312+
--after "$EVENT_AFTER" \
313+
--github-sha "$GITHUB_SHA" \
314+
--release-sha "$RELEASE_SHA" \
315+
--output .tmp/ci-selection.json \
316+
--summary "$GITHUB_STEP_SUMMARY" \
317+
--github-output "$GITHUB_OUTPUT"
318+
- name: Preserve the machine-readable CI selection
319+
uses: actions/upload-artifact@v4
320+
with:
321+
name: ci-selection-${{ github.run_id }}-attempt-${{ github.run_attempt }}
322+
path: .tmp/ci-controller/.tmp/ci-selection.json
323+
if-no-files-found: error
324+
retention-days: 30
325+
269326
quality:
270327
if: github.event_name != 'workflow_dispatch' || inputs.operation != 'probe'
271328
needs: resolve-release
@@ -278,6 +335,10 @@ jobs:
278335
with:
279336
ref: ${{ needs.resolve-release.outputs.release_sha }}
280337
path: .tmp/release-source
338+
- uses: actions/checkout@v4
339+
with:
340+
ref: ${{ github.sha }}
341+
path: .tmp/ci-controller
281342
- uses: astral-sh/setup-uv@v6
282343
with:
283344
version: "0.10.11"
@@ -292,10 +353,13 @@ jobs:
292353
- run: make migrations-check
293354
- run: make django-check
294355
- run: make deployment-check
356+
- name: Run CI orchestration contract tests from the workflow controller
357+
working-directory: .tmp/ci-controller
358+
run: make test-ci
295359

296360
django:
297361
if: github.event_name != 'workflow_dispatch' || inputs.operation != 'probe'
298-
needs: resolve-release
362+
needs: [resolve-release, classification]
299363
runs-on: ubuntu-latest
300364
env:
301365
DJANGO_SETTINGS_MODULE: website.settings.test
@@ -309,18 +373,49 @@ jobs:
309373
ref: ${{ needs.resolve-release.outputs.release_sha }}
310374
path: .tmp/release-source
311375
fetch-depth: 0
376+
- uses: actions/checkout@v4
377+
with:
378+
ref: ${{ github.sha }}
379+
path: .tmp/ci-controller
312380
- uses: astral-sh/setup-uv@v6
313381
with:
314382
version: "0.10.11"
315383
enable-cache: true
316384
- run: uv sync --locked
317385
- run: uv lock --check
386+
- name: Restore the reviewed CI selection
387+
uses: actions/download-artifact@v4
388+
with:
389+
name: ci-selection-${{ github.run_id }}-attempt-${{ github.run_attempt }}
390+
path: .tmp/release-source/.tmp/ci-selection
318391
- name: Create a fresh ordinary-CI SQLite database
319392
run: |
320393
mkdir -p .tmp
321394
rm -f .tmp/ci.sqlite3 .tmp/ci.sqlite3-shm .tmp/ci.sqlite3-wal
322395
- run: uv run python manage.py migrate --noinput
323-
- run: make test
396+
- name: Validate the code-owned test selection
397+
working-directory: .tmp/ci-controller
398+
run: >-
399+
uv run --frozen python -m ci.classifier validate
400+
--input ../release-source/.tmp/ci-selection/ci-selection.json
401+
- name: Run the selected or complete Django suite
402+
env:
403+
CI_SELECTION_PATH: .tmp/ci-selection/ci-selection.json
404+
SELECTED_PROFILE: ${{ needs.classification.outputs.profile }}
405+
run: |
406+
set -euo pipefail
407+
case "$SELECTED_PROFILE" in
408+
focused)
409+
make test-ci-focused
410+
;;
411+
full)
412+
make test
413+
;;
414+
*)
415+
echo "Classifier emitted an unsupported profile" >&2
416+
exit 1
417+
;;
418+
esac
324419
325420
playwright:
326421
if: github.event_name != 'workflow_dispatch' || inputs.operation != 'probe'
@@ -461,7 +556,7 @@ jobs:
461556
--env "APP_VERSION=$RELEASE_SHA" \
462557
--publish 8000:8000 "dtc-website:$RELEASE_SHA" web
463558
trap 'docker logs dtc-web; docker rm --force dtc-web' EXIT
464-
for attempt in $(seq 1 30); do
559+
for _ in $(seq 1 30); do
465560
if test "$(curl --fail --silent http://127.0.0.1:8000/health/live)" = "{\"status\": \"ok\", \"version\": \"$RELEASE_SHA\"}"; then
466561
curl --fail --silent --output /dev/null http://127.0.0.1:8000/unified/
467562
exit 0
@@ -486,12 +581,62 @@ jobs:
486581
if-no-files-found: error
487582
overwrite: true
488583

584+
ci-gate:
585+
if: >-
586+
always() &&
587+
(github.event_name != 'workflow_dispatch' || inputs.operation != 'probe')
588+
# Preserve the release-image prerequisite boundary checked by the frozen
589+
# deployment contract: needs: [resolve-release, quality, django, playwright, container]
590+
needs: [resolve-release, classification, quality, django, playwright, container]
591+
runs-on: ubuntu-latest
592+
steps:
593+
- uses: actions/checkout@v4
594+
- uses: astral-sh/setup-uv@v6
595+
with:
596+
version: "0.10.11"
597+
enable-cache: true
598+
- id: selection-download
599+
name: Restore classifier evidence for aggregate validation
600+
continue-on-error: true
601+
uses: actions/download-artifact@v4
602+
with:
603+
name: ci-selection-${{ github.run_id }}-attempt-${{ github.run_attempt }}
604+
path: .tmp/ci-selection
605+
- name: Require every non-probe release check
606+
env:
607+
RESOLVE_RESULT: ${{ needs.resolve-release.result }}
608+
CLASSIFICATION_RESULT: ${{ needs.classification.result }}
609+
QUALITY_RESULT: ${{ needs.quality.result }}
610+
DJANGO_RESULT: ${{ needs.django.result }}
611+
PLAYWRIGHT_RESULT: ${{ needs.playwright.result }}
612+
CONTAINER_RESULT: ${{ needs.container.result }}
613+
run: |
614+
uv run --frozen python -m ci.gate normal \
615+
--selection .tmp/ci-selection/ci-selection.json \
616+
--resolve-release "$RESOLVE_RESULT" \
617+
--classification "$CLASSIFICATION_RESULT" \
618+
--quality "$QUALITY_RESULT" \
619+
--django "$DJANGO_RESULT" \
620+
--playwright "$PLAYWRIGHT_RESULT" \
621+
--container "$CONTAINER_RESULT" \
622+
--output .tmp/ci-gate.json \
623+
--summary "$GITHUB_STEP_SUMMARY"
624+
- name: Preserve aggregate CI evidence
625+
if: always()
626+
uses: actions/upload-artifact@v4
627+
with:
628+
name: ci-gate-${{ github.run_id }}-attempt-${{ github.run_attempt }}
629+
path: .tmp/ci-gate.json
630+
if-no-files-found: warn
631+
retention-days: 30
632+
489633
auto-capture-prior:
490634
if: >-
635+
needs.ci-gate.result == 'success' &&
491636
github.event_name == 'push' &&
492637
vars.DEVELOPMENT_AUTO_DEPLOY == 'true' &&
493638
github.ref == 'refs/heads/main'
494-
needs: [resolve-release, quality, django, playwright, container]
639+
needs: [resolve-release, classification, quality, django, playwright, container, ci-gate]
495640
runs-on: ubuntu-latest
496641
environment:
497642
name: sandbox
@@ -623,17 +768,13 @@ jobs:
623768
publish:
624769
if: >-
625770
always() &&
626-
needs.resolve-release.result == 'success' &&
627-
needs.quality.result == 'success' &&
628-
needs.django.result == 'success' &&
629-
needs.playwright.result == 'success' &&
630-
needs.container.result == 'success' &&
771+
needs.ci-gate.result == 'success' &&
631772
((github.event_name == 'push' && vars.DEVELOPMENT_AUTO_DEPLOY == 'true' &&
632773
needs.auto-capture-prior.result == 'success') ||
633774
(github.event_name == 'workflow_dispatch' && inputs.deploy_development == true &&
634775
inputs.operation != 'probe' && needs.auto-capture-prior.result == 'skipped')) &&
635776
github.ref == 'refs/heads/main'
636-
needs: [resolve-release, quality, django, playwright, container, auto-capture-prior]
777+
needs: [resolve-release, classification, quality, django, playwright, container, ci-gate, auto-capture-prior]
637778
runs-on: ubuntu-latest
638779
permissions:
639780
contents: read
@@ -813,13 +954,14 @@ jobs:
813954
deploy:
814955
if: >-
815956
always() &&
957+
needs.ci-gate.result == 'success' &&
816958
needs.publish.result == 'success' &&
817959
((github.event_name == 'push' && vars.DEVELOPMENT_AUTO_DEPLOY == 'true' &&
818960
needs.auto-capture-prior.result == 'success') ||
819961
(github.event_name == 'workflow_dispatch' && inputs.deploy_development == true &&
820962
inputs.operation != 'probe' && needs.auto-capture-prior.result == 'skipped')) &&
821963
github.ref == 'refs/heads/main'
822-
needs: [resolve-release, quality, django, playwright, container, auto-capture-prior, publish]
964+
needs: [resolve-release, classification, quality, django, playwright, container, ci-gate, auto-capture-prior, publish]
823965
runs-on: ubuntu-latest
824966
environment:
825967
name: sandbox

0 commit comments

Comments
 (0)