Skip to content

Commit c6967be

Browse files
tyler-daneclaude
andauthored
feat(cutover): open prod gate, fix backfill string-_id crash, add execution plan (#2144)
The founder closed the staging gate on 2026-07-15 (acceptance validated on staging; S3 rollback rehearsal explicitly waived — waiver recorded in the runbook). A dry-run rehearsal of the full migration sequence against a restored copy of production data found: - the 2025.10.18 prototype pair crashes on prod data under current code (ledger-mark decision recorded in the execution plan); - the event backfill's series-base scan crashed on legacy rows with string _ids (1,712 on prod) — fixed here with a regression test; - 168,015 Google-linked rows fail the strict contract (data-policy deletion approved by the founder; recovery via sync-token reset + forced re-import). With the fix and pre-clean, the full sequence converges: backfill verification passed, repairs green, strict-validator checks clean, ledger matches staging (12 records). Sensitive infra details live in the operator's private notes outside git. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7d1854f commit c6967be

6 files changed

Lines changed: 432 additions & 76 deletions

File tree

packages/scripts/src/migrations/2026.07.10T21.30.00.event-record-backfill.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,39 @@ describe("2026.07.10T21.30.00.event-record-backfill", () => {
299299
);
300300
});
301301

302+
it("migrates a series whose legacy base has a string _id (anomalous prod rows)", async () => {
303+
const user = await UserDriver.createUser({ withGoogle: false });
304+
await insertLocalCalendar(user._id);
305+
const userIdHex = user._id.toHexString();
306+
307+
// Some production rows carry a hex string _id instead of an ObjectId;
308+
// the transform accepts both, and the series-base scan must too.
309+
const stringBaseId = new ObjectId().toHexString();
310+
const seriesBase = legacyEvent(userIdHex, {
311+
_id: stringBaseId,
312+
title: "String-id series base",
313+
recurrence: { rule: ["FREQ=WEEKLY;COUNT=3"] },
314+
});
315+
const occurrence = legacyEvent(userIdHex, {
316+
title: "Occurrence of string-id base",
317+
recurrence: { eventId: stringBaseId },
318+
});
319+
320+
await legacyCollection().insertMany([seriesBase, occurrence]);
321+
322+
await migration.up(migrationContext);
323+
324+
const docs = await destinationCollection().find({}).toArray();
325+
expect(docs).toHaveLength(2);
326+
const base = docs.find((d) => d["recurrence"]?.["kind"] === "series");
327+
const occ = docs.find((d) => d["recurrence"]?.["kind"] === "occurrence");
328+
expect(base?.["_id"]).toBeInstanceOf(ObjectId);
329+
expect((base?.["_id"] as ObjectId).toHexString()).toBe(stringBaseId);
330+
expect(
331+
(occ?.["recurrence"]?.["seriesId"] as ObjectId).toHexString(),
332+
).toBe(stringBaseId);
333+
});
334+
302335
it("excludes a mix of someday events, migrates timed/allDay, reports the excluded count, and verifies cleanly", async () => {
303336
// Regression for the Someday-removal cutover: legacy someday events are
304337
// counted and dropped rather than migrated, the timed/allDay events for

packages/scripts/src/migrations/2026.07.10T21.30.00.event-record-backfill.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ export default class Migration implements RunnableMigration<MigrationContext> {
154154
{ projection: { _id: 1 }, batchSize: MONGO_BATCH_SIZE },
155155
);
156156
for await (const base of baseCursor) {
157-
baseIds.add((base._id as unknown as ObjectId).toHexString());
157+
// Legacy rows may carry a string _id (hex) instead of an ObjectId --
158+
// the transform tolerates both, so this scan must too.
159+
const baseId = base._id as unknown as ObjectId | string;
160+
baseIds.add(typeof baseId === "string" ? baseId : baseId.toHexString());
158161
}
159162
legacyBaseIdsByUser.set(userIdHex, baseIds);
160163

team/architect/google-subcalendar-big-bang-runbook.md

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Google sub-calendar big-bang deployment runbook
22

3-
Status: **staging validator recovery and full-sync acceptance are complete;
4-
production is locked**.
3+
Status: **staging complete; production gate opened by the founder on
4+
2026-07-15**. Execution of Phase P is planned and logged in
5+
[`prod-cutover-execution-2026-07-15.md`](./prod-cutover-execution-2026-07-15.md);
6+
the operator gate is [`prod-go-no-go-checklist.md`](./prod-go-no-go-checklist.md).
57

68
This is the master operator runbook for landing the work from
79
[`google-subcalendar-project`](../archive/google-subcalendar-project/master-doc.md)
@@ -199,11 +201,18 @@ content or credentials.
199201
| Backup restore checked | 2026-07-14: 103,940 documents restored to scratch DB; 0 failures | 2026-07-14: 17 documents restored to scratch DB; 0 failures |
200202
| Validator repair migrated | 2026-07-14, `v1.0.207`: 12 ledger records; strict validator valid; `priority` absent; 50,396 active rows | 2026-07-14, `v1.0.207`: 12 ledger records; strict validator valid; `priority` absent; 0 active rows |
201203
| Google sync code `121` absent | 2026-07-14, `v1.0.211`: designated account full resync passed with 0 invalid rows and no code `121` | Not applicable: no Google account is configured on this target; service health and logs passed |
202-
| 12-step acceptance passed | pending | pending |
203-
| Rollback and second forward run passed | pending | pending |
204-
| Operator/date/release tag | Codex / 2026-07-14 / `v1.0.211` | Codex / 2026-07-14 / `v1.0.211` |
205-
206-
Production remains locked while any cell is pending.
204+
| 12-step acceptance passed | 2026-07-15: founder-validated on staging (running `v1.0.235`) through real use of the signed-in app, including Google multi-calendar sync | Not applicable: no Google account on this target; service health passed |
205+
| Rollback and second forward run passed | **Waived by founder, 2026-07-15** — see waiver below | **Waived by founder, 2026-07-15** — see waiver below |
206+
| Operator/date/release tag | Codex / 2026-07-14 / `v1.0.211`; founder / 2026-07-15 / `v1.0.235` | Codex / 2026-07-14 / `v1.0.211` |
207+
208+
**Founder waiver (2026-07-15):** the Phase S3 staging rollback + second-forward-run
209+
rehearsal is waived. Rationale: the forward path was effectively rehearsed twice on
210+
staging during the `v1.0.205``v1.0.207` recovery (both targets converged cleanly on
211+
the second forward run), and staging has run the cut-over schema in real use since
212+
2026-07-14. The rollback path (backup restore + reverse rename) therefore remains
213+
**untested end-to-end**; this is an accepted risk, compensated by treating the
214+
production abort rules as hard stops and by the pre-window dry-run rehearsal of the
215+
full forward sequence on a restored copy of production data (see the execution plan).
207216

208217
### Recorded staging rehearsal — 2026-07-14
209218

@@ -229,13 +238,16 @@ entries after the acceptance start time. Self-hosted staging has no configured
229238
Google account, so direct Google acceptance there is not applicable; do not
230239
misread that as evidence for a production Google sync.
231240

232-
Production remains locked because the complete packet `09` manual acceptance,
233-
watch-renewal soak, and rollback/second-forward rehearsal are still pending.
234-
Release `v1.0.205` must not be used for another migration attempt.
241+
The gate closed on 2026-07-15: the founder validated acceptance on staging
242+
(`v1.0.235`) and waived the rollback/second-forward rehearsal (see the waiver in
243+
the evidence table). Release `v1.0.205` must not be used for another migration
244+
attempt.
235245

236-
## Future Phase P — production big-bang cutover
246+
## Phase P — production big-bang cutover
237247

238-
This section is a reference plan, not current authorization.
248+
Authorized by the founder on 2026-07-15. The operator-facing execution plan,
249+
including production-specific preflight findings and the data-policy pre-clean,
250+
is [`prod-cutover-execution-2026-07-15.md`](./prod-cutover-execution-2026-07-15.md).
239251

240252
### Production go/no-go
241253

@@ -284,9 +296,10 @@ bullets below are its summary.
284296
7. Deploy the selected tag with **Deploy production**, resume service, and run
285297
the Phase S2 smoke plus production health checks.
286298

287-
If the installed Umzug CLI does not accept `up --to`, stop during rehearsal and
288-
resolve the exact supported syntax. Do not replace the boundary with an
289-
unbounded pre-rename `migrate up`.
299+
Resolved 2026-07-15: the CLI is Umzug `3.8.2`'s standard CLI and `up --to <name>`
300+
is supported (applies every pending migration up to and including the named one).
301+
Verified against production with `migrate pending`. Do not replace the boundary
302+
with an unbounded pre-rename `migrate up`.
290303

291304
### Production rollback
292305

0 commit comments

Comments
 (0)