feat(canvas): source-version and build persistence with guarded activation - #73730
Conversation
…ation Phase 3 of the canvas application build pipeline plan: - New normalized tables CanvasSourceVersion and CanvasBuild (fail-closed team-scoped managers, no hot-table FK constraints); the desktop file-system row keeps only compatibility fields and the currentSourceVersionId/publishedBuildId pointers in meta. - Upload-then-commit publishing: the serialized source project is uploaded to a private, content-addressed object-storage key before the canvas transaction inserts the version + queued build and advances the source pointer. Conflicts leave at most an unreferenced upload. Storage outages degrade to a legacy-only publish rather than failing the save. - Celery build worker: verifies source integrity, validates, uploads immutable per-build artifact objects with a frozen manifest + integrity hash, marks the build ready, and advances publishedBuildId only while the build's source version is still the canvas head. Failed builds record diagnostics and never displace the last-known-good build. - Build status/diagnostics API (GET canvas/builds) + MCP tool, and a daily retention sweep keeping the active, rollback, and pinned builds while pruning failed artifacts after 24h and superseded ones after 30 days. Generated-By: PostHog Code Task-Id: 9e9a7b3c-f90d-4867-aa0d-b9acc83e26e1
|
Hey @k11kirky! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
🤖 CI report✅ Bundle size — no changeUncompressed size of every built Total: 64.44 MiB · no change No file changed by more than 1000 B. Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report ✅ Eager graph — within budgetHow much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy
🟢 Largest files eagerly shipped from
|
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 789 B | src/scenes/ChunkLoadErrorBoundary.tsx |
| 762 B | src/index.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 281.5 KiB | ../node_modules/.pnpm/posthog-js@1.407.2/node_modules/posthog-js/dist/rrweb.js |
| 267.7 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 236.0 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 226.1 KiB | ../node_modules/.pnpm/posthog-js@1.407.2/node_modules/posthog-js/dist/module.js |
| 154.3 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.2 KiB | src/lib/api.ts |
| 94.0 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 90.6 KiB | ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Toolbar bundle — eager 2.18 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.18 MiB · 17 files | no change | ████░░░░░░ 38.1% of 5.72 MiB |
| Deferred (lazy) | 2.07 MiB · 33 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | no change | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 716.5 KiB | dist/toolbar/toolbar-app-HMV4VZ5O.css |
| 545.2 KiB | dist/toolbar/chunk-chunk-3RADJBLD.js |
| 484.3 KiB | dist/toolbar/chunk-chunk-ZXJK34VQ.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-6SQZIKIH.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-G6ARZY2E.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-P4AKBHPC.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-B7POBA4G.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +13.5 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1355.91 MiB · 🔺 +13.5 KiB (+0.0%)
ℹ️ MCP UI apps size — 32 app(s), 17065.5 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 599.5 KB | 187.7 KB |
| action | 457.8 KB | 187.7 KB |
| action-list | 564.3 KB | 187.7 KB |
| cohort | 456.8 KB | 187.7 KB |
| cohort-list | 563.3 KB | 187.7 KB |
| email-template | 456.6 KB | 187.7 KB |
| error-details | 472.4 KB | 187.7 KB |
| error-issue | 457.5 KB | 187.7 KB |
| error-issue-list | 564.2 KB | 187.7 KB |
| experiment | 561.5 KB | 187.7 KB |
| experiment-list | 565.1 KB | 187.7 KB |
| experiment-results | 563.2 KB | 187.7 KB |
| feature-flag | 567.1 KB | 187.7 KB |
| feature-flag-list | 570.9 KB | 187.7 KB |
| feature-flag-testing | 461.0 KB | 187.7 KB |
| insight-actors | 562.1 KB | 187.7 KB |
| invite-email-preview | 456.0 KB | 187.7 KB |
| llm-costs | 559.5 KB | 187.7 KB |
| session-recording | 458.6 KB | 187.7 KB |
| session-summary | 463.9 KB | 187.7 KB |
| survey | 458.4 KB | 187.7 KB |
| survey-global-stats | 562.2 KB | 187.7 KB |
| survey-list | 565.0 KB | 187.7 KB |
| survey-stats | 562.2 KB | 187.7 KB |
| trace-span | 457.2 KB | 187.7 KB |
| trace-span-list | 564.2 KB | 187.7 KB |
| workflow | 457.1 KB | 187.7 KB |
| workflow-list | 563.7 KB | 187.7 KB |
| loops-review | 461.2 KB | 187.7 KB |
| query-results | 745.5 KB | 187.7 KB |
| render-ui | 826.2 KB | 187.7 KB |
| visual-review-snapshots | 461.6 KB | 187.7 KB |
|
Superseded by #73874, which consolidates the full Canvas build pipeline into one reviewable PR. |
Note
Stacked PR chain (Graphite-style) — merge in this order:
posthog/posthog: #73725 (phase 1) → #73730 (phase 3) → #73731 (phase 4)PostHog/code: #3824 (phase 1) → #3825 (phase 2) → #3826 (phase 3) → #3827 (phase 4)Problem
Phase 3 ("Cloud builds and immutable artifacts") of the canvas application build pipeline plan (PostHog/code#3823). After phase 1, canvas source still lives only in the
FileSystem.metaJSON blob: no normalized history, no object storage, no build lifecycle, and generation state dies with the initiating client.Note
Stacked PR — merge order: #73725 (phase 1) → this PR (phase 3). Base branch is
posthog-code/canvas-build-pipeline-phase-1. On thecodeside, PostHog/code#3825 (phase 2) can land independently; its phase-3b client PR depends on this one being deployed.Changes
Models (
posthog/models/file_system/canvas_build.py, migration1265) —CanvasSourceVersion(immutable per-publish record: content hash, private object key, parent lineage, task attribution, legacy-history correlation) andCanvasBuild(queued → building → ready/failed, bounded diagnostics, frozen manifest, integrity hash, pinning). Both on fail-closed team-scoped managers; theteam/created_byFKs usedb_constraint=Falseso the migration takes no hot-table locks (risk analysis: ✅ safe, twoCREATE TABLEs). The desktop file-system row keeps only pointers (currentSourceVersionId,publishedBuildId) inmeta.Publish (
canvas_build_service.py) — upload-then-commit: the canonical serialized project is uploaded to a private, content-addressed key (canvas_source/team_…/{sha256}.json.gz, never the user-content origin, dedup never crosses a canvas) before the row transaction inserts the version + queued build and advances the source pointer. A version-conflict publish creates no rows and leaves at most an unreferenced upload for the retention sweep. Object-storage outages degrade the publish to legacy-only instead of failing the save.Build worker (
posthog/tasks/canvas_build.py) — verifies source integrity (hash check), validates, uploads immutable per-build artifact files, and marks the build ready — advancingpublishedBuildIdin a second locked transaction only while the build's source version is still the canvas head. A superseded build finishes without stealing the pointer; a failed build records diagnostics and the last-known-good build stays live.API + MCP —
GET …/{id}/canvas/builds/(desktop-file-system-canvas-builds-retrievetool): live pointers + recent builds with status and diagnostics, for post-publish polling. Canvas summaries now exposecurrent_source_version_idandpublished_build_id. Thevalidating-and-publishing-canvasesskill gains an "after publishing: the build" section.Retention — a daily sweep prunes failed-build artifacts after 24h and superseded successful ones after 30 days, always keeping the active, rollback (most recent other ready), and pinned builds; source versions are never pruned.
Warning
The worker does not yet run the shared esbuild recipe (that needs the isolated node build image). For legacy-compatible projects the "build" freezes an immutable, integrity-hashed snapshot of the project files as the artifact — the runtime keeps rendering exactly as today. The compile upgrade slots into
run_canvas_buildwithout changing the lifecycle contract; the shared contract fixtures for it already ship in PostHog/code#3825.How did you test this code?
I'm an agent; automated tests only, all run locally against Postgres + ClickHouse:
test_canvas_builds.py(9 tests, object storage faked): the atomic publish → version+queued-build record (guards partial publishes), ready-build pointer advancement, stale build finishing late without stealing the pointer, failed build preserving the last-known-good published build, conflict publishes creating no rows, parent-version lineage, storage-outage degradation, the builds endpoint payload, and the retention sweep keeping active/rollback/pinned while pruning the rest — each is a regression the plan calls out explicitly and nothing else covered.mypy --cache-fine-grained .clean; ruff clean;sqlmigrateinspected (no hot-table locks);analyze_migration_riskreports both operations safe; MCP schema snapshots + generate-tools suites green; OpenAPI--fail-on-warnpasses; skills lint clean.Automatic notifications
Docs update
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored by Claude continuing PostHog/code#3823 phase delivery, directed by @k11kirky. Skills invoked: /django-migrations (hot-table FK guidance drove
db_constraint=False), /improving-drf-endpoints, /writing-tests. Notable decisions: pointers stay inmetaper the plan's migration section (noFileSystemschema change); the build status field is exposed asbuild_statusto avoid the drf-spectacularstatusenum collision; the worker is Celery (short-lived, idempotent,on_commit-enqueued) rather than Temporal;for_team()is threaded into the worker via the task payload so nounscoped()reads exist outside the genuinely cross-team retention sweep.