You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Release CI (push to main and workflow_dispatch promote of a main SHA) must finish in about five minutes of wall-clock time when the change is ordinary. It always runs a small core, plus only the tests the change affects. A broken change still fails the run and does not deploy.
The complete Django suite and the large Playwright suite stay on the scheduled path. End-to-end checks against the already deployed host stay on a live/post-deploy path. They must not sit on the critical path of every release.
Why current CI is ~30–50 minutes
#104 already shipped change-selective Django. Follow-ups #210 (Playwright smoke/core/full), #212, and #213 also shipped. That design is still too slow for release because of gaps, not because the selector is missing:
workflow_dispatch is always profile=full / reason=manual_dispatch, with no Git range. A promote of current main therefore selects full Playwright (~49 minutes measured on [P0] Root-cause and fix the Playwright harness deadlock #206) even when the commit only changed docs.
_browser_profile forces full for manual_dispatch and diff_empty, so the smoke tier never runs on the exact path operators use to ship.
The Django job ignores django_mode. Playwright already skips the suite when playwright_mode != rerun. Django still uv syncs and runs tests. Docs-only plans that mark Django skip still pay the full suite.
Quality failure does not stop siblings. Quality can fail in seconds; Playwright keeps the serialized website-development-release group busy for tens of minutes. Observed on run 32949380835.
courses/ focused closure is most of the suite and used to be slower than full; --parallel --noinput is now on the focused runner, but the closure is still huge.
Concurrency cancel-in-progress: false is correct for an in-flight deploy, but a long failed run still blocks the next release. Fail-fast is the fix for that case.
Live e2e exists (e2e/, playwright_tests/test_deployed_smoke.py, deploy smoke) but is not the scheduled “test the version that is live” lane the release path should rely on.
Do not rebuild the classifier, ownership graph, evidence envelopes, or the four-hour scheduled backstop. Tighten the release lane and keep the full lane scheduled.
Target shape
Lane
When
What
Budget
Release
push to main; workflow_dispatchoperation=promote of a main SHA
Core + affected. Deploy if the aggregate gate is green.
~5 minutes wall clock, jobs in parallel
Scheduled full
existing 17 */4 * * *
Full Django, full Playwright, quality, container
unbounded except current job timeouts
Live e2e
after a successful deploy and/or a cheap schedule against https://web.dtcdev.click
Deployed smoke / remote-readonly, not in-process Playwright
does not block the next commit's compile/test start
Playwright smoke unless the plan selects core/full for render/harness impact
Release affected:
Django focused closure when the first-parent (or push) range is one mapped app
Django full only when the existing force-full rules fire (shared core/accounts, migrations, templates, deps, unknown paths, cross-app)
Screenshots only on render impact (already planned)
Fail closed: unknown/unsafe range still full. Full on the release lane may miss the 5-minute budget; that is acceptable for migrations/shared runtime. The SLO applies to ordinary application or docs commits.
Scope (implement in slices)
Slice A — this issue's first engineering delivery
Classify promote dispatch from the exact first parent of release_sha (git rev-parse <sha>^1). That SHA is the previous main for a fast-forward or --no-ff merge. Use the same NUL-safe diff and force-full rules as push. operation=rollback and any dispatch whose first parent cannot be proven stay manual_dispatch full.
Allow workflow_dispatch selections to carry a real base and single_application when that range is valid. Stop forcing Playwright full solely because reason=manual_dispatch when the selection is no longer that reason.
Honor django_mode == 'rerun' in the Django job the same way Playwright honors playwright_mode (skip the suite, still record success for an intentional skip).
Fail fast: if the quality job fails, cancel the rest of the same run so Playwright cannot occupy the release group. Document that the run conclusion may be cancelled while the quality log holds the failure. Do not weaken the quality contract.
Contract tests and _docs/ci/change-selective-ci.md for the new dispatch rule, Django skip, and fail-fast step.
Slice B — follow-up (same epic, can be the same issue's later commits or a child)
Default release Playwright to smoke for non-render; keep core for ordinary render; full only for harness/templates. Confirm smoke is a small set (today test_foundation_smoke.py is still a large file).
Scheduled or post-deploy live e2e against https://web.dtcdev.click (test_deployed_smoke / e2e remote-readonly), never blocking the next push's compile.
Measure wall-clock of green ordinary pushes; report in the existing scheduled observability summary.
Slice C — later
Shrink the courses/ focused closure once reverse-import coverage is trusted.
Optional: serialize only after a failed quality job without cancelling (job if:), if cancel-as-conclusion is too noisy.
Non-goals
No skipped quality/lint/security on release.
No deploy when the aggregate gate is red.
No deleting the four-hour full regression.
No cancel-in-progress: true on an in-flight deploy.
No guessing a dispatch base from HEAD^ of the runner checkout, merge-base with origin, or the last green run. First parent of the sealed release_sha only.
No application product behavior changes.
Authority
_docs/ci/change-selective-ci.md
_docs/audits/2026-08-20-ci-smart-test-selection-audit.md (items 5–6 still relevant; 7–10 largely done)
workflow_dispatchoperation=promote of a reachable main SHA classifies release_sha^1..release_sha when that parent exists and is an ancestor; otherwise manual_dispatch full.
Rollback dispatch remains full / manual_dispatch.
A one-app promote dispatch can be focused and Playwright smoke/core according to impact, not automatically full Playwright.
Django job skips the suite when django_mode != rerun and still succeeds; ci-gate still requires the job outcome success.
Quality failure cancels remaining jobs in the same run within a short bound; the quality log remains the failure evidence.
Push selection is unchanged: exact github.event.before → after.
Scheduled full regression still runs local make test and full Playwright as today.
Classifier, selection, gate, workflow-contract, and provenance tests cover the new dispatch range, the still-full rollback path, Django skip, and fail-fast permissions/step.
Docs: reason glossary for promote-dispatch vs manual_dispatch; 5-minute release budget; scheduled + live e2e lanes.
Scenarios
Repository: promote dispatch of a docs-only commit vs first parent selects documentation skip for Django/Playwright suite execution; quality still runs; wall-clock dominated by quality+container.
Repository: promote dispatch of courses/** only is focused Django + smoke Playwright, not make test-playwright.
Repository: rollback dispatch stays full.
Repository: quality portability failure cancels Playwright; run does not sit for 30+ minutes.
Operations: scheduled full regression unchanged; live host e2e not required for slice A.
Dependencies
None. #104/#210/#212/#213 are merged. #222 (image cache) helps container time but is not a blocker for slice A.
Outcome
Release CI (push to
mainandworkflow_dispatchpromote of a main SHA) must finish in about five minutes of wall-clock time when the change is ordinary. It always runs a small core, plus only the tests the change affects. A broken change still fails the run and does not deploy.The complete Django suite and the large Playwright suite stay on the scheduled path. End-to-end checks against the already deployed host stay on a live/post-deploy path. They must not sit on the critical path of every release.
Why current CI is ~30–50 minutes
#104 already shipped change-selective Django. Follow-ups #210 (Playwright smoke/core/full), #212, and #213 also shipped. That design is still too slow for release because of gaps, not because the selector is missing:
workflow_dispatchis alwaysprofile=full/reason=manual_dispatch, with no Git range. A promote of currentmaintherefore selects full Playwright (~49 minutes measured on [P0] Root-cause and fix the Playwright harness deadlock #206) even when the commit only changed docs._browser_profileforcesfullformanual_dispatchanddiff_empty, so the smoke tier never runs on the exact path operators use to ship.django_mode. Playwright already skips the suite whenplaywright_mode != rerun. Django stilluv syncs and runs tests. Docs-only plans that mark Django skip still pay the full suite.website-development-releasegroup busy for tens of minutes. Observed on run 32949380835.courses/focused closure is most of the suite and used to be slower than full;--parallel --noinputis now on the focused runner, but the closure is still huge.cancel-in-progress: falseis correct for an in-flight deploy, but a long failed run still blocks the next release. Fail-fast is the fix for that case.e2e/,playwright_tests/test_deployed_smoke.py, deploy smoke) but is not the scheduled “test the version that is live” lane the release path should rely on.Do not rebuild the classifier, ownership graph, evidence envelopes, or the four-hour scheduled backstop. Tighten the release lane and keep the full lane scheduled.
Target shape
main;workflow_dispatchoperation=promoteof a main SHA17 */4 * * *https://web.dtcdev.clickRelease core (always, in parallel):
test-ci)Release affected:
core/accounts, migrations, templates, deps, unknown paths, cross-app)Fail closed: unknown/unsafe range still full. Full on the release lane may miss the 5-minute budget; that is acceptable for migrations/shared runtime. The SLO applies to ordinary application or docs commits.
Scope (implement in slices)
Slice A — this issue's first engineering delivery
release_sha(git rev-parse <sha>^1). That SHA is the previous main for a fast-forward or--no-ffmerge. Use the same NUL-safe diff and force-full rules as push.operation=rollbackand any dispatch whose first parent cannot be proven staymanual_dispatchfull.workflow_dispatchselections to carry a realbaseandsingle_applicationwhen that range is valid. Stop forcing Playwrightfullsolely becausereason=manual_dispatchwhen the selection is no longer that reason.django_mode == 'rerun'in the Django job the same way Playwright honorsplaywright_mode(skip the suite, still record success for an intentional skip).cancelledwhile the quality log holds the failure. Do not weaken the quality contract._docs/ci/change-selective-ci.mdfor the new dispatch rule, Django skip, and fail-fast step.Slice B — follow-up (same epic, can be the same issue's later commits or a child)
test_foundation_smoke.pyis still a large file).https://web.dtcdev.click(test_deployed_smoke/e2eremote-readonly), never blocking the next push's compile.Slice C — later
courses/focused closure once reverse-import coverage is trusted.if:), if cancel-as-conclusion is too noisy.Non-goals
cancel-in-progress: trueon an in-flight deploy.HEAD^of the runner checkout, merge-base with origin, or the last green run. First parent of the sealedrelease_shaonly.Authority
_docs/ci/change-selective-ci.md_docs/audits/2026-08-20-ci-smart-test-selection-audit.md(items 5–6 still relevant; 7–10 largely done)_docs/specs/10-verification-strategy.md_docs/PROCESS.md(smoke/core/full tester tiers)Acceptance criteria
workflow_dispatchoperation=promoteof a reachable main SHA classifiesrelease_sha^1..release_shawhen that parent exists and is an ancestor; otherwisemanual_dispatchfull.manual_dispatch.focusedand Playwright smoke/core according to impact, not automatically full Playwright.django_mode != rerunand still succeeds; ci-gate still requires the job outcomesuccess.github.event.before→after.make testand full Playwright as today.manual_dispatch; 5-minute release budget; scheduled + live e2e lanes.Scenarios
courses/**only is focused Django + smoke Playwright, notmake test-playwright.Dependencies
None. #104/#210/#212/#213 are merged. #222 (image cache) helps container time but is not a blocker for slice A.