Every PR merged since we moved CI to our own machines (10,766 of them, Sep 15 → Oct 29): what happened to speed and cost, an honest accounting of splitting the app, and the moves that reach the targets.
Approved 2026-10-29 (Fede): C plus dev tools out. All merged by Oct 30 14:42 UTC; 48-hour watch to Oct 31: rollout log. Tests-at-open reverted Oct 30. #23884 and #23886 live, two arm-run cuts in build; marketing-site repo built and pixel-identical, cutover pending Fede (Oct 30 23:10Z). · Proposed 2026-10-29 · a decision page under the CI velocity plan · targets from today's team call: open→live 60 min, merge→live 30 min, an emergency lane · recomputed from GitHub and the AWS bill with the factory's own formulas (the factory page was not readable from the session)
We got cheaper, not faster. A PR takes about 2 hours from open to live (was 1 hour on GitHub's machines) and 54 minutes from merge to live (was 14–22). Most is configured waiting: a hidden 30-minute deploy window, a 12-minute release spacing, tests that wait for arming, and a merge queue that re-tests every PR alone: 2,372 runs in two weeks, 54 reds, none of them needing a code change. Splitting the app saves 1–3 minutes per merge; it is not the lever. Deleting waits and double work is.
Order chosen by how easily each change is undone, not by size of win. Baseline for every before/after: week of Oct 26, open→live 125 min, merge→live 54, 2% live within an hour, about $75 of CI a day. Reports every 8 hours in #spending, three lines each.
| When (UTC) | Change | How | Undo | Status |
|---|---|---|---|---|
| Oct 30 04:14 | Production deploy window 30 → 5 min | repo variable | one line, in the ledger | live; Smith no objection; reverses Gera's Oct 11 call on Fede's go; keep/30 decided at hour 48 on the Vercel delta |
| Oct 30 04:52 | Batch-train parking window 30 → 5 min (trains live since Oct 20 park armed green PRs to share one queue run; the page had missed it) | repo variable | one line, in the ledger | live |
| Oct 30 05:25 | Batch trains drained (CI_BATCH_TRAINS=drain, variables deleted after one tick) | repo variable | set it back to live | live; judge: 0 of 33 PRs rode a batch since Oct 28, +18 min median per PR |
| Oct 30 04:53 | Checkpoint T0 (baseline, last 8 h before anything took effect): open→live 146 min, merge→live 59, 0% live within an hour, 62 PRs merged; runs PR 280 / queue 78 / main 53; fails 17%, cancels 30% | report | — | PRs 23656 (dev tools) and 23657 (review bot) open |
| Oct 30 | Unit tests at PR open (undoes only the test deferral from #20840; the rest of the Oct 17 cost program stays); always-run list sharded in a second PR | PR | Revert button | merged Oct 30 06:20 (#23661, judge SHIP); sharding PR #23673 merged 06:33 (judge SHIP); cost counted: ~1.5× test runs per commit for ~17 min per PR |
| Oct 30 06:40 | Release gate: a docs-only main tip no longer freezes releases (pre-existing defect found by the watch: 7 holds, 111 min in 48 h) | PR #23684 | Revert button | merged, judge SHIP |
| Oct 30 | Delete the 12-min release spacing; delete the 3-min main debounce | 2 PRs | Revert button | debounce merged 06:45 (#23686, judge SHIP); spacing merged 07:30 (#23708, judge SHIP; watch worker rollouts/h, Temporal time-outs, Lambda cold starts) |
| Oct 30 | Review bot: 🔴 only with file, line, reproducing input and why tests miss it | PR | Revert button | merged 06:52 (#23657, judge SHIP); watch: 🟡 findings that later turn into reverts |
| Oct 30 | Stale dev tools out of the production build (.local.tsx); hub hides them in production | PR | Revert button | merged 07:00 (#23656, judge SHIP): 14 of 830 routes out; in-use tools stay; QA on production 07:05: hub shows 16 tools, the five stale ones gone, removed route 404, Status page loads (screenshot) |
| Oct 30 | Express lane v1: [urgent] + express label → thin PR run, immediate promote, max 3/day, guard on paths, incident link and a daily cap (who applied the label is not a guard: every session acts as Fede) | 2–3 PRs | Revert button | judge SHIP on all three branches; #23718 (deploy dispatch keeps the red-sweep hold) merged 08:35; release side #23671 merged 10:46 (ships a merged express PR at once, promote-now takes a PR number, red-sweep check now sees push-triggered sweeps); PR side #23752 merged 11:40 (thin run on express label + [urgent] + ## Incident, guarded by the dangerous-diff list and a rolling 24-h cap of 3). Express lane v1 live. no queue jump yet (Smith: a jump restarts every train; needs the red-main claim); floor ~28 min until the queue is thin |
| Oct 30 12:53 | Checkpoint T1 (hour 8, 04:53–12:53 UTC): open→live 91 min (T0 146, week 125), merge→live 18 (59 / 54), 31% live within an hour (0% / 2%), 81 PRs merged; runs PR 275 / 15 min, queue 93 / 10, main 61 / 5; fails 12%, cancels 24%. Most changes landed in the second half of the window, so the next checkpoint is the first clean read. | report | — | all plan PRs merged except the thin queue (#23767, one fix pending) |
| Oct 30 06:26 | Red sweep: one flaky test (turnover work-order ids drawn in parallel since June; 4–5 of 30 runs fail under load). Releases held 24 min; re-test green 06:50. Not a rollout change. Fix-forward PR on the test. | incident | — | resolved; driver 009 |
| Oct 30 | Sweep per merge batch (30-min coalescing, ~41 sweeps/day, +$5–7/day) | PR #23699 | Revert button | judge SHIP (twice); one bot catch fixed: the urgent-merge check could not see push-triggered sweeps, now every sweep reader is event-agnostic with a scanner; merged 11:28 |
| Oct 30 | Sweep retries failed test files once (judge's precondition before the queue drops its test re-run: at 41 sweeps/day, flakes alone would freeze releases 1–2 times a day) | PR #23715 | Revert button | merged 08:10 (judge SHIP); would have saved 4 of 6 red sweeps last week; first live proof is the next red shard |
| after ack | Merge queue keeps typecheck, lint, build and guard pins; drops only the unit-test re-run (saves ~$6–9/day; +2–4 min merge→live on normal merges because push:main now tests) | PR #23767 | Revert button | judge SHIP (gap stated in his words); Smith acked with one condition, built: a red sweep files the red-main claim at once (driver: Smith by his choice) and opens the issue; the #alerts page is held out because Fede's 2026-10-01 ruling keeps CI health off Slack except the 8-hourly #spending report: Fede's call: allow one #alerts line per red sweep, or keep the ruling. Smith: claim + issue meets his condition. PR #23767 re-armed after the judge's and the bot's findings (skipped-test block gone, mutation control added, claim-posted flag into the issue); judge SHIP; disarmed because Smith's listener drops a claim posted by his own bot user: the claim must post as Trinity, which needs a new repo secret (TRINITY_SLACK_BOT_TOKEN, the bot token already in the Keychain). Fede said finish: the secret is in; merged 14:42. Every change in the plan is live; the 48-hour watch continues to Oct 31 22:40 CT. |
| deferred | Revert-not-freeze on a red sweep; queue jump for express | — | — | not in the 48 h; the driver reverts by hand |
| Oct 30 16:00–18:15 UTC | Fleet overload | — | — | 1,640 runner jobs in the hour vs 593 the day before; PR runs 74 vs 29; AWS spot out of stock; GitHub Actions degraded. Tests-at-open made each PR run about 25% heavier. Main stayed green; production stayed current. |
| Oct 30 20:55 UTC | Reverted: tests at PR open (#23661, #23693) | — | — | Fede: repeats the Oct 17/19 finding (2.1 runs per PR vs 1.3 at arm). Tests and guard pins run once a PR is armed again (#23845, #23854). Expect open→live to lose about 10 minutes until the review-rounds work lands. |
| Oct 30 19:40 UTC | Opus 5.5 max full pass, every change vs history | — | — | One change repeated a known failure (tests at open). Fifteen keeps. The 5-minute deploy window and the removed release spacing stay only while Vercel spend is near its $6.67/day budget; Vercel added to the watch. |
| Oct 30 20:30 UTC | Tried and dropped: tests start once on the bot's green | — | — | Built (#23873), measured about 2 minutes saved per PR against one extra type-check/lint/build run per PR. Closed, branch kept. |
| Oct 30 21:00 UTC | Review-rounds read, 397 PRs | — | — | Pushes after the first verdict: 48% fix a red check, 23% fix a real bug, 18% merge main, 10% new work. Five changes worth 13–18 min per PR. First two opened: no merging main to pick up a fix (#23884); five cheap hygiene checks in the local self-review (#23886). |
| Oct 30 20:54 UTC | Checkpoint, hour 16 | — | — | Last 8 hours: open→live 73 min (week before 125), merge→live 21 (54), 34% live within an hour (2%), 80 PRs merged. Runs: PR 295 at 16 min, queue 92 at 9, main 61 at 8. Deploys on pace for ~66/day (was 42). No red main, no red sweep. |
| Oct 30 21:30Z | Where the hour goes, measured on 88 PRs merged today | per PR, median minutes: open → first bot verdict 2; first verdict → last push 8 (p90 109); last push → armed 6; armed → entered the queue 19 (p90 50); queue → merged 10. First verdicts: 41 green, 30 yellow, 16 red. | — | read; the 19-minute “armed → queue” block is the largest piece left |
| Oct 30 21:40Z | Why PR runs go red (60 failed runs today) | 26 real test breaks by the author, 8 policy gates, 4 type errors, 3 flaky, 0 machine failures, 12 double-counted re-runs | — | read; reds are real, so the lever is catching them on the laptop, not CI flake |
| Oct 30 21:50Z | Dropped: skip the repeated type-check/lint/build job at arm time | Opus audit (never tried; #20840 measured it as waste but it was never built). Over 39 arm-time runs it saves 0.0 min of wall time; only ~$3/day of machine cost | — | not a speed lever; parked as a cost item |
| Oct 30 22:05Z | Found: test shards start 8 min into the arm-time run | they wait on the “affected” job: 66 s deep checkout, ~2.5 min full dependency walk (vitest list --changed, no cache), ~2 min running the selected tests serially, 41 s always-run list; each shard then does its own 60 s deep checkout | — | two no-behavior-change cuts in build: shallow checkout in both test jobs (~1.5 min), send the selected tests to the shards (~2 min) |
| Oct 30 22:00Z | Review-rounds change 1 merged: never merge main to pick up a fix | PR #23884 merged 48742e697; the pre-push guard loses its override, the manual and the review bot say so | revert #23884 | live |
| Oct 30 22:03Z | Review-rounds change 5 armed: fast self-review runs five cheap CI checks before the first push | PR #23886 armed at 5204221165 after three red rounds on its own test file (git env shape, then two type errors); five judge suggestions filed as #23904–#23908, bot nit #23910 | revert #23886 | live (merged 5d6e82095) |
| Oct 31 01:20Z | Shallow checkout on the test and shard jobs merged | PR #23931 merged a45c267f5; each checkout 62–63 s → 20 s, about 1.5 min per PR; a spot-reclaim exit inside a sub-step was misreported as a lint failure on the way (issue #23961) | revert #23931 | live |
| Oct 31 02:22Z | Affected selection runs on the shards | PR #23950 merged 391d3741b; planted failing test went red on one shard, the aggregate and the required check (run 41154171774); unit-test job 433 → 307 s, first shard start 600 → 395 s, arm-time run 931 → 792 s | revert #23950 | live |
| Oct 31 03:50Z | Two push-hook false positives fixed | #24051 (deleted-file check no longer halts on a comment path that climbs past the repo), #24055 (symbol sweep ignores nine ordinary words); found when the marketing-dashboard cleanup could not push | revert either | live |
| Oct 31 04:53 UTC | Checkpoint, hour 24 | — | — | Last 8 hours: open→live 111 min (week before 125), merge→live 26 (54), 10% live within an hour (2%), 203 PRs merged. Runs: PR 651 at 13 min, queue 234 at 9, main 151 at 7; fails 14%, cancels 18%. Vercel production deploys: Oct 29 42, Oct 30 67. No red main, no red queue run, no red sweep. Open→merge is the whole gap (79 min median); merge→live is done. |
| Oct 31 12:53 UTC | Checkpoint, hour 32 | — | — | Last 8 hours (overnight): open→live 114 min (week before 125), merge→live 15 (54), 25% live within an hour (2%), 20 PRs merged. Runs: PR 44 at 13 min, queue 20 at 9, main 20 at 7; PR-run fails 36% (author test breaks), queue and main 0%. Vercel production deploys: Oct 31 30 by 12:53Z. Nothing new went live; no red main, no red queue run, no red sweep. |
| Oct 31 15:11 UTC | #24082 merged: marketing dashboard code out of the app | — | — | Closes the marketing move: the staff dashboard is served from its own repo and Vercel project behind Google sign-in (live since 01:44Z); the app no longer builds or routes it (34 files, −2,984 lines). Two false reds on the way from the arm-run verdict adopting a cancelled sibling run (issue #24115); a full re-run of the label run cleared it. Rollback: Revert #24082, DNS unproxied, domain back to the app project. |
| Option | What it does | Open→live | Merge→live | CI $/day (now ~$75) | Effort | Hits 60 / 30 / $65? |
|---|---|---|---|---|---|---|
| A Stop waiting | Delete the 30-min window, the 12-min spacing and the 3-min main debounce; delete dead and advisory work | ~80–85 | ~15–20 | about −$2 (and +$6–10 on Vercel) | ~3 days | no / yes / no |
| B A + test once | Tests start at open; the queue only type-checks and builds the merged tree; a red sweep reverts its culprit instead of freezing every release | ~45–55 | ~15–20 | −$35 to −$50 | ~10 days | yes / yes / yes |
| C B + express lane recommended | Fix-to-production lane in ~15 min with a guard; a 🔴 needs a failing input or test, otherwise it is a 🟡 issue | ~55–60 in the 48 h, ~50–55 once the queue is thin (judge's numbers, Oct 30) | ~16–21; emergencies ~28 | +$30 to +$45 during the 48 h; +$10 to +$25 after; cost comes down only with the fix-push narrowing | ~15 days | yes / yes / not yet |
| D C + first split | Dev tools out of the production build; marketing site as its own app | 1–3 lower | 1–3 lower | about the same | ~22–26 days | yes / yes / yes |
Recommendation: C, in this order. The window and spacing today (one setting and a small PR), then tests at open, then the thin queue with revert-not-freeze, then the express lane and the evidence rule for reds. Dev-tool deletions ride inside the cleanup; the rest of the split waits. Everything in A–C ships without switches, as fixes to live behaviour.
| Period | Deploy window | Merge→live med / p90 | Open→live med | Deploys/day |
|---|---|---|---|---|
| Sep 15–24, GitHub machines | none | 14 / 20 | 60 | 140 |
| Sep 24–28, GitHub machines | 30 min | 48 / 72 | 85 | 58 |
| Oct 7–11, AWS + release train | 5 min | 20 / 28 | 73 | 81 |
| Oct 12–17, AWS | 30 min (repo variable) | 61 / 80 | 116 | 41 |
| Oct 17–29, plus tests-on-arm and the 2-h sweep | 30 min | 53 / 77 | 120 | 36 |
| # | Move | Open→live | Merge→live | $/day | Eng-days | Risk / what could break | Reverses |
|---|---|---|---|---|---|---|---|
| 1 | Stop waiting on purpose: delete the 30-min window variable (the docs already say 5), the 12-min release spacing and the 3-min main debounce | −35 to −40 | 55 → 15–20 | Vercel +$5.5 to +$9.7; AWS −2 | 0.5–1 | Deploys go from 36 to 70–105 a day: more Lambda and worker ships, more cold starts (warm-up exists). Smaller batches roll back more easily | Oct 12 window; Oct 6 pacing |
| 2 | Queue proves only the merge: type-check and build the merged tree; drop its test re-run and the 2-min wait | −6 to −8 | 0 | −20 to −26, net about 0 to −6 once the sweep runs per batch | 1–2 | A test break that only shows on the merged tree reaches main; guard pins stay in the queue and the per-batch sweep catches the rest (needs #6 first) | Sep 30 queue reinstatement |
| 3 | Test once, early: unit tests start at open alongside the review; fix pushes re-run only the tests the fix touches; the 11-min always-run step runs only when its inputs change, or is split across machines | −20 to −25 | 0 | +$10 to +$20 as built (about 1.5× test runs per commit); the narrowing of fix-push re-tests, later, is what makes it net negative | 3–4 | A wrong narrow test pick lets a break slip; the queue type-check and the sweep catch it | the Oct 17 deferral |
| 4 | Delete dead and advisory work: 13 dead workflow files (14% of the YAML); day0-replay, the pre-queue type check and the eval dispatch loop off the PR path; 4 red report-only nightlies; 8 sub-minute crons folded into one; the deploy wait off the runner | 0–2 | 0 | −5 to −7 | 2 | Low: a few reports stop | — |
| 5 | 🔴 only with evidence: a blocking finding names a failing input or test, otherwise it becomes a 🟡 issue | −6 to −8 (mean) | 0 | −5 to −6 + tokens | 1 | Real bugs could slip; watch the revert rate. 27% of PRs get a 🔴 and it doubles time to merge (49 → 110 min), mostly from the restarted test run, so #3 shrinks every round | — |
| 6 | A red sweep reverts its culprit and ships the rest (no global freeze) | slowest 1% | slowest 1% | 0 | 1–2 | Medium. Freezes added 66 h of deploy gaps since Oct 12 but touched only 20 of the 250 slowest PRs | — |
| 7 | Express lane (below) | emergencies | emergencies | ~0 | 3–4 | Becoming a shortcut; see the guard | — |
| 8 | First split: dev tools out of the production build; marketing site as its own app | −1 to −3 | −1 to −3 | small | 6–11 | Login and middleware are shared | — |
On #1, the Vercel doc from Oct 11 prices the window at $5.55–$9.72 a day; that puts Vercel at $15–19/day against a $6.67 budget. If we want Vercel flat later, build the production bundle once on our machines and only upload it (also removes the 6-min build), at the price of production secrets on CI machines: a security call. Faster machines are on no list: the whole checks job is under 4 minutes.
Today there is no way past the merge queue: the ruleset has no bypass list, editing it empties the queue, and GitHub's "jump to the front" option exists but is unused. [urgent] only skips the spacing and the window after merge (measured 8 cases: 41.5 min to merge, 20 to live). The manual promote failed 3 of its last 6 runs on a short commit id.
| Step | Today ([urgent]) | Express |
|---|---|---|
| PR checks | ~21 (armed run) | ~6 (type-check, lint, affected tests, split across machines) |
| Review bot | 5–6, in parallel | 5–6, in parallel |
| Merge | 10–12 (queue) | ~4 (jump to the front; type-check and build only) |
| After merge | ~8 main check, then ~6 build | ~6 (Vercel build starts at merge, promote in seconds) |
| Total | ~60 | ~14–16 |
Guard, so it never becomes the normal path: an express label plus an [urgent] title and an incident link (who applied the label is not a guard: every session acts as Fede); no change under auth, middleware, permissions, secrets or the merge guard; at most 3 a day, each posting one line to #alerts; the full sweep runs on the shipped commit and a red one reverts it automatically; a weekly count on the Decisions page; fix the manual promote to accept a short id or a PR number.
The repo was inventoried unit by unit: files, lines, whether the unit is in the web build, the type check and the test set, and which workflow deploys it. Compile and type-check are about 2 minutes of a PR run and about 8 minutes after merge (queue build 1.8 min, Vercel build 5.5 min). Those are the only minutes a split can touch. None of the splits touches the review, queue or deploy waits that make up the two hours.
| Unit | Size | Where it costs time | What pulling it out saves | What ties it in | Verdict |
|---|---|---|---|---|---|
| Dev tools + staff admin + admin/dev APIs | 301 of 1,001 routes (30%); 893 files / 194k lines only they use (11% of the web build) | queue build, Vercel build | 0.5–1.5 min per merge, 0 per PR; build memory (a build died of memory on Oct 16) | shared login, data layer, UI kit | Do it, no split needed rename unused routes to .local.ts, the way 10 dev routes already stay out of production; 1–3 days |
Marketing site (public) | 100 files, 25k lines, ~3% of the build | build | seconds for product CI; marketing edits ship in ~5 min instead of queuing behind the app | same host, middleware, sitemap, shared components | Go — Fede, Oct 30 evening 5–8 days; plan below |
| Lambdas + renewal worker | 215 files, 63k lines, ~1% of type check and tests | nothing on a PR | nothing; they already deploy on their own but wait for the whole web CI | import src/lib | Workflow change, not a split |
Clara agent loop agents/ | 888 files, 288k lines; 314 files compiled into the web app | 6% of type check, 4.5% of tests | 1–2 min | 216 files import src/lib; 195 src/lib files import agents back; an import alias falls through to it | Architecture project weeks; undo the two-way imports first |
scripts/ + evals/ | 7,500 files, 1.66M lines, 1,070 test files (10% of the test set) | tests, scripts type check (13 s) | seconds per PR | every file imports src/lib | Own packages, later |
| Tests | 2.56M of the 4.64M type-checked lines (55%) | type check 36 s | — | — | Type check is already small |
Fede asked for this on Oct 29; it was parked as Later behind option C. Tonight two one-line homepage copy PRs each ran the full app CI, so it is a go.
Also found: mock-pms-service, testing-harness and tools are type-checked for no reason; two stale pages ((public)/test, api/mock) can go; 8 packages have no importer. Seconds each, zero risk.
Decision (Fede, Oct 31 00:1xZ): option A, with login, no MFA, "like how we did factory." Cut over the same night: the dashboard now lives in its own repo and Vercel project (propflow-marketing), and marketing.propflowai.co sits behind a Cloudflare Access app with Google sign-in for any @propflowai.co address (same shape as factory.propflowai.co; the app itself refuses any request without a valid Access token). The visits number reads the main app's Vercel Analytics project, so it matches what the dashboard showed before.
| Step | Before | After | Proof |
|---|---|---|---|
| Where the code lives | Main app repo, every change = a full app CI run | Own repo, own 1-minute CI, own deploy | repo propflow-marketing, main a3f690d |
| Who can open it | Three platform admins after app sign-in + MFA | Anyone with a @propflowai.co Google account, one sign-in, 30-day session | Access app 69df77f1, policy email_domain=propflowai.co |
| marketing.propflowai.co | App project, 307 to /login | 302 to propflowai.cloudflareaccess.com (Google), then the dashboard | curl 01:44Z; origin reached through Cloudflare (cf-ray + x-vercel-id on a throwaway probe host, deleted) |
| Dashboard code in the app | ~2,500 lines | Removed in PR #24082 (merged Oct 31 15:11Z, a12be253c) | #24082; two push-hook false positives fixed on the way (#24051, #24055) |
Still open: one real Google sign-in on the new host by a person confirms the six views (every automated step is proven: edge redirect, origin reachable, token audience matched, six views on the preview build); if it fails, rollback is Revert #24082 + DNS unproxied + domain back to the app project. Also open: whether the dashboard keeps the copied read/write AWS key or gets its own read-only user (Fede's call); rollback = set the DNS record back to unproxied and move the domain back to the app project (two API calls, under a minute). Scoping that preceded the decision is kept below.
What it is. One staff dashboard at marketing.propflowai.co (Overview, Traffic, Channels, Campaigns, Leads, Connections), about 1,000 lines of page code plus 1,500 of data and LinkedIn code, built Oct 24, open only to the three platform admins after sign-in and MFA. It reads visit events and campaign counts from the traffic table, demo requests and the LinkedIn token from the main table, and the Vercel Analytics and LinkedIn APIs. No cron feeds it.
What cannot move. The visit-recording endpoint the public pages post to, the /go campaign links and their counters, the code that writes traffic rows, and sign-in. Only the dashboard (the read side) can move; the one shared contract is the traffic key layout.
| Option | Shape | Effort | Main risk |
|---|---|---|---|
| A | Own repo and Vercel project on marketing.propflowai.co, reading the same tables | 8–12 days | Sign-in: passkeys, MFA and the admin roster live in the app; A must route sign-in through the app or verify the session against it |
| B | Same repo, own Vercel project, a marketing-only CI class so a dashboard PR skips the app's tests | 3–5 days | A path-filtered required check can wait forever (seen before in this repo) |
| C | Leave it | 0.5 day | None new |
Change volume: 13 commits in 60 days, 8 of them on the day it was built; nothing since. A move buys ownership isolation, not publish speed. Recommendation: C; B if Fede wants it out for ownership; A only if a non-engineer must ship dashboard changes alone. Unverified: whether the app's session check accepts a forwarded cookie from another server; the traffic table's access role.
Tonight's two copy changes each paid the whole app pipeline: type check, lint, build, test shards, paid bot review, merge queue. One needed a 19-minute run after two cancelled ones; the other went red for 13 minutes and re-ran. The case is speed to publish and keeping copy edits away from the app, not CI spend: only 3 of 998 PRs merged Oct 16–30 were marketing-only (~42 machine-minutes in 14 days).
| Option | What it is | Trade-off | Pick |
|---|---|---|---|
| A New repo | Site repo with its own Vercel project on propflowai.co; the app keeps app.propflowai.co. Light checks. The UI pieces the site uses (18 UI-kit and 7 app-library modules, counted) and the brand colors and fonts copied once. | Two copies of a few components. Every app page served under propflowai.co today (sign-in, legal pages, callbacks) must be listed and moved before the cutover. | Recommended the only option where a copy edit never touches app CI |
| B Same repo, own package | Site as a separate package; a path filter runs only site checks on site-only PRs. | Still shares the merge queue and bot review. A path filter is a new switch, and ours have crept. | Second |
| C Copy-only fast lane | Text-file-only PRs skip the app pipeline. | Smallest, but only copy; layout and component edits still ride full CI. | Stopgap |
| Today | With A | |
|---|---|---|
| Copy change to live | 60–75 min: one app CI cycle plus the queue (open→live median 73 min today, 125 last week) | Minutes: a site build and preview. To measure (the accounting above estimates ~5 min) |
What makes A work. 37 site files import the app's UI kit, 28 import app library code, 2 touch login-like paths. Those are what get copied or cut.
| Step 1 inventory, measured Oct 30 | |
|---|---|
| Routes on the public host | 21 site, 24 app, 13 shared or unclear (legal pages, sitemap, robots, og images, contact form) |
| API routes the site calls | 5, all app routes; /api/contact is called by the contact form but has no handler on main |
| Shared pieces to copy or package | 18 UI-kit modules, 7 app-library modules |
| Env vars read | 2 |
| Tests importing the marketing tree | 39 |
| Analytics | in the root layout (Vercel Analytics plus a first-party beacon); none in the marketing tree |
| Preview-token mechanism | none in the marketing tree |
Rollout, dark and small.
Risks to carry through: identical URLs for search ranking, analytics tags (they live in the app shell today, not in the site code), the [preview] build flow for unpublished designs, and who owns the site repo (Fede's sessions or Gera's).
Footprint: src/app/(public) 100 files / 24,874 lines; src/components/marketing/page 77 / 21,314; marketing/components 12 / 10,741; marketing/home 29 / 1,865; 21 tests / 2,222 lines; 37 assets under public/marketing and public/videos. (standalone)/marketing is an internal analytics dashboard and stays. No site repo exists today; camellia-site is the nearest shape. Tonight's copy PRs: #23897, #23899.
Gera's side (Agent Smith, Oct 30 22:15Z): agrees the end state is a separate repo with its own Vercel project, but recommends two steps. Now: a CI filter so a PR that only touches marketing paths runs lint, a build and the bot review, then goes live without the full test suite. Then: the separate repo. His coupling read matches ours: the marketing tree pulls brand tokens, the home content module, shared UI and composition pieces and the logo animation; about 12 tests and the middleware (which decides what the root domain serves) depend on it, so the root route and the preview links move with the site. He found no earlier written decision either. He asked that the two auth-like imports be named on this page.
Stopped, Oct 31 00:35Z: Fede: Gera is already driving the landing page refactor, so this lane ends here. What exists is handed to Gera through Smith: the repo, the idle Vercel project, the merged dark app change (inert while the setting is off), and the cutover issue. Nothing customer-facing changed.
Built, Oct 30 23:10Z. Steps 1–3 done: route list (21 site, 24 app, 13 shared), repo propflow-site with its own Vercel project on a preview host, 27 pages pixel-identical to production (one animated icon differs by 0.002%). Shape changed to multi-zone: the app keeps propflowai.co and every app path; only the 27 marketing paths get sent to the site, and only when a setting is on (off = today's behavior). Found and fixed before any traffic: cache header parity, "don't index" on the preview host, assets served from the site's own host, and the security header that lists allowed asset hosts (both repos). The app-side change is PR #23963, merged dark Oct 31 00:0xZ: nothing changes until the setting is on (cutover issue #23957). A throwaway probe proved a Vercel rewrite stamps the forwarding domain on the request, so the live pages will stay indexable. Cutover ask is with Fede. One slip: the traffic beacon was live on the preview host during the first screenshot runs; being counted, nothing deleted without Fede.
Decided: A — Fede, Oct 30 22:30Z ("go, you have 4 hours"). Build window to about 02:30Z Oct 31: new repo, own Vercel project on a preview host, screenshot match of every public page against production. Constraints: every domain accounted for, nothing breaks, analytics identical. The root-domain cutover is a separate go from Fede.
Smith's two-step order (B now, then A) is on the table as a fourth choice; the trade is a faster copy lane within days against building a path filter that is thrown away when the repo moves.
review check.| Week of | CI $/day | Merged PRs/day | $ per merged PR |
|---|---|---|---|
| Sep 22 (GitHub-hosted) | 738 (weekday) | 261 | ~2.8 |
| Sep 28 | 348 | 398 | 0.87 |
| Oct 5 | 155 | 267 | 0.58 |
| Oct 12 | 92 | 201 | 0.46 |
| Oct 19 | 61 | 126 | 0.48 |
| Oct 26 (4 days) | 104 | 204 | 0.51 |
Where the machine minutes go on a day like Oct 28 (1,495 CI runs): PR checks and tests about half, the merge queue's per-PR re-test about a third, main-branch runs and the 2-hourly sweep the rest; scheduled jobs add ~1,640 machine-minutes. The queue cut (#2) saves $20–26 a day, tests-once (#3) $9–18, the cleanup (#4) $5–7: together $35–50 a day, from ~$75 to under the $65 target. (Dollar deltas are shares of the AWS bill, ±30%.) Per-run price is already 6× below GitHub's; what is left is runs per PR, not price per minute.
PROMOTE_WINDOW_MINUTES; the script default and the runbook still say 5); tests deferred to arming Oct 17.| Week of | PRs | Open→merge med / p90 | Merge→live med / p90 | Open→live med / p90 | Live ≤ 60 min | CI runs per PR | PRs with a red run |
|---|---|---|---|---|---|---|---|
| Sep 14 | 1,184 | 46 / 428 | 13 / 20 | 64 / 460 | 48% | 3 | 40% |
| Sep 21 | 1,824 | 34 / 273 | 22 / 66 | 73 / 311 | 39% | 2 | 35% |
| Sep 28 | 2,785 | 97 / 692 | 51 / 97 | 155 / 762 | 8% | 2 | 24% |
| Oct 5 | 1,867 | 68 / 349 | 25 / 87 | 111 / 443 | 21% | 2 | 31% |
| Oct 12 | 1,407 | 52 / 228 | 60 / 80 | 115 / 291 | 4% | 2 | 29% |
| Oct 19 | 881 | 63 / 234 | 52 / 77 | 119 / 298 | 4% | 3 | 37% |
| Oct 26 | 818 | 67 / 304 | 54 / 77 | 125 / 357 | 2% | 3 | 38% |
CI runs per PR is 3 since Oct 17 because arming now starts a second run by design.