Skip to content

Production hosting, DNS and content migration — tracking checklist #309

Description

@alexeygrigorev

Tracking issue for the production hosting, DNS and content migration. The authority is the runbook — _docs/runbooks/production-hosting-and-dns-migration.md. This issue is the tick-off surface; the runbook holds the detail, the rationale and the verification for every line below. Section references (§) point into it.

What this migration does. datatalks.club moves off GitHub Pages and courses.datatalks.club moves off CMP, both onto this Django deployment, with DNS moving from GoDaddy to Route 53 and email moving to Relay in the main AWS account.

Markers. [P] needs production AWS credentials (387546586013) · [G] needs GoDaddy console access · [OW] one-way or expensive to reverse · [W:x] hard wait of duration x · [B:x] blocked on x.

Lanes. MAIN is the apex/DNS critical path. COURSES, EMAIL, DEV and CLEANUP run in parallel with MAIN and with each other — only the order within a lane is strict, plus the named cross-lane joins.

Companion design documents (linked from the runbook, not restated in it): _docs/design/specs/data-migration-architecture.md · _docs/design/specs/unit-content-pipeline.md · _docs/design/specs/script-inventory.md · the costed Terraform design for website prod/dev + Relay prod/dev, currently at .tmp/infra-design-dev-prod-relay.md and outside version control (D36).


Decisions

Resolved (§3.1):

  • D1 — two-stage apex swap: apex serves the static site from S3 first, then swaps to the Django site
  • D2 — minimal shared dev.; the sandbox account is retired
  • D3 — two distinct Lambdas on courses. (maintenance, then redirect)
  • D13 — Relay is the sender, not Datamailer
  • D18 — one curriculum layout: cohorts/<year>/<module>/module.yaml. llm-zoomcamp already conforms (7 modules); machine-learning-zoomcamp (9) and ai-dev-tools-zoomcamp (4) move. ⚠️ This reverses the earlier advice to revert llm-zoomcamp@c04db93 (_docs/runbooks/session-handoff-20260902.md:53-55) — under D18 that commit is correct and llm-zoomcamp/cohorts/README.md is what is wrong. No public URL moves.
  • D19 — modules-format rollout covers every course except stock-markets-analytics-zoomcamp, which stays legacy for 2026. data-engineering-zoomcamp is a candidate to assess, not a commitment.
  • D20 — convention over configuration: unit/page structure is consistent across courses and derived by convention, not declared per course
  • D21DataTalksClub/zoomcamp-template owns that convention (STRUCTURE.md, docs/conventions.md, templates/{root,module,cohort}-README.md). Any layout change includes that repository.
  • D22dev.datatalks.club carries fake data, never production-derived. Dev Relay therefore never sees a real address, which is what makes the shared dev database low-stakes.
  • D23 — Datamailer is the source of Relay's suppression state, and copying it is a precondition of the first production send, not Phase 7 cleanup. Consequence: sandbox Datamailer must survive until that copy is done.
  • D24 — no homework slug rewrites; the ^hw(\d+)$homework-0N transform is removed, explicit overrides only
  • D25 — six cohorts stay skipped: ai-bootcamp-2025, ai-hero-2025, ai-hero-2026, sma-zoomcamp-2026, ai-buildcamp-2, ai-buildcamp-3. Four need only a mapping entry; ai-buildcamp-2/-3 carry edition numbers, not years, which the family+year model cannot express — that pair needs design.
  • D26 — the cost bar is website-only. Website (prod + dev) ≤ CMP; Relay is a separate budget.
  • D27 — the SEO impact of the apex /docs/, /faq/, /podwiki/ redirect is accepted; no action (also recorded at C3/C4/C5 below)

Open (§3.2) — each one gates the steps that cite it:

  • D4 — cache freshness strategy (still unresolved, and it gates the apex swap — see the Phase 5 note)
  • D5 — media key scheme
  • D6 — zone root placement
  • D7 — registrar transfer (deferred)
  • D8 — courses-zone fold-in
  • D9 — RDS Multi-AZ timing — gates 4.1; single-AZ is what the $169.98 Budget A assumes
  • D10 — SPF strategy
  • D11 — git history rewrite
  • D12 — direct-sync source manifest
  • D14 — SES identity owner ⚠️ on the EMAIL critical path
  • D15 — Relay sandbox data (clean start recommended; superseded in part by D23 for suppression state)
  • D16 — Luma-exporter home
  • D17 — Luma refresh owner and cadence
  • D28SITE.md rollout shape. Two incompatible implementations in flight: one committed to local main in three repos (short copy, deletes description_path), one as six PRs from origin/main (551–635-char copy from content/docs_projection.json, repoints description_path). The website side has already landed and decides part of it — course.yaml no longer accepts description_path at all, so the repointing lane would be rejected with unknown_key. Recommended default: the first lane's convention with the second lane's copy.
  • D29 — dev-database placement: dedicated db.t4g.micro (≈$15.68/mo) or logical databases on the production instance ($0). Recommended: dedicated.
  • D30 — Relay production database size: db.t4g.medium (≈$52.56) or db.t4g.small (≈$26.28). Recommended: medium.
  • D31 — real transactional email volume, and the real newsletter cadence (weekly vs the 5–7/month stated to AWS)
  • D32 — NAT gateway (≈$41.09) or NAT instance (≈$7.46). Recommended: gateway.
  • D33 — when main/dtc-website joins the policy scan (adding it converts an existing grant into a violation). Recommended: after the apex swap, sequenced with D4.
  • D34 — extract modules/relay-host or duplicate two Relay roots. Recommended: extract.
  • D35 — retire the main/common bastion in favour of SSM Session Manager (−$6.70/mo, removes a 24/7 SSH-exposed host)
  • D36 — give the costed infrastructure design a home in version control

Phase 0 — Inventories and prerequisites

MAIN · read-only · §5

  • 0.1 [G] Export the complete GoDaddy zone, every record, including a _domainkey sweep
  • 0.2 [G] Record the www and join forwarding configurations
  • 0.3 [P] Pull the 3-month Cost Explorer baseline
  • 0.4 [P] Identify and record the courses hosted zone (Z00653771…)
  • 0.5 Owner settles open decisions D4–D10
  • 0.6 [P] Record the live SES sending quota and maximum send rate — same step as E.1. Production access already exists and the quota has been raised above the default, so this is a verification step, not a blocker. Two numbers, not one: Max24HourSend must clear ~130,000 recipients in a day alongside transactional traffic, and MaxSendRate decides whether a 130,000 campaign takes ~2.6 h (at 14/s) or ~11 min (at 200/s). If either falls short, the increase is a support request with multi-day lead time and should be raised at once.
  • 0.7 Re-verify compatibility artifacts (make compatibility-artifacts-check)

Phase 1 — DNS hosting moves to Route 53

MAIN · §6

  • 1.1 [P] [B:D6,D10] Create the main/dns zone; byte-copy every record from 0.1 — ⚠️ written, in main/dtc-website/dns.tf rather than a separate main/dns root (304189f, 73d3364). Note the zone was created while D6 is still open, and the apex record still points at GitHub Pages rather than the new distribution. Apply status not determinable from the repo.
  • 1.2 [P] join replacement: API Gateway custom domain, with the ACM CNAME added at GoDaddy now
  • 1.3 www strategy — build the Phase 2 distribution first (recommended)
  • 1.4 Pre-delegation gate: dns-parity-check green, GoDaddy TTLs lowered [W:48h]
  • 1.5 [G] [OW-ish] NS cutover at the registrar; record freeze; mail checks at +1 h and +24 h [W:48h]
  • 1.6 Update stale DNS claims in the aws-infra docs

Phase 2 — Apex to S3/CloudFront (stage 1 of the D1 swap)

MAIN · needs Phase 1 · §7 · ✅ EXECUTED 2026-09-02, with a design change

Only the main site tree was rehosted into S3. /docs/, /faq/ and /podwiki/ were not rebuilt — a CloudFront viewer-request Function now 302s them to datatalksclub.github.io. Verified live: x-cache: FunctionGeneratedResponse from cloudfront.

Tree source_id Baseline rows Now
Main site dtc-main-site 2,301 Served from S3 (200)
Docs dtc-docs 174 302 → github.io
FAQ dtc-faq 152 302 → github.io
Podwiki dtc-podwiki 310 302 → github.io

636 rows — 21.7% of the compatibility contract — now redirect instead of returning 200.

  • 2.1 Rebuild the pinned legacy trees — done for dtc-main-site; the other three are no longer rebuilt at all
  • 2.2 [P] Bucket + OAC + resolver function + distribution — landed in main/dtc-website/legacy_static_site.tf (038d4c3, 3d66411, 580ef4b) rather than a separate main/legacy-site root. ⚠️ That merge complicates step 7.3, which assumes the legacy site can be retired independently.
  • 2.3 [P] Upload the main tree
  • 2.5 [P] Apex alias swap — done, apex now answers from CloudFront/S3

Follow-ups this created

  • C1 — the compatibility contract no longer describes reality. All 636 redirected rows carry classification: "preserve" / expected_status: 200. The baseline schema hard-codes const: "preserve", so a redirect cannot be expressed in it — only through the digest-bound approved-expectations sidecar. Until that is recorded, the 2.4 parity crawl reports 636 regressions and cannot function as a gate — not for this phase, and not for the 9.4 stage-2 swap that depends on it. Highest priority in the phase.
  • 2.4 [B:C1] Staging parity crawl — rescope to the 2,301 S3-served rows plus redirect assertions for the other 636
  • C2 — GitHub Pages is now a permanent production dependency, not a rollback net. docs, faq and podwiki must stay live and maintained indefinitely — the apex holds no copy of their content. This invalidates 2.6 ("frozen for ≥30 days") and 7.2 ("retire the four Pages deployments") for three of the four.
  • C3/C4/C5 — SEO impact accepted; no action (D27). Owner decision, 2026-09-02: these pages are not meaningfully indexed, so the redirect's search consequences do not need mitigating. This closes three findings together — the missing rel=canonical on the FAQ tree (152 rows; docs and podwiki do carry one), the 302-vs-301 question, and the apex robots.txt still advertising Sitemap: https://datatalks.club/podwiki/sitemap.xml which now redirects off-domain. Recorded so they are not re-raised. Note this does not touch C1 or C2, which are contract and availability problems rather than search problems.
  • C6 — Redirect legacy /podwiki/* URLs to the canonical /wiki/* routes #306 is superseded. It records that 310 /podwiki/* URLs would 404 at cutover; they redirect now. Close or rewrite it rather than leaving it describing a condition that no longer exists.
  • C7 — the redirect must survive the stage-2 swap. When the apex alias moves to the Django site (4.4), these 636 paths break unless the new distribution carries the same Function or Django answers them itself. Added to the 4.4 gate below.
  • 2.6 Keep the main-site Pages deployment live and frozen ≥30 days as the rollback target

Phase 3 — Media to S3

Independent of MAIN · #301 · §8

  • 3.1 [P] Land aws-infra PR Copy the existing course platform verbatim and establish its characterization baseline #30; apply main/dtc-website (bucket and upload role only)
  • 3.2 Groom and implement Serve public projection media from object storage instead of the git-tracked tree #301 — projection integrity without the git-tracked tree
  • 3.2a Resolve the confirmed orphan media file (1,254 files against 1,253 records) — identified as images/podcast/s24e06-how-to-build-ai-that-actually-ships-in-production.jpg, unrecorded on disk and correctly absent from the bucket; still needs a decision on whether to record it or delete it
  • 3.3 [P] media-sync initial upload — done; all 1,253 objects are present in s3://dtc-website-media/
  • 3.4 [P] media-verify: 1,253/1,253 digest matches plus a two-way set reconciliation
    • 1,253/1,253 verified by fetching every recorded key through CloudFront and re-digesting the bytes — zero unreachable, zero digest mismatches, zero content-type mismatches. Both space-containing keys return 200.
    • two-way set reconciliation — needs s3:ListBucket, so it cannot be done anonymously; there is currently no proof the bucket holds no extra objects
    • idempotency unproven by execution — a second media-sync run should report 0 uploads, but that is inferred from reading content/media_tooling.py:285, not observed
  • 3.5 Wire the media store into the deployed task definitions (57fc3ae); the /images/* CloudFront behavior itself still waits on the Phase 4 distribution

Phase 4 — Production stack, prod. and dev., stage-2 swap

MAIN + DEV · §9

  • 4.0 [P] Policy-suite amendment first — credential-free and the longest pole. Three fail-closed blockers, all verified in ~/git/aws-infra (§14.6):

    • SOURCE_DIRECTORY_SCOPES is a literal four-element tuple (tests/policy/safe_repository_inputs.py:16-20) and contains none of the new roots; adding one drags in EXPECTED_TERRAFORM_CONFIG_FILES, the source manifest, and the pinned test counts
    • a production root under the DataTalksClub owner cannot pass OIDC004/OIDC005 — both force exact sandbox claim values today
    • aws_iam_instance_profile has zero permitted owners, so Relay cannot enter scope at all
    • (D33) adding main/dtc-website converts its existing cloudfront:CreateInvalidation grant (legacy_static_site.tf:459-469) into a violation — defer until after the apex swap
    • while in there: main/maintenance-page has no enable_deployment gate, no backend block, no allowed_account_ids, no tags — an apply at defaults stands up a live public HTTP API
  • 4.1 [P] [B:D9] Production website root — module instantiation, with policy-suite churn budgeted (§9.1). Root naming still open between main/dtc-website and main/website; the costed design recommends main/website.

  • 4.2 [P] Minimal dev.datatalks.club on shared production infrastructure; tradeoffs accepted under D2 (§9.2) and made low-stakes by D22 (fake data)

  • 4.3 Staging discipline while prod. and the apex both serve: four duplicate-content controls, prod. noindexed (§9.3)

  • 4.4 [P] [OW-SEO] Stage-2 apex swap (§9.4), after a six-item gate:

    • parity on prod. against the live apex
    • M8 steps 1–4 complete
    • caching evidence captured
    • rollback rehearsed — including the three-step alias procedure below
    • data freshness: sources re-synced within 72 h and at least N future-dated events (§13.8)
    • full-fidelity CMP import proven against a disposable target (§13.9) — ⚠️ currently unbuilt
    • the /docs/, /faq/, /podwiki/ → github.io redirect survives the swap (C7) — either the new distribution carries the same CloudFront Function, or Django answers those 636 paths itself

    ⚠️ The DNS change alone is not sufficient. CloudFront refuses an alias another distribution already claims, and both apex names are claimed by the legacy distribution (legacy_static_site.tf:269) — a second distribution gets CNAMEAlreadyExists. Order: aws cloudfront associate-alias onto the new distribution → flip the Route 53 aliases → then remove the names from the legacy distribution's aliases. Doing the last step first turns a rehearsed swap into an outage. Alias rollback is 1–10 min; keep the S3 legacy site warm ≥30 days.

Phase 5 — Anonymous edge caching

MAIN · must be proven before 4.4 · §10

  • 5.i [website] Route-cache registry, cache-class headers, and no-Set-Cookie/CSRF tests
  • 5.ii [P] [aws-infra] Per-class edge policies, anonymous classifier, response guard
  • 5.iii [B:D4] Reviewed narrow invalidation grant for the worker role — after or alongside the direct-sync work

⚠️ §10.4 records a hard contradiction between the owner's "update the cache on updates" requirement and the standing ban on cloudfront:CreateInvalidation. Still unresolved, and it gates the apex swap. It needs resolving, not working around — and it is entangled with D33, because main/dtc-website already holds a scoped invalidation grant that only stays legal while that root is outside the scan.

Phase 6 — courses.datatalks.club, two Lambdas

COURSES lane · fully parallel, the zone is already on Route 53 · §11

  • 6a.1 Fix the maintenance Lambda: 200 → 503 with Retry-After; track the root, add a backend — the main/maintenance-page root now exists (fcdcdbf) but still returns 200 (lambda/index.js:433-439), and 🚨 unlike main/courses-redirect it has no enable_deployment gate at all — every resource is unconditional, so an apply today creates live resources. "Inactive" here means only that nobody has applied it.
  • 6a.2 [P] Deploy dark with a certificate; rehearse the alias swap on dev.courses.datatalks.club
  • 6a.3 [P] Activate during the M8 freeze (rollback: revert the alias, rehearsed)
  • 6A′ [P] Compatibility views on the new stack — APIs must never cross-host redirect
  • 6c.1 [P] main/courses-redirect: generated path map, 301 on GET/HEAD, gated 308 — root written (fcdcdbf) and genuinely inert: every resource carries count = var.enable_deployment ? 1 : 0, defaulting to false
  • 6c.2 [P] [B:consumer gate] Redirect Lambda live; CMP infrastructure retained through its window

Phase E — Email via Relay

EMAIL lane · parallel, does not wait for DNS · §11E

Volume, and what it changes. 130,000 newsletter recipients weekly plus transactional ≈ 563,000 messages/month. Consequences already folded into the runbook: SES postage alone is ≈$56/mo (≈$56–91 depending on the real cadence — D31); delivery history grows 0.65–1.04 GB per weekly campaign, 34–54 GB/year unpruned; and §14's old "$15–18/mo for all of main/relay" was wrong by roughly — Relay production is ≈$144/mo.

  • E.1 [P] SES quota and send rate recorded — the same step as 0.6. Not a blocker: production access exists (aws-infra/docs/aws-support/2026-08-09-ses-newsletter-quota-increase.md:19) and the quota is raised. That document records the 2026-08-09 state (50,000/24 h, 14/s) and two open increase requests to 200,000/24 h and 200/s; its own follow-up to record the approved values was never done. Do it here.
  • E.3 [P] [B:D14 hand-off] main/relay root:
    • bind to the datatalks.club identity and a Relay-owned configuration set
    • outbound only — inbound stays unconfigured, which is the key mitigation from the code audit
    • --memory limits set for the 1 GiB host
    • managed RDS db.t4g.medium, single-AZ, gp3 50 → 500 GB (D30) — not PostgreSQL on the host as sandbox does
    • retention/pruning job shipped in the same changeprune_db_task_results exists and has never been invoked; RDS storage autoscaling is one-way, so this is a go-live prerequisite, not an open milestone
    • backups and one rehearsed restore are part of done. Note C3 ("no backup") is not a defect — Relay has never had a client, the website is its first, so backups/pruning/volume monitoring are capabilities that were never required and now become requirements
    • observability on the django-website injected-alarm contract, decided before the Terraform is written
    • deploy dark
  • E.4 Clean-start the data volume (D15); the sandbox Relay stays on as the dev Relay
  • E.4a Copy Datamailer's suppression and unsubscribe state into Relay (D23) — a precondition of the first production send, before E.6 stage 1. An empty suppression list re-mails exactly the addresses that already damaged the sending reputation. Fallback if the export is impractical: seed SES account-level suppression. Keeps sandbox Datamailer alive until verified.
  • E.5 GATE — both assessments delivered. [B:remediation] — the code-audit verdict is "not ready to send production email":
    • E.5a minimum fix list C1–C4 plus H2 and H5, via groomed issues
    • E.5b observability items 1–9 (items 1 and 1b are under a day's work and get detection to 15 minutes)
  • E.6 [B:E.5a] Ramp: a stage −1 shadow week running dry_run at volume, then stages 0–4. What constrains the ramp is deliverability evidence — bounce and complaint rates — not sending capacity. Hard preconditions: the canary and outbox_* alarms re-pointed at Relay with a live subscriber proven; suppression state copied (E.4a) before stage 1; pruning proven before stage 4. Abort thresholds: 3% bounce, 0.08% complaint.
  • E.7 Datamailer stays read-only; fixes the 7.4 teardown ordering trap

Phase 7 — Decommission and cost harvest

CLEANUP · after every rollback window has closed · §12

  • 7.0 [P] SES identity state mv out of main/cmp — execute early, together with E.3
  • 7.0a [P] Rehome the email canary (health probe and alarms) out of main/cmp into main/relay
  • 7.1 [P] [OW] Destroy main/cmp — the final Aurora snapshot is the point of no return
  • 7.2 [OW] Retire the GitHub Pages deployments — ⚠️ only the main-site one. docs, faq and podwiki are now permanent redirect targets (C2) and must stay live and maintained.
  • 7.3 Retire the legacy static site after the 4.4 window closes — ⚠️ it lives inside main/dtc-website/ rather than its own root, so this is surgery on a shared root, not a clean destroy
  • 7.4 Sandbox preconditions, then teardown in the corrected order: sandbox Relay → sandbox/website → CMP (7.1) → sandbox Datamailer. Datamailer retires last, held alive by three independent dependencies: sandbox Relay sends through its configuration set; CMP production is its client until 7.1; and the D23 suppression copy must complete first. The old ordering counted its ≈$13–15/mo saving too early.
  • 7.5 [P] Remove the cross-account media-bucket read granted by PR Copy the existing course platform verbatim and establish its characterization baseline #30
  • 7.6 [P] [OW] Destroy sandbox/website; retire its OIDC pipeline and policy fixtures
  • 7.7 [P] Cost Explorer harvest against §14 — expected ≈$270–340/mo freed. ⚠️ main/common is not in the harvest: main/aisl runs in its VPC and needs its NAT, so that NAT can never be retired.

Cost — two budgets (D26)

§14 · all figures estimates; Phase 0.3's Cost Explorer pull is the authority for actuals

Scope Bar Est. $/mo Verdict
Budget A website prod + dev + shared plumbing + the three small Lambdas ≤ CMP's $180–225 169.98 PASS
Budget B Relay prod + dev judged separately 155.48 40% is SES postage
Combined the actual invoice 325.46 vs ≈$193–240 today
  • Confirm Budget A's assumptions hold at implementation: single-AZ RDS (D9), dedicated dev database (D29), NAT gateway (D32), Container Insights ON
  • Record the transitional peak in the budget alarm: ≈**$597–665/mo** while everything runs at once, ≈$305/mo above steady state. Compressing the overlap is worth more than any single sizing decision.

Seven corrections the costed design found in §14, all now folded in:

  • Relay priced at $15–18/mo → ≈$144 for Relay production alone
  • "Turn Container Insights off" would put web_running_tasks and worker_running_tasks into permanent ALARM (modules/django-website/observability.tf:27-78, treat_missing_data = "breaching") — withdrawn
  • The cited "free S3 gateway endpoint in the module" does not existmodules/django-website has no VPC endpoint at all, so a new VPC ships with the July 2026 NAT-data failure mode un-fixed unless the endpoint is written at root level
  • A new NAT does not replace an existing cost — main/aisl runs in main/common's VPC and needs its NAT
  • Aurora Serverless v2 re-examined at a 0-ACU floor and still rejected — the dev readiness probe prevents the pause, so it bills ≈$43.80/mo
  • §9.4's apex swap was incomplete — aws cloudfront associate-alias is required
  • §7.4's teardown order understated the transitional bill

Content ingestion

Runs in parallel with its own gates · §13

  • 13.3 Direct-sync programme — in flight, all gates closed
  • 13.4 Ongoing update path: target state plus its approval and rollback story
  • 13.7 Settle the genuinely undecided ingestion items
  • 13.8 Events: the Luma data dump, the freshness gate, and the PII boundary
  • 13.9 Full-fidelity CMP database import — two modes specified, only one built
    • Data sources pinned correctly: the production export is the newest /data/tmp/rds-export/rds-prod-*.db, produced nightly by cron (six rotations on disk — always take the newest). course-management-platform/db/db.sqlite3 is CMP's dev database and must not be used for schema mapping.
    • Real counts recorded: courses_course 21, courses_project 52, courses_question 616, courses_homework 128; 20,009 accounts / 20,907 enrollments / 36,547 submissions
    • ⚠️ Schema drift is a real blocker with no escape hatch. The three post-pin tables are not empty in production — courses_emailcampaign 1, courses_systemprojectevaluation 1, courses_systemevaluationcriteriaresponse 11 — so make review-data's fail-closed refusal cannot be waved away as "they're empty anyway". Adopt the upstream migrations or re-pin the adoption; no hand-editing dumps. Re-count at freeze time.
    • ⚠️ Project Attempt N is real production copy, not placeholder junk — 31 of 52 courses_project rows, with a public FAQ entry explaining it (content/faq_projection.json:310). The real placeholder marker is the description prefix Production-like generated (courses/services/local_course_seed.py:362), which appears zero times in production. Filter on that, never on the title.
  • 13.10 Course-repository curriculum layout and the unit content pipeline (D18–D21, D24, D25) — detail in _docs/design/specs/unit-content-pipeline.md
    • Move machine-learning-zoomcamp's 9 and ai-dev-tools-zoomcamp's 4 module directories under cohorts/2026/, repairing in the same commit: nine homework.md collisions, 102 relative image references, 103 ](../) back-links
    • Rewrite llm-zoomcamp/cohorts/README.md, which documents the opposite convention
    • Carry the change into DataTalksClub/zoomcamp-template (D21)
    • Delete the ML homework.md module-directory fallback heuristic (unit_links.py:168-171) with the move, not before
    • Fix the 4 ai-dev-tools lesson bodies that open # Module N — <Title> while their unit title is <Title> — a content fix in the course repo; the stripper is correct to leave them

Content-pipeline fixes already landed (aed2497, c928f37, fbab381, 2c97886, 1b1a49f, 1b8352a):

  • Every repository-relative link a lesson writes now resolves — 272 relative non-.md links (45 + 219 + 8), 0 unresolved. Independently re-measured; a "288" figure that circulated is not reproducible under this definition.
  • Duplicate title heading removed — 103 of 181 units were repeating their title; removals go from 74 to 177 of 181, the 4 remaining being the ai-dev-tools lessons above
  • _VIDEO_LINE_RE deleted — it matched 0 of 181 units
  • ⚠️ New columns video_url, code_sources (migration 0053) and instructions_source_path (0054) are empty until a reimport, which needs a fresh commit or a cleared run row
  • The "Edit on GitHub" 404 mechanism is branch-path absence, not commit-SHA resolutionUnitRepository.edit_url() builds {base}/edit/{branch}/{source_path} (courses/services/unit_assets.py:63-70); no commit SHA appears in any public affordance. The fbab381 guard is still right and stays.

Reference layers

Not steps, but the analysis the steps depend on: §14 cost and security (two budgets) · §14.6 the policy-suite blockers · §15 migration-script specifications · §16 risk register · §17 what this runbook does not cover · Appendix A DNS parity worksheet.


Known blockers outside this checklist

  • Push llm-zoomcamp@c04db93 and machine-learning-zoomcamp@1aa481edone. Both are now on their repositories' origin/main (944b35e, 61fdaab). Under D18, c04db93 is the correct layout and ships.
  • Push the newer SITE.md commits, which are still local-only in all three repositories — and settle D28 first, since origin/main's course.yaml still carries description_path, which the parser now rejects as an unknown key
  • Get consent from the people quoted on the homepage.
  • No way to generate article, podcast and event assets in the current style #311 — a repeatable way to generate article, podcast and event assets in the current design style. Courses have one (_docs/design/illustration-assets.md); the three editorial content types do not, and 213 podcast covers are being regenerated.
  • Decide the eight external link literals gating the events refresh
  • Author or generate the speaker-bio normalization rows for the nine new events
  • Decide whether the accent-border ban is global (it is enforced in practice but written down nowhere)
  • Redirect legacy /podwiki/* URLs to the canonical /wiki/* routes #306 — 310 legacy /podwiki/* URLs would 404 at cutover — superseded, they now 302 to github.io (see C6)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions