Skip to content

feat(canvas): source-version and build persistence with guarded activation - #73730

Closed
k11kirky wants to merge 1 commit into
posthog-code/canvas-build-pipeline-phase-1from
posthog-code/canvas-build-pipeline-phase-3
Closed

feat(canvas): source-version and build persistence with guarded activation#73730
k11kirky wants to merge 1 commit into
posthog-code/canvas-build-pipeline-phase-1from
posthog-code/canvas-build-pipeline-phase-3

Conversation

@k11kirky

@k11kirky k11kirky commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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)
  • Cross-repo: each posthog PR should deploy before the same-phase code PR ships (skills/tools/endpoints must exist before clients rely on them).

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.meta JSON 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 the code side, 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, migration 1265) — CanvasSourceVersion (immutable per-publish record: content hash, private object key, parent lineage, task attribution, legacy-history correlation) and CanvasBuild (queued → building → ready/failed, bounded diagnostics, frozen manifest, integrity hash, pinning). Both on fail-closed team-scoped managers; the team/created_by FKs use db_constraint=False so the migration takes no hot-table locks (risk analysis: ✅ safe, two CREATE TABLEs). The desktop file-system row keeps only pointers (currentSourceVersionId, publishedBuildId) in meta.

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 — advancing publishedBuildId in 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 + MCPGET …/{id}/canvas/builds/ (desktop-file-system-canvas-builds-retrieve tool): live pointers + recent builds with status and diagnostics, for post-publish polling. Canvas summaries now expose current_source_version_id and published_build_id. The validating-and-publishing-canvases skill 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_build without 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.
  • All 50 existing phase-1 canvas tests still green (publish behavior unchanged when storage is unavailable or the legacy path is used).
  • Repo-wide mypy --cache-fine-grained . clean; ruff clean; sqlmigrate inspected (no hot-table locks); analyze_migration_risk reports both operations safe; MCP schema snapshots + generate-tools suites green; OpenAPI --fail-on-warn passes; skills lint clean.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

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 in meta per the plan's migration section (no FileSystem schema change); the build status field is exposed as build_status to avoid the drf-spectacular status enum 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 no unscoped() reads exist outside the genuinely cross-team retention sweep.

…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
@k11kirky k11kirky self-assigned this Jul 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hey @k11kirky! 👋

It looks like your git author email on this PR isn't your @posthog.com address (k11kirky@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

Uncompressed size of every built .js bundle, compared against the base branch.

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 budget

How 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 import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.24 MiB · 22 files no change ███░░░░░░░ 27.6% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.08 MiB · 3,014 files no change ████████░░ 83.2% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
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

Copy link
Copy Markdown
Contributor Author

Superseded by #73874, which consolidates the full Canvas build pipeline into one reviewable PR.

@k11kirky k11kirky closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant