Skip to content

Five-minute release CI: core plus affected tests, full suite off the critical path #228

Description

@alexeygrigorev

Outcome

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:

  1. 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.
  2. _browser_profile forces full for manual_dispatch and diff_empty, so the smoke tier never runs on the exact path operators use to ship.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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_dispatch operation=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

Release core (always, in parallel):

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

  1. 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.
  2. 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.
  3. 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).
  4. 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.
  5. 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

Acceptance criteria

  • workflow_dispatch operation=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.beforeafter.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingenhancementNew feature or requestinfraArea: infraoperationsArea: operationstestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions