Skip to content

Make the squashed course migration history safe for populated databases #220

Description

@alexeygrigorev

Outcome

Repair the Django courses migration graph so a release built from current main can safely migrate both a fresh database and a populated database whose migration history comes from the deployed legacy course release. Preserve course data and migration provenance without manually rewriting django_migrations, dropping/recreating tables, or importing a database dump as a workaround.

This is a narrowly bounded migration-history compatibility repair. It is not the Course → Cohort product migration, the full course-platform preservation/cutover rehearsal, or the content release digest repair in #219.

Normative authority and incident evidence

The exact incident inputs are frozen for this issue:

Evidence Fact Role in #220
Active deployed source 11b2bd1 The deployed course graph contains courses.0001_initial through courses.0041_courseregistrationcountsourcerun_and_more and represents the legacy migration history a populated database may record. Compatibility lower boundary.
Squashing commit 4825aa3 Replaced the active course graph with a rewritten 0001_initial plus 00020010, deleting the legacy 00020041 modules. Its own adoption note says the squash was local-only and not a production upgrade path. Introducing change under repair.
Current main b7c693e The exact source that reached the failed deployment migration task. Candidate/current target baseline.
CI run 32659885289 Application quality, Django, Playwright, container, screenshots, publish, and source-resolution jobs passed; the exact-image deployment migration stage started and the run failed. Confirmed deployment evidence, not acceptance.
Deployment evidence artifact 9499226819 Redacted evidence records source_sha=b7c693e, migration stage contract_contradiction, failed_without_success_record, and retention of the prior release without web/worker promotion. Failure classification and required regression evidence.

The failed deployment must not be “fixed” by retrying blindly, deleting migration rows, flushing the database, or treating a migration contract failure as an infrastructure success. Future evidence must identify whether a failure is a migration-graph/data-state defect or a runtime/infrastructure defect using the exact source SHA, immutable image identity, migration-task exit/error, applied migration-state snapshot, and controller/release result.

Frozen compatibility boundary

The implementation must support this exact boundary and no wider one:

  1. Fresh: an empty database at migration zero reaches every current course leaf from the repaired graph.
  2. Legacy populated: a synthetic, non-production database is created with the course migrations from 11b2bd1 applied through courses.0041_courseregistrationcountsourcerun_and_more, then populated with representative course rows and relationships. The repaired current graph must upgrade it without an inconsistent-history error, data loss, or silent schema replacement.
  3. Repaired replay: a database already upgraded by the candidate can run the same migration command again and converge to an unchanged leaf with no duplicate backfill, duplicate rows, new side effects, or checksum/count drift.

The lower boundary is the exact deployed legacy migration identity above. The upper boundary is the candidate’s repaired current course leaf. Databases older than that deployed graph, future Course → Cohort migrations, and arbitrary hand-edited django_migrations tables are outside this issue.

The engineer may use a Django-compatible replacement/bridge/staged migration design after documenting its graph and state semantics. The implementation must preserve the meaning of already-applied legacy migration identities and must prove both fresh and populated behavior. It must not assume that a fresh 0001_initial name means the existing database has the new 0001 schema.

Scope

  • Repair only the courses migration graph introduced by 4825aa3, including the compatibility boundary between the deployed legacy chain and the current squashed graph.
  • Retain or bridge deployed migration history in a way Django can resolve for existing django_migrations state; do not silently discard the legacy chain.
  • Add deterministic synthetic legacy-history fixtures/tests that exercise schema state, applied migration records, representative data, foreign keys, content types/permissions where applicable, and current leaf state.
  • Prove safe migration behavior for fresh, populated-legacy, replayed-current, and failure/retry paths. Any data transformation must be an ordinary, historical-apps-only migration operation with a clear reversible/no-op policy; no raw runtime import or external side effect is permitted.
  • Update the course migration adoption note only if needed to record the corrected compatibility contract and remaining HUMAN gate. Do not rewrite authoritative specs in this issue.
  • Leave the current course application behavior, URLs, APIs, Studio, templates, email/Datamailer behavior, and other app migration histories unchanged.

Exact changed-path boundary

The frozen engineer candidate may change only these paths:

  • courses/migrations/** — course migration graph/compatibility modules only;
  • courses/tests/test_migration_history_compatibility.py — a dedicated test module if useful;
  • test_support/tests/test_migrations.py — the existing migration harness, if extension is required;
  • test_support/migration_seeds/courses-legacy-history-v1.json — synthetic, non-PII fixture data if a committed seed is required;
  • _docs/adoption/course-platform/migration-squash-gate.md and _docs/adoption/course-platform/verification.md — factual gate/evidence updates only, if needed.

The engineer handoff must list every actual path and the exact base/head, plan digest, graph digest, evidence paths, and diff identity. No tracked path outside that allow-list is in scope. In particular, this issue must not change courses/models/**, views, templates, APIs, services, accounts/**, content/**, content_sync/**, events/**, Makefile, .github/workflows/**, pyproject.toml, uv.lock, Terraform/AWS configuration, or any database/export/protected data.

Explicit non-goals

  • No Course → Cohort domain/model/relationship redesign or broader course-platform implementation from Migrate edition-like Course records to reusable Course → Cohort #51.
  • No full production-like course data migration, route/API consumer migration, redirect/cutover, write-freeze/delta rehearsal, or release authorization from Preserve legacy course HTML/API compatibility and rehearse full data migration #60.
  • No content release digest migration or content_sync behavior from #219.
  • No destructive table recreation, DROP TABLE, TRUNCATE, flush, bulk delete/reinsert, manual django_migrations edits, or database dump restore as a compatibility strategy.
  • No production database, AWS console/API, protected export, credential, real learner data, email/provider/Datamailer dispatch, or worker side effect.
  • No dependency, CI workflow, Makefile, model, route, UI, API, schema-policy, or unrelated migration cleanup.
  • No claim that the failed run 32659885289 was fixed, no deployment success claim, and no issue closure before the named HUMAN gates pass.

Dependencies and blocked work

Acceptance criteria

  • Migration graph is explicit and compatible. The candidate documents the repaired graph, dependencies, replacement/bridge semantics, and current leaf. showmigrations/MigrationLoader finds no missing legacy dependency, duplicate leaf, or inconsistent applied-history state. No already-deployed migration identity is silently redefined as a different schema.
  • Fresh database. An isolated empty database migrates from zero to every leaf with no error. makemigrations --check --dry-run reports no model drift, and the resulting schema matches the current models and expected course table/constraint contract.
  • Populated legacy database. A deterministic fixture built from the exact 11b2bd1 legacy graph applies through courses.0041_courseregistrationcountsourcerun_and_more, inserts representative synthetic Course/enrollment/homework/project/review/score/statistics/certificate/registration data and relationships, then upgrades with the candidate. The run succeeds without InconsistentMigrationHistory, missing-table/column errors, guessed mappings, or destructive reset.
  • Data and provenance preservation. Before/after evidence compares table row counts, primary keys, foreign keys, representative deterministic row checksums, content types/permissions where touched, and the complete applied migration set. No existing course row is silently dropped, duplicated, renumbered, or recreated under a new identity.
  • Replay and retry are safe. Re-running the candidate migration on the upgraded populated database is a successful no-op: migrate --plan is empty at the leaf, row/checksum/migration-state snapshots are unchanged, no duplicate backfill occurs, and no email, provider, Datamailer, queue, or other external side effect is emitted. A failed/retried local migration either resumes deterministically or fails closed without partial destructive cleanup.
  • Historical migration isolation. Every data migration uses historical apps models, is backend-portable, has bounded transaction/atomic behavior, and passes the repository’s migration import/side-effect isolation checks. No migration imports mutable runtime services, reads secrets, reaches the network, or depends on current application code that can drift later.
  • Repository and regression verification. The engineer generates the versioned selective-verification plan from the frozen candidate, records exact identities/digests and all component dispositions, and leaves an uncommitted frozen worktree. The independent tester reruns the required focused migration/Django checks and make test-playwright-core; any render-impact classification triggers the required desktop/mobile screenshot gate, while a justified no-render classification is recorded as not_applicable per the process. No required component is partial or undocumented.
  • Browser/HTTP compatibility smoke. On the synthetic migrated state, representative public course list/detail and private course workflow/API smoke requests retain expected status, canonical/compatibility behavior, no migration traceback, and no accidental data exposure. Desktop/mobile browser evidence is required when the selective plan marks those routes render-impacting.
  • [HUMAN] Exact-image populated deployment. After commit, local merge, push, and a successful CI build, on-call deploys the exact immutable image/source identity to the authorized development environment and runs the migration task against an authorized populated development database copy initialized at the legacy boundary. Redacted evidence must include source SHA, image digest/version, migration task result, applied-history summary, release-controller result, and whether the prior release remained protected on failure. No production/AWS mutation is authorized by this issue’s engineering scope.
  • [HUMAN] Readiness, smoke, and failure classification. On-call verifies database-aware readiness, web/worker identity parity, representative course HTTP/browser smoke, migration-task logs, and the release artifact. If the task fails, the evidence must distinguish a graph/data-state defect from a runtime/infrastructure defect; a controller rejection or missing verdict is unresolved, not green. The prior release remains the rollback target and no destructive database rollback is attempted.
  • [HUMAN] CI/on-call lifecycle gate. The post-push CI run is observed by on-call to a terminal green result with no cancellation, missing required artifact, deployment contract contradiction, or unreviewed exception. The issue remains OPEN with human until these named gates are reported. If a technical commit is later authorized while the HUMAN gates remain, its body must use Refs #220, not Closes #220.

Required verification commands

Use the repository’s uv-backed commands only; all local migration databases and fixtures must be isolated under project-local .tmp/ or the deterministic test harness.

uv sync --locked
uv lock --check
make migrations-check
make test-migrations
make verification-quality
make test-django-full
make test-playwright-core
make verification-container
make verification-plan VERIFY_BASE_SHA=<frozen-base> VERIFY_HEAD_SHA=<frozen-head> VERIFY_ISSUE=220 VERIFY_WORKTREE=<frozen-worktree> VERIFY_CONSUMER=engineer VERIFY_PHASE=engineer
make verification-run VERIFY_ISSUE=220 VERIFY_WORKTREE=<frozen-worktree> VERIFY_CONSUMER=tester VERIFY_PHASE=tester
make verification-evidence-check VERIFY_ISSUE=220
make verification-report-check VERIFY_ISSUE=220 VERIFY_PHASE=tester

The handoff must include the exact commands actually run, terminal exit statuses, test counts, migration plan/output, redacted before/after snapshots, screenshot disposition/paths when applicable, and validated plan/evidence/report paths. make test-migrations and make migrations-check are required even if a broader target also invokes them. Do not run these commands against the shared development database.

Handoff and disposition

Engineering starts only after this issue is groomed and needs grooming is removed. The engineer must leave the candidate uncommitted and frozen for an independent tester; the tester must report every automated criterion separately; PM acceptance is a later lifecycle gate and is not granted by this issue rewrite. The current failed deployment evidence is preserved as the baseline incident and is not a success signal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingbugSomething isn't workingcoursesArea: coursesdata-migrationArea: data-migrationhumanCode complete; manual verification requiredoperationsArea: operationstestingArea: testing

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions