Skip to content

Run change-selective CI with four-hour full regression #104

Description

@alexeygrigorev

Outcome

Main-push CI keeps its existing release, quality, browser, image, and serialized deployment safety gates, but replaces the unconditional full Django test invocation with a deterministic application-aware selection. A push whose complete diff is safely owned by one mapped application runs that application's reviewed test closure; shared, cross-application, migration, dependency, configuration, workflow, deployment, template/static, testless, unknown, or malformed changes run the full existing suite.

A separate scheduled workflow starts every four hours and runs that complete suite only when the scheduled main SHA has not already passed a successful scheduled full regression. It retries the same SHA after any unsuccessful scheduled run, runs when no successful full-regression anchor exists, and fails open to testing (never to skipping) when GitHub history cannot be interpreted safely.

This is CI orchestration only. It does not change Django admin, Studio, public behavior, application code, test assertions, deployment behavior, or HTML templates.

Product and architecture authority

The reviewed workflow currently has push and workflow_dispatch controllers; immutable release resolution; full quality; make test; core Playwright; container verification; serialized prior-capture/publish/deploy jobs; and separate manual probe jobs. The current make test runs compatibility pytest and every Django test. Ordinary CI creates a fresh SQLite database, while deployment-check only validates production configuration and must not connect to PostgreSQL.

GitHub's documented event model is part of the design:

Normal CI change selection

Exact source range

For push to main:

  1. HEAD is the immutable release SHA already validated by resolve-release; it must equal both github.sha and github.event.after and be exactly 40 lowercase hexadecimal characters.
  2. BASE is exactly github.event.before; do not derive it from HEAD^, the webhook commits array, a merge base, the last successful workflow, or a path-filter action.
  3. Checkout/fetch full history. Both commits must resolve exactly, and BASE must be an ancestor of HEAD.
  4. Enumerate the complete range with a NUL-delimited, rename/copy-aware Git diff equivalent to git diff --name-status -z --find-renames --find-copies BASE HEAD. For rename/copy records, classify both the old and new paths. Deletions are classified by their old path.
  5. An all-zero, missing, malformed, unfetchable, non-commit, non-ancestor/force-push base; a mismatched head; an empty/failed/unparseable diff; an unsupported status/mode; or any path that cannot be classified selects full with a stable fallback reason. It must never produce a successful no-test decision.

For a non-probe workflow_dispatch, there is no authoritative push base. The selected exact reachable release_sha therefore always uses profile=full, reason=manual_dispatch; no guessed diff is allowed. Existing operation=probe validation and probe-only jobs remain unchanged and do not enter this selector.

The classifier is code-owned and committed, not an inline shell expression. It emits schema-versioned JSON containing event, base, head, changed-path count, application roots, profile, sorted unique test labels, and stable reason code. Filenames are parsed NUL-safely. Test labels come only from a closed allowlist; no filename or user input is evaluated or interpolated as shell code.

Selection precedence and exact map

Force-full rules are evaluated before the application map. A focused profile is permitted only when every changed path belongs to the same mapped root, no force-full rule matches, every changed object is an ordinary tracked file, and the resulting labels validate against the allowlist. A diff spanning two or more application roots is cross-application and selects full even if every individual root is mapped.

For one safely owned application root, use this exact Django-label closure. Multiple changed files within that one root use the row once.

Changed root Sorted Django labels
api/** api
cadmin/** cadmin
content/** accounts content.tests core
courses/** accounts api cadmin content.tests core courses data
data/** api cadmin courses data
jobs/** jobs
management_api/** api management_api
management_auth/** api core management_api management_auth
review_import/** accounts review_import
studio/** accounts core studio

These closures cover the directly observed consumers at the reviewed baseline; they are deliberately broader than only the owning label. Updating the map or its allowlist is itself a force-full workflow/configuration change.

The following select full before that table is considered:

  • any **/migrations/**, **/templates/**, **/static/**, or HTML/template file;
  • accounts/** and core/**, because identity/authentication and shared primitives/configuration are deployment-wide;
  • content_sync/**, email_app/**, and events/**, because they have no owned test label at the reviewed baseline;
  • website/**, course_management/**, course_platform_templates/**, templates/**, compatibility/**, playwright_tests/**, e2e/**, deploy/**, scripts/**, .github/**, and .claude/**;
  • manage.py, Makefile, Dockerfile, pyproject.toml, uv.lock, .python-version, repository-root configuration, and dependency/tool lock files;
  • _docs/**, AGENTS.md, README.md, generated/checked contracts, fixtures outside a mapped root, symlinks/submodules/type changes, and every other unmatched path.

This default is intentionally conservative. New applications, directories, file kinds, generated contracts, or cross-application exceptions receive the full suite until an independently reviewed issue extends the map and its tests.

Workflow execution and deployment gate

For every non-probe main push and manual release:

  • preserve uv sync --locked, uv lock --check, and every existing quality command;
  • create/migrate a fresh ordinary-CI SQLite database exactly as today; do not add a PostgreSQL service or connection;
  • for profile=focused, invoke one new uv-backed Make target that validates the classifier JSON and runs one Django test process with exactly the sorted labels above;
  • for profile=full, keep make test, including compatibility pytest and all Django tests;
  • always retain the current core Playwright job and exact-image container verification/reuse rules; and
  • do not delete, weaken, quarantine, or silently ignore any test to make selection pass.

Add one ci-gate aggregation job with if: always() for the non-probe path. It validates the classifier schema/reason and requires successful release resolution, classification, quality, selected/full Django, core Playwright, and container outcomes. A skipped/cancelled/neutral/timed-out required job fails the gate; the only accepted test bypass is the existing separately validated manual probe operation.

auto-capture-prior, publish, and deploy must depend on the successful aggregate gate plus their existing data/image prerequisites rather than reconstructing conditional success independently. Preserve exact source/image coherence checks, current input semantics, job-level AWS id-token: write only where already required, and the existing workflow concurrency group website-development-release with cancel-in-progress: false. A focused selection must never allow deployment after a failed or missing required check.

Four-hour scheduled full regression

Create a separate workflow with:

  • on.schedule.cron: "17 */4 * * *" (UTC, deliberately away from the hour boundary);
  • exact scheduled source github.sha, validated as a 40-character commit and checked out exactly; this is the main commit GitHub associated with that schedule event;
  • concurrency group website-scheduled-full-regression, cancel-in-progress: false, and queue: max, so a running or pending scheduled check is not cancelled by the next tick;
  • workflow permissions only contents: read and actions: read; no id-token, AWS role, environment secret, package write, issue write, or repository-content write permission; and
  • no publish, prior-release capture, deploy, probe, production/RDS, live E2E, or AWS job.

The scheduled selector inspects completed runs of this workflow only, event=schedule, branch=main, excluding the current run. It requests up to 100 prior runs and the latest-attempt jobs for each candidate through the documented REST APIs. It records neither a repository variable nor a commit; workflow/job history is the source of truth.

Decision algorithm, in order:

  1. If history/API data is unavailable, incomplete, malformed, over the bounded search without a usable anchor, or otherwise ambiguous, run full with reason=history_unavailable or reason=no_coverage_anchor.
  2. If there is no prior completed scheduled run, run full with reason=first_scheduled_run.
  3. If the immediately previous completed scheduled run has any conclusion other than success, run full with reason=retry_after_<conclusion>, even when its SHA or an older coverage SHA equals the current SHA.
  4. Search newest-to-oldest for the latest scheduled run whose workflow conclusion is success and whose latest-attempt job with the fixed job id/name full-regression concluded success. A successful selector-only run where full-regression was skipped is not a coverage anchor.
  5. If that anchor's exact head_sha differs from the current scheduled SHA, run full with reason=sha_changed.
  6. Only when the anchor exists, its full job succeeded, the immediately previous run succeeded, and its head_sha equals the current scheduled SHA may the full job skip with reason=already_successfully_covered.

When selected, full-regression runs the same complete non-deployment verification as full main CI: locked uv environment/lock check, all current quality commands, fresh SQLite migration, make test, make test-playwright-core, and current container build/runtime/static/liveness checks. It may be split into jobs for parallelism, but a fixed final full-regression marker job succeeds only after every component succeeds; that marker is the sole future coverage anchor.

An always-running scheduled summary/gate reports the decision and succeeds on an intentional unchanged-SHA skip, but it fails when a selected full component fails. A failed selected run never advances coverage and therefore forces the next scheduled tick to test the unchanged SHA again.

Observability and safe evidence

Both selectors write a concise Actions step summary and a machine-readable JSON artifact. Normal CI reports base/head, profile, stable reason, application root, changed-path count, selected labels, and required job outcomes. Scheduled CI reports current SHA, decision/reason, previous scheduled run id/conclusion, coverage-anchor run id/SHA, history depth inspected, and full component outcomes.

Do not print API tokens, headers, environment secrets, release records, production data, or raw API responses. Repository filenames are untrusted: JSON-encode artifacts and HTML/Markdown-escape plus bound any displayed filename/reason so crafted paths cannot inject Actions-summary markup. An intentional scheduled skip must be visible as already_successfully_covered, not as an unexplained absent job.

Explicit non-goals

  • Changing application behavior, Django admin, Studio, the admin API, API-token generation, authorization, models, migrations, fixtures, or test expectations.
  • Editing, reformatting, compacting, minifying, or generating HTML templates; the candidate's template diff must be empty.
  • Predicting individual tests inside an application, parallelizing/sharding test cases, or extending selection beyond the exact table.
  • Making scheduled CI publish or deploy, changing AWS infrastructure/roles, adding secrets, using production data, or adding PostgreSQL to ordinary CI.
  • Changing the current manual probe contract, automatic-deploy enablement variable, immutable image/source checks, release inputs, deployment ordering, or rollback/promotion behavior except to consume the aggregate CI gate.
  • Treating a successful unchanged-SHA selector-only run as full-regression coverage.

Acceptance criteria

  • Push selection uses exact validated github.event.before → resolved github.event.after/github.sha, a full-history NUL-safe rename/copy-aware diff, both paths of rename/copy records, and the exact precedence/map above.
  • One eligible application root runs exactly its sorted reviewed label closure; a second root or any force-full/unknown/ambiguous input runs make test.
  • Zero/malformed/missing/unreachable/non-ancestor bases, head mismatches, empty/failed diff, unsupported records, and classifier/schema errors visibly fall back to full instead of skipping or failing open to deployment.
  • Non-probe manual dispatch always runs full; the existing probe path remains separately validated and behaviorally unchanged.
  • Focused and full pushes retain locked uv, full quality, fresh SQLite, core Playwright, container verification, immutable release/image checks, aggregate gating, and serialized automatic deployment. No ordinary-CI PostgreSQL connection is added.
  • The schedule fires at minute 17 every four UTC hours, is separately serialized/queued, checks the exact scheduled main SHA, and has only contents: read plus actions: read with no AWS/deployment authority.
  • First/no-anchor, changed-SHA, previous-failure/cancel/timeout/stale/action-required, bounded-history/API-error, and malformed-history states run full. Only a matching successful full-regression anchor with no later failed run skips.
  • A scheduled full failure retries the unchanged SHA at the next tick; a successful full advances the anchor; successful unchanged-SHA skips do not replace that anchor.
  • Normal and scheduled aggregate gates cannot report success when a required selected component failed, was cancelled, or was unexpectedly skipped.
  • Actions summaries/artifacts expose bounded safe reason/evidence without secrets or unescaped filenames; an unchanged-SHA skip has the exact observable reason already_successfully_covered.
  • Classifier, scheduler, workflow-contract, focused/full execution, and deployment-dependency tests pass through uv-backed targets; the candidate changes no HTML template.

Required automated scenarios

  1. Table-drive every application-map row and assert exact sorted labels, deduplication, schema, and focused profile for one-root multi-file diffs.
  2. Cover two mapped roots; mapped plus unknown; core/accounts/testless apps; migration/template/static/HTML; workflow/deploy/scripts/config/dependency/lock; docs/generated contracts; symlink/submodule/type change; and new unknown root. Every case selects full with its stable reason.
  3. Cover add/modify/delete and rename/copy within one root, across roots, and between mapped/force-full paths. Include spaces, tabs, newlines, Unicode, leading dashes, and shell metacharacters in paths; prove NUL-safe parsing, safe summaries, and no command injection.
  4. Cover exact push base/head validation, all-zero before, missing commit, shallow/unfetchable history, forced/non-ancestor update, mismatched after/SHA, empty diff, unsupported status, parser error, invalid output/schema, and allowlist rejection. Unsafe cases run full or block the aggregate gate—never a focused/skip deployment.
  5. Assert focused Django runs one process with only code-owned labels and full runs the existing make test; both use fresh SQLite. Assert quality, core Playwright, and container remain required in either profile.
  6. Table-drive scheduled history: no runs; same/different successful full anchor; intervening successful skips; previous full/selector failure, cancellation, timeout, stale, or action-required; successful re-run latest attempt; skipped/missing/duplicate marker job; current/non-schedule/wrong-workflow/wrong-branch exclusion; pagination boundary; rate/error/malformed response; and no anchor within 100. Verify the exact decision/reason and retry behavior.
  7. Workflow-contract tests parse the YAML and assert the four-hour cron, separate queued concurrency, least permissions, exact checkout, fixed marker, absence of AWS/publish/deploy jobs, unchanged release concurrency, aggregate dependencies, full-suite targets, and manual-full/probe behavior.
  8. Run the classifier and scheduled selector twice on fixed fixtures and byte-compare JSON. Run the existing full ordinary suite once as parity evidence and demonstrate representative focused, fallback-full, first-schedule, changed-SHA, unchanged-skip, and failed-then-retry summaries.

Tester and handoff evidence

The engineer implements and tests without committing. The independent tester receives the uncommitted candidate and records:

  • focused/fallback classifier fixtures and exact labels/reasons;
  • full-suite parity and aggregate/deployment dependency results;
  • scheduled first/change/skip/failure-retry fixture output and least-permission/concurrency checks;
  • ordinary SQLite evidence and absence of a PostgreSQL service/connection;
  • git diff --name-only evidence showing no HTML/template changes; and
  • safe Actions-summary examples with no secret/raw response data.

This issue has no public-page or template change, so browser screenshots are not applicable. Existing core Playwright must still pass. Store any scratch output only under .tmp/issue-104/; do not expose tokens, release records, production data, or credentials.

After tester verification and PM acceptance, the engineer makes the single focused commit, the orchestrator locally merges/pushes without a pull request, and on-call observes the normal automatic deployment. No commit, push, workflow dispatch, schedule manipulation, or deployment belongs to grooming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important follow-upenhancementNew feature or requestoperationsArea: operationstestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions