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
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
_docs/PROCESS.md: engineer/tester/PM lifecycle and no commit before acceptance.
Exact reviewed website main baseline: 872fd8a9145a0e29de8890e9ea3e9d8df8c63cfd.
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:
a push payload supplies exact before and after commit identifiers (push webhook payload);
scheduled workflows run the latest default-branch commit associated with that schedule event, use UTC by default, can be delayed, and require the workflow on the default branch (events that trigger workflows);
workflow_dispatch also requires the workflow file on the default branch (triggering a workflow);
prior runs can be filtered by workflow, branch, and event, and their jobs can be inspected with Actions read permission (workflow-runs API, workflow-jobs API); and
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.
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.
Checkout/fetch full history. Both commits must resolve exactly, and BASE must be an ancestor of HEAD.
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.
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;
_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:
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.
If there is no prior completed scheduled run, run full with reason=first_scheduled_run.
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.
Search newest-to-oldest for the latest scheduled run whose workflow conclusion is successand 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.
If that anchor's exact head_sha differs from the current scheduled SHA, run full with reason=sha_changed.
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
Table-drive every application-map row and assert exact sorted labels, deduplication, schema, and focused profile for one-root multi-file diffs.
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.
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.
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.
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.
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.
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.
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.
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
_docs/PROCESS.md: engineer/tester/PM lifecycle and no commit before acceptance._docs/architecture/app-boundaries.md: one Django deployment, application ownership, shared services, and cross-application boundaries._docs/specs/10-verification-strategy.md: deterministic verification, ordinary-CI SQLite, core browser checks, and deployment verification.872fd8a9145a0e29de8890e9ea3e9d8df8c63cfd.The reviewed workflow currently has
pushandworkflow_dispatchcontrollers; immutable release resolution; full quality;make test; core Playwright; container verification; serialized prior-capture/publish/deploy jobs; and separate manual probe jobs. The currentmake testruns compatibility pytest and every Django test. Ordinary CI creates a fresh SQLite database, whiledeployment-checkonly validates production configuration and must not connect to PostgreSQL.GitHub's documented event model is part of the design:
beforeandaftercommit identifiers (push webhook payload);workflow_dispatchalso requires the workflow file on the default branch (triggering a workflow);Normal CI change selection
Exact source range
For
pushtomain:HEADis the immutable release SHA already validated byresolve-release; it must equal bothgithub.shaandgithub.event.afterand be exactly 40 lowercase hexadecimal characters.BASEis exactlygithub.event.before; do not derive it fromHEAD^, the webhookcommitsarray, a merge base, the last successful workflow, or a path-filter action.BASEmust be an ancestor ofHEAD.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.fullwith 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 reachablerelease_shatherefore always usesprofile=full,reason=manual_dispatch; no guessed diff is allowed. Existingoperation=probevalidation 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
fulleven 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.
api/**apicadmin/**cadmincontent/**accounts content.tests corecourses/**accounts api cadmin content.tests core courses datadata/**api cadmin courses datajobs/**jobsmanagement_api/**api management_apimanagement_auth/**api core management_api management_authreview_import/**accounts review_importstudio/**accounts core studioThese 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
fullbefore that table is considered:**/migrations/**,**/templates/**,**/static/**, or HTML/template file;accounts/**andcore/**, because identity/authentication and shared primitives/configuration are deployment-wide;content_sync/**,email_app/**, andevents/**, 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:
uv sync --locked,uv lock --check, and every existing quality command;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;profile=full, keepmake test, including compatibility pytest and all Django tests;Add one
ci-gateaggregation job withif: 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, anddeploymust 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 AWSid-token: writeonly where already required, and the existing workflow concurrency groupwebsite-development-releasewithcancel-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);github.sha, validated as a 40-character commit and checked out exactly; this is the main commit GitHub associated with that schedule event;website-scheduled-full-regression,cancel-in-progress: false, andqueue: max, so a running or pending scheduled check is not cancelled by the next tick;contents: readandactions: read; noid-token, AWS role, environment secret, package write, issue write, or repository-content write permission; andThe 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:
reason=history_unavailableorreason=no_coverage_anchor.reason=first_scheduled_run.success, run full withreason=retry_after_<conclusion>, even when its SHA or an older coverage SHA equals the current SHA.successand whose latest-attempt job with the fixed job id/namefull-regressionconcludedsuccess. A successful selector-only run wherefull-regressionwas skipped is not a coverage anchor.head_shadiffers from the current scheduled SHA, run full withreason=sha_changed.head_shaequals the current scheduled SHA may the full job skip withreason=already_successfully_covered.When selected,
full-regressionruns 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 finalfull-regressionmarker 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
Acceptance criteria
github.event.before→ resolvedgithub.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.make test.contents: readplusactions: readwith no AWS/deployment authority.full-regressionanchor with no later failed run skips.already_successfully_covered.Required automated scenarios
make test; both use fresh SQLite. Assert quality, core Playwright, and container remain required in either profile.Tester and handoff evidence
The engineer implements and tests without committing. The independent tester receives the uncommitted candidate and records:
git diff --name-onlyevidence showing no HTML/template changes; andThis 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.