Repository navigation
Conversation
…eleases, site tokens
For Blocks v8 projects on GitHub behind site_editor_content_protocol (v7 is
unchanged):
- Editor saves go to sites/<site>/drafts/<slug>.json on the delivery bucket
(R2, S3 API), never git; /rpc reads main with the draft layered on, and the
session read answers the draft pointer (<host>/…/<slug>.json@<etag>).
- Publish commits the draft directly to main (fast-forward only), writes
revisions/<sha>.json, re-checks main's head, writes latest.json and deletes
the draft; a release that isn't live yet is "pending" with Resync.
- A Releases view next to Assets: what latest.json serves, main's history,
"Make current" for commits with a revision (rewrites latest.json only),
the Rolled back state and the schemaHash warning.
- Site tokens (Ed25519 JWS {site, kid, iat}, two active at most) with revoke
through the Cloudflare KV denylist, and a deployment-admin kill switch
writing kill:<site>.
- The managed-asset CDN default moves to assets.decocms.com.
- Removes the v8 git-branch draft machinery (/changes, draft-changes,
mergeBase, the v8 write side of repo-content-storage).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…s an absent schema Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e shim - apps/api (dev), apps/web and packages/e2e pin 8.1.0-next.6 exactly; bun.lock changes only the @decocms/blocks entries (frozen install passes). - The hosted draft storage no longer describes refs/idempotency, which next.6 removed from the protocol. - e2e: a `ref` param on blocks.list is rejected again (Invalid params, an unknown parameter in next.6). Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ring publish - Hosted routes, the hosted decofile branches and the sandbox-less git routes use metadata.siteSlug only when the org owns it in org_sites (siteSlug is member-editable); otherwise no hosted features (404). - Hosted routes honour project-scoped roles (isProjectAllowed). - Publish deletes the draft only while it is still the published save (ETag check under the draft lock); a save during publish is kept. - Site tokens are issued for Blocks v8 sites only. - Kill switch: state is read from the denylist (kill:<site>), the org KV copy is gone; it covers the sites the org owns in org_sites. - Hosted v8 publish never offers submit for review (no pull request). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Takes the base's lockfile and its retirement of the docs workspace; keeps this branch's repo-content-storage (no describe) and e2e. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… backfill
Creating a project (web create, GitHub import, deco import) now claims its
`metadata.siteSlug` in `org_sites` for the project's org, so the hosted
ownership check never blocks a legitimate project. Ownership also authorises
asset-storage credentials for `<slug>/*`, and `siteSlug` is member-editable
free text, so a slug is claimed only when no org owns it and deco.cx has no
site by that name (the deco import claims those after proving access).
Best-effort: a failed claim never fails the creation.
`POST /api/_admin/hosted/site-claims/backfill` claims the sites of existing
Blocks v8 projects. Dry run unless `{"dryRun": false}`; safe to re-run;
never takes a slug another org or deco.cx has (reported as conflicts); a free
slug two orgs' projects name is claimed for neither (ambiguous).
e2e: the fast-preview fixture no longer claims the slug itself, so the hosted
specs run on the auto-claim; a new case checks another org naming the same
site gets no claim.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nd the deco.cx lookup Creating a project no longer claims an unowned siteSlug that a project of another org already names (that org may hold assets under it from before claims existed) — the same rule the backfill applies as `ambiguous`; it is reported as a conflict. The deco.cx site lookup made during creation is bounded at 3 s and fails closed (no claim) on timeout. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… same rule - Rolled back = latest.json serves a revision older (in main's history) than the newest CMS-published one, or one no longer on main. A head the CMS didn't publish keeps the header Live with "main has unpublished commits". The search walks main 50 commits per page, at most 5 pages. - Resync asks for confirmation only when the screen says Rolled back, and writes main head's revision only when it's missing. - Every latest.json write stamps publishedAt now (the SDK's timeline rule). - Rename the hosted publish-popover adapter (no behaviour change): draft-git-compat.ts -> draft-publish-status.ts, fastPreviewHostedDraft -> loadHostedDraft (in hosted/), draftGitStatus/draftGitDiff -> hostedDraftPublishStatus/hostedDraftPublishDiff. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…cks 8.1.0-next.7) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…cms/blocks 8.1.0-next.7) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
latest.json is now `public, max-age=0, s-maxage=3600, must-revalidate`: the edge holds it up to an hour, readers revalidate every poll. After every pointer write (Publish, Make current, Resync) Studio purges exactly <delivery origin>/sites/<site>/latest.json through the zone's purge_cache API, retrying with short backoff. A purge that still fails leaves Publish pending; Resync reruns the pointer write and the purge. The 1 h s-maxage heals a purge that never landed. New settings CF_DELIVERY_ZONE_ID and CF_PURGE_API_TOKEN (Zone -> Cache Purge); unset, the purge is skipped with a warning. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Updating the pointer is now: write latest.json, then ONE purge with a 5 s timeout (AbortSignal.timeout). The purge retry loop is gone, and there is no restore of the previous pointer or other automatic recovery. A failed write (which skips the purge) or a failed/timed-out purge throws LatestUpdateError at once: - Publish: the git commit stands, the result is the existing "pending" state, and the draft is kept; it is deleted only after the pointer write and its purge both succeeded. - Make current and Resync: answer 502 with a clear message that ends in "Try again". The user retries from Studio (Resync, Make current, Publish). The popover's pending text now says "Resync to try again". Unconfigured purge (no zone or token) still skips with a warning. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
PO follow-up to the fail-fast change: the purge now makes at most two attempts, each bounded by AbortSignal.timeout(5000); a timed-out attempt counts as failed. If both fail, the operation fails as before (no restore of the previous pointer, no other recovery): Publish is pending with the draft kept, Make current and Resync surface the error, and the user retries from Studio. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…tatus
Publish deletes the draft right after the commit to main succeeds; the
CDN step (revision, head check, latest.json, purge) still runs but its
failure no longer fails Publish. The result is { result: "merged", sha,
cdn: "live" | "failed" } and the popover closes with "Merged · Live" or
"Merged · CDN update failed. Resync it from Releases."
Releases shows every commit as Merged; the CDN status is derived, not
stored: latest.json on main's head is Live; otherwise the newest release
shows Failed with Resync, unless latest.json was written after main's
head was committed (a Make current: Rolled back). Drops the history walk
and its flags (unpublished commits, revision off main, no recent release)
and the popover's in-place Resync.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…first publish Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
- Releases lists main's commits; a commit with a companion release (revisions/<sha>.json, one prefix listing per page) can be made current. Current is read from latest.json, never inferred; none means "Nothing on the CDN yet". - Removed the derived states (Live/Failed/Rolled back/Not live), the commit-date heuristic and Resync (route, API and UI): Make current on the newest release covers it. - Publish: merge, write the companion, make it current. The result says current / created (making it current failed) / none (no release). A draft that commits nothing (also when it equals main byte for byte) answers up-to-date, deletes the draft and leaves latest.json alone. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The Current row now keeps its Make current action, so a failed CDN purge can be retried on the commit latest.json already names. A failed latest.json write or purge returns the code latest-update-failed, which the Releases screen shows as a localized message. The e2e ownership test checks the Releases routes instead of the removed resync route. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
The product owner removed the kill switch and token revocation: there is no denylist. Studio still issues Ed25519 site tokens and the edge still verifies them. - Delete hosted/denylist.ts, hosted/kill-switch.ts and its test. - Remove the deployment-admin hosted-kill routes and dialog. - Remove DELETE .../site-tokens/:kid and the Revoke UI; issuing has no two-token limit any more (the limit only existed for revoke-and-rotate). - Drop CF_ACCOUNT_ID, CF_DENYLIST_KV_NAMESPACE_ID, CF_KV_API_TOKEN settings. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…osted Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…ship follows the org_sites link Brings in #7796 (org_sites.project_id link, immutable site slugs) and main. Hosted switch: - ownedProjectSite(orgSites, projectId, orgId) reads the project's org_sites link (getByProject) instead of metadata.siteSlug. hosted routes, decofile scope and the sandbox-less draft status use it. - claim-site.ts: a project's site is linked once (linkProjectSite): a slug no org owns is claimed first (still refused for deco.cx sites and slugs another org's project names), then linked. Tombstoned (reserved) slugs, another org's, another project's, or a second slug for a linked project are refused. - Admin backfill links v8 projects (report: linked / alreadyLinked / refused / ambiguous); several projects of one org naming one slug are ambiguous. - e2e: a slug change is refused and hosted keys stay on the linked site. Conflicts: - publish UI: main (#7792) replaced cms-publish-popover.tsx with publish-dialog.tsx; the hosted changes (no review mode, no Request approval, hosted publish action) moved into publish-dialog.tsx. - main removed the project "git" tab; kept hosted's "releases" tab only. - sandbox-proxy.ts: main dropped /git/suggest-commit; judge-review keeps the hosted-aware fastPreviewStatus/fastPreviewDiff backfill. - create.ts: project creation goes through claimProjectSite (claim + link). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Makes the Studio stack one line: #7796 -> #7728 -> #7770 -> #7766. Conflict in content-protocol-api.ts (applyProtocolPatch doc): kept both the hosted CDN-draft and the sandbox working-tree notes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
…m refusals A row that vanished between claim and link is "not-found", not "other-org". A used slug whose project is gone (or that predates the link) is refused as "relink-requires-admin", matching the org_sites rule from #7796, in dry runs too. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
tlgimenes
changed the base branch from
feat/blocks-v8-support
to
feat/sandbox-content-protocol
October 8, 2026 15:46
4 tasks done
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
This was referenced Oct 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack
One Studio stack, each PR based on the one before it:
#7796 (org_sites project link, base:
main) → #7728 (blocks v8 support) → #7770 (sandbox content protocol) → #7766 (hosted)This PR: #7766, based on #7770.
Restack onto #7796
ba3a5a7be: merged feat: support Blocks v8 (content protocol) behind a flag #7728 (with feat(sites): link org_sites to projects — site ids are immutable and never reused #7796 and currentmain) and moved hosted ownership onto the link:ownedProjectSitereadsgetByProject(hosted routes, decofile scope, sandbox-less draft status).claim-site.ts: claim = link once; reserved (tombstoned), other-org and other-project slugs are refused. The admin backfill reportslinked / alreadyLinked / refused / ambiguous.main(feat(publish): one before/after publish dialog for Fast Preview and sandbox #7792) replacedcms-publish-popover.tsxwithpublish-dialog.tsx, so the hosted changes (no review mode and no Request approval for a hosted draft, publish via the hosted action) moved intopublish-dialog.tsx.mainremoved the projectgittab; only the hostedreleasestab is kept.sandbox-proxy.ts:/git/suggest-commitis gone, and judge-review keeps the hosted-awarefastPreviewStatus/fastPreviewDiffbackfill.create.tsgoes throughclaimProjectSite.claim-site.test.tsrewritten for link semantics (tombstone, other project, second slug, races, backfill ambiguity), plus two integration cases inorg-sites-link.integration.test.ts(an unowned slug is claimed and linked; a deleted org's slug stays reserved). The e2e "not-owned siteSlug" case now asserts the change is refused and hosted keys stay on the linked site.d9cdbab8f: merged feat(sandbox): serve the content protocol from the sandbox daemon #7770 so the stack is one line. One doc-comment conflict incontent-protocol-api.ts.1a735835e: merged feat(sites): link org_sites to projects — site ids are immutable and never reused #7796's review fixes up the stack (no conflicts).a7afc8457:claim-site.tsmaps the newrelink_requires_adminrefusal to its ownrelink-requires-adminreason (also answered on dry runs), andnot_foundtonot-foundinstead ofother-org(superseded byca7672303).4a53858a7: merged feat(sites): link org_sites to projects — site ids are immutable and never reused #7796's PO decisions (2026-10-08).ca7672303:claim-site.tsdrops therelink-requires-adminreason. A same-org slug whose project was deleted is linked again, and another org's slug is refused (other-org).claim-site.test.tscovers both.d0e9efdc1merged feat(sites): link org_sites to projects — site ids are immutable and never reused #7796's comment tidy-up (no conflicts);a68e1acc5rewraps theclaim-site.tsheader comment.bun run test10605 pass, 0 fail; hosted, virtual-tool, migration and lifecycle integration tests on a throwaway Postgres: pass. The e2e hosted spec was not run locally.What
Hosted Deco CMS for Blocks v8 projects on GitHub (
blocksMajor === 8, org flagsite_editor_content_protocol). v7 is unchanged.Site ownership. Every hosted path (the hosted routes, the hosted branches of
decofile.tsand the sandbox-less git routes insandbox-proxy.ts) uses the project'sorg_siteslink from feat(sites): link org_sites to projects — site ids are immutable and never reused #7796 (ownedProjectSite(orgSites, projectId, orgId)inhosted/scope.ts, which readsgetByProject), nevermetadata.siteSlug. A project that isn't linked to a site its own org owns has no hosted features (404), so nobody can write another org's delivery objects or drafts, or mint its tokens. The slug itself can't change (feat(sites): link org_sites to projects — site ids are immutable and never reused #7796 refuses it in the API). The hosted routes also apply project-scoped roles (isProjectAllowed).Site links. Creating or importing a project links it to its
metadata.siteSlugonce (linkProjectSite/claimProjectSiteinhosted/claim-site.ts). A slug no org owns is claimed for the project's org first, then linked. Nothing is linked (project creation still succeeds, the slug stays unlinked and gets no hosted features) when the slug is reserved (its org was deleted: never reused), another org owns it, another project already has it, the project already has a different site, deco.cx has a site by that name (the deco import claims those after proving access), or a project of another org already names an unowned slug. The deco.cx lookup is bounded at 3 s and fails closed.POST /api/_admin/hosted/site-claims/backfilllinks existing v8 projects the same way: dry run unless{"dryRun": false}, idempotent, and it reportslinked,alreadyLinked,refused(with the reason) andambiguous(an unowned slug several orgs name, or a slug several projects of one org name with none linked yet).CDN drafts. The v8 editor's saves go to
sites/<site>/drafts/<slug>.json({ set, delete },Cache-Control: no-cache, max-age=0, must-revalidate) on the delivery bucket through its S3 API. No git commit; the last writer wins./rpcnow reads main with the draft layered on (hosted/draft-content-storage.ts). The slug is random (16 bytes, base64url) and kept in the org KV per (project, branch). The preview pointer is<delivery host>/sites/<site>/drafts/<slug>.json@<etag>, answered by the sessionGET /decofile/:vmcp/:branchfor a v8 project.Publish.
POST /decofile/:vmcp/:branch/publishfor a v8 project: commit the draft directly to main (fast-forward only) → delete the draft (the merge is done) → write the commit's companion releaserevisions/<sha>.json({ revision, schemaHash, blocks }, immutable) → make it current: re-read main's head and, only if it is still that commit,PUT latest.json({ revision, schemaHash, publishedAt }) and purge that URL from Cloudflare's edge (5 s timeout per attempt, exactly one retry, no restore). A failed commit fails Publish and keeps the draft. The answer is{ result: "merged", sha, release: "current" | "created" | "none" }→ "Merged · Current on the CDN" / "Merged · release created, making it current failed — use Make current on Releases" / "Merged · no release created (the next Publish includes these changes)". A draft that commits nothing (empty, or byte-equal to main; such blocks are also dropped from the popover's change list) answers{ result: "up-to-date" }→ "Up to date, nothing to publish", deletes the draft and never toucheslatest.json. Only the published save is deleted (ETag re-checked under the draft lock); a save made during the publish keeps the draft.schemaHash = sha256Hex(canonicalJson(schema.gen.json)), pinned to the same test vector asdeco content.Releases (new project view next to Assets): a timeline of main's commits (GitHub integration, one page of 50), newest first, joined to one R2 listing of
sites/<site>/revisions/per page (no per-commit HEADs). A commit with a companion release has Make current; one without (a developer push, or a Publish whose release write failed) is just a merge: no badge, no action. Current marks exactly the commitlatest.jsonnames, read fromlatest.json, never inferred; with nolatest.jsonthe header says "Nothing on the CDN yet". Make current writeslatest.jsonfor that companion and purges it; it succeeds or fails at once (502{ error: "latest-update-failed" }, shown as a localized toast), and the screen refetches. The Current row keeps Make current too, so a failed purge can be retried on the commitlatest.jsonalready names, so Current shows whateverlatest.jsonsays. It warns when the target's schemaHash differs from main's. Everylatest.jsonwrite setspublishedAtto now: the SDK prefers the CDN only whenpublishedAtis later than its bundle's build time.Site tokens (Settings → Site): issue and list, for v8 sites only (issuing checks main's schema). An Ed25519 JWS
{site, kid, iat}, shown once, signed withDECO_SITE_TOKEN_SIGNING_KEY. Issuing always works (no limit). There is no revocation and no kill switch (product owner decision): the edge only verifies the signature and stamps rows with the token's site.The managed-asset CDN default moves from
decoims.comtoassets.decocms.com.Deleted: the v8 git-branch draft machinery:
GET /:branch/changes,draft-changes.tsand its tests,mergeBaseon the content clients, the v8 write side ofrepo-content-storage.ts(it is now a read-only reader of main), the v8"/changes"pointer suffix and the draft token for v8.The publish popover keeps working unchanged: for a v8 project the sandbox-less
/git/status|diff|discardroutes answer from the draft (hosted/draft-publish-status.ts:loadHostedDraft,hostedDraftPublishStatus,hostedDraftPublishDiff; routes and response shapes unchanged).New Studio env (infra)
DELIVERY_R2_ACCOUNT_ID,DELIVERY_R2_ACCESS_KEY_ID,DELIVERY_R2_SECRET_ACCESS_KEY,DELIVERY_R2_BUCKETDECO_SITE_TOKEN_SIGNING_KEY(Ed25519 private key, base64 PKCS8)CF_DELIVERY_ZONE_ID(zonedecocms.com),CF_PURGE_API_TOKEN(Zone → Cache Purge on it). Unset, the latest.json purge is skipped with a warning (local/dev/e2e); publish never fails for it.DELIVERY_R2_ENDPOINT,DELIVERY_PUBLIC_ORIGINWithout the delivery settings, the v8 hosted routes (and
/rpc) answer 503 "hosted delivery not configured".OPEN (smallest choice taken, marked
// OPEN:in code)v8-draft:<project>:<branch>, so there's no migration. Saves in one process are serialized per draft; across pods the last writer wins.{kid, iat}are kept in the org KV assite-tokens:<site>.GET /decofile/:vmcp/:branchanswers{ draft, version }in place of v7's decofile, token and API host. For v8 detection it reads main'sschema.gen.json, but only on flag-on orgs with delivery configured./api/:org/hosted/:vmcp/*. Publish reuses/decofile/.../publish.listCommitsneedssince. It is the epoch, paged 50 at a time by cursor.openssl genpkey -algorithm ed25519).schemaHash. There is no R2 metadata field.latest.jsonPUT is plain after the head check.kidis 16 random bytes, base64url./api/_admin,DEPLOYMENT_ADMIN_EMAILS), not an@deco.cxcheck.draftGitDiscardkept its name (the decision renamed only status/diff).siteSlugcouldn't be claimed (a conflict above, or a slug changed later viaCOLLECTION_VIRTUAL_MCP_UPDATE) gets no hosted features until an admin claims it (// OPEN:inhosted/scope.ts).hosted/draft-git-compat.ts→hosted/draft-publish-status.ts(+test),fastPreviewHostedDraft→loadHostedDraft(now inhosted/;sandbox-proxy.tskeeps a thin call),draftGitStatus/draftGitDiff→hostedDraftPublishStatus/hostedDraftPublishDiff. It's reached only by the sandbox-less Fast Preview session; sandbox sessions of v8 projects edit over the daemon's/_sandbox/rpc.tool-io.tswas hand-patched (adding"releases"to the sidebar-view enum).generate:tool-contractsproduced a broken file in this environment, so it should be regenerated in CI or a clean checkout.Rollout
Run the backfill with
{"dryRun": false}right after deploying, before announcing the feature: until it runs, an existing v8 project's site is unclaimed, and although the create path no longer claims a slug another org's project names, those existing projects get no hosted features until claimed. Do a dry run first and resolve the reportedconflicts/ambiguousslugs by hand (admin claim).Verification
apps/apitsc --noEmit,apps/web/packages/e2e/packages/sharedtypecheck,oxlint,knip,biome format: pass.bun run test: 10493 pass. One failure,duplicate-check.test.ts(task-board), passes when run on its own. 41 new tests inapps/api/src/hosted/, covering:deco content;content-protocol-github.spec.ts: 12/12 passed, against a new in-memory delivery stub (S3 API plus the public origin with ETag/304) and a GraphQL commit-history answer on the GitHub stub. It covers conformance, saves into the CDN draft, the pointer with 304 revalidation, publish/releases/rollback/resync, and site tokens.decofile-api,cms-publish-*,fast-preview-git-sync,content-protocol-deco-serve): 28 passed. 1 failed,deco-serve › refuses a link to a server off this machine: the anonymous/site-editorpage was still on the splash after 5 s. That code isn't touched here, and it wasn't reproduced on the base branch.apps/apiandapps/webtypecheck,oxlint,biome format, and the hosted, decofile and route unit tests (207 pass). A new e2e case checks that asiteSlugthe org doesn't own gets 404 on the hosted routes; the e2e fixture claims each test site inorg_sites. (Since the restack: changing a project's site is refused, and its hosted keys stay on the linked site.)content-protocol-github.spec.ts: 13/13 passed after merging the base branch. The GitHub ETag cache always revalidates (If-None-Matchon every GET), so the publish head check never reads a stale head.apps/apitypecheck,oxlint,biome format,apps/api/src/hostedunit tests (57 pass, incl. 2 newclaimProjectSitecases); theconnectionsquery was checked read-only against a local Postgres;bun run test10513 pass, 1 fail (hosted-harness-workflow.test.tsheartbeat timing; passes alone); e2econtent-protocol-github.spec.ts14/14 passed.apps/api,apps/web,packages/e2etypecheck,oxlint,biome format: pass.apps/api/src/hostedunit tests 67 pass (new: the state rule incl. developer push, older revision, revision off main, the 5×50 walk cap with "no CMS-published release", Resync without asking over a developer push and asking over a rollback, reusing an existing revision,publishedAtstamped on Publish/Make current/Resync).bun run test10523 pass, 1 fail (circuit-breaker.test.tstiming; passes alone). e2econtent-protocol-github.spec.ts(one worker): 14/14 passed, now also asserting the new fields and that rollback and Resync restamppublishedAt.On published @decocms/[email protected]
feat/blocks-v8-support(4dfe9ce, then again with main after it):@decocms/blocksis pinned exactly at8.1.0-next.7(published, dist-tagnext) in api, web and e2e;bun.lockdiffers only in the@decocms/blocksentries. next.7 is the SDK side of hosted releases (readslatest.json, swaps only whenpublishedAtis newer than the bundle'scommittedAtandschemaHashmatches).walkMaininhosted/releases.tsis no longer exported (it's only used in that file; knip flagged it) (40ed33b).latest.json edge cache + purge (1ca3832, fail fast e0b4fd7, one retry e84cb5d)
latest.jsonis written withCache-Control: public, max-age=0, s-maxage=3600, must-revalidate: the edge holds it up to 1 h, browsers and servers revalidate every poll (ETag). Revisions staypublic, max-age=31536000, immutable, draftsno-cache, max-age=0, must-revalidate. No new cache rule: the existing decocms.com rule respects origin headers.latest.jsonwrite (Publish, Make current; Resync was later removed) Studio purges exactly<delivery origin>/sites/<site>/latest.jsonviaPOST /zones/<zone>/purge_cache {"files":[…]}(hosted/delivery-purge.ts): each attempt bounded byAbortSignal.timeout(5000)(a timed-out attempt counts as failed), and exactly one retry: two attempts at most. Order: commit → revision → head check → latest.json → purge. Fail fast: a failed latest.json write (the purge is then skipped) or a purge whose two attempts both failed (or timed out) throwsLatestUpdateError, with no restore of the previous pointer and no automatic recovery. Publish: the commit stands and Publish is done (draft already deleted), answeringcdn: "failed". Make current / Resync: 502 with a clear message ending in "Try again", shown as the existing error toast. The user retries from Studio (Resync, Make current); the 1 hs-maxagestill bounds how stale the edge can be.mergedwithcdn: "failed"and the draft deleted): exact header values; the purge request (endpoint, bearer, exact URL, configured origin, a timeout signal); first attempt fails + retry succeeds → success (two calls); both attempts fail → failure after exactly two calls; 200 withoutsuccessfails; a timed-out attempt counts as failed and is retried; both time out → failure after two calls; skip with a warning when unconfigured; purge logged right after each pointer write on Publish, Resync and Make current; purge failure → Publish pending, one purge call, no restore, draft kept; with the real purge, both attempts failing → Publish pending after exactly two calls, no restore, draft kept, and a successful retry → published, draft deleted; latest.json write failure → no purge, pending, draft kept; Resync and Make current failures surfaceLatestUpdateError("Try again") with no restore, and a Resync retry succeeds; success path unchanged (draft deleted).apps/api/src/hosted85 pass;apps/apitsc --noEmit,oxlint,biome format,knip,apps/webtsc --noEmit: pass.rollout churnthread scenario timed out once, unrelated to hosted, and passed on rerun). Test/e2e workflows only run on PRs tomain, so they don't run on this stacked PR.Publish is done once merged (543af34)
Product decision: "the publish flow is considered as 'done' when the merge is successful."
hosted/publish.ts: the draft is deleted right after a successful commit, before the CDN step; the CDN step's failure no longer fails Publish.PublishResultis{ result: "merged"; sha; cdn: "live" | "failed" } | { result: "up-to-date" }(waspublished/pending).hosted/releases.ts:ReleaseStateislive | failed | rolled-back, derived byreleaseState(current, head, headDate). DroppedwalkMain,HISTORY_WINDOWand theunpublishedCommits/revisionOffMain/noRecentReleaseflags. Resync returns{ sha, cdn }.latest.jsonwas written still reads Live on Releases (the pointer names the head), while the edge may serve the old pointer for up to 1 h. A developer's push (or one with an old committer date) shows Failed until resynced.apps/api/src/hosted85 pass (draft deleted after the commit when the revision write, latest.json write or purge fails, or main moved; draft kept when the commit fails; Merged·Live vs Merged·Failed results; derived state for live / newest-not-live / missing latest.json / rolled back / whole-second compare; Resync flips to Live).apps/webrelated tests 603 pass.tsc --noEmit(api, web),oxlint,biome format,knip: pass.Releases is a timeline; Current is what latest.json names (796b9e9)
Product decision: "current" means current on the CDN; a merge without a publish is just a merge.
ReleaseState(Live / Failed / Rolled back / Not live), the commit-date heuristic,releaseStatus, the "Failed · Resync" badge, Resync (routePOST /hosted/:vmcp/resync,resync(),RolledBackError,useResync, its dialog and i18n). Make current on the newest release covers what Resync did; commits without a release are not meant to get one after the fact.latest.json(a slower Publish must not move the pointer back past a newer commit). When it trips, the result isrelease: "created".ReleasesPageis{ current, commits: [{ sha, date, message, author, hasRelease }], nextCursor }.draftFileChangesskips a block whose serialized content is byte-equal to main's file, so a no-change publish commits nothing.apps/api/src/hosted78 pass (timeline with/without companions, Current from latest.json, missing latest.json, one prefix listing and one latest.json read per page; Make current success, failed purge, failed write; Publish current / pointer fails / main moved / companion fails / no-change and empty drafts leave the pointer).tsc --noEmit(api, web),oxlint,biome format,knip: pass. The e2e hosted spec was updated to the new shapes (not run locally).Review fixes (eea8156)
latest.jsonwas written can be retried from Releases; the API already re-purges the current sha.latest.jsonwrite or purge on Make current returns{ error: "latest-update-failed" }; the web maps it toreleases.makeCurrentFailed(en "Making it current failed. Try again." / pt-br "Não foi possível torná-la atual. Tente de novo.").GET /releasesandPOST /releases/current(the removed/resyncroute always 404'd).apps/api/src/hosted78 pass;tsc --noEmit(api, web),oxlint,biome format,knip(api, web): pass; web i18n/tab tests 261 pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig