feat(pages): publish without a workspace
Links
- Issue: SUPER-2334
Summary
workspaceIdis no longer required to publish a page. With a workspace, nothing changes; without one, the publish goes through and returns a page id.- The CLI's
No workspace to publish intorefusal is gone, andpages_publishtakesworkspaceIdas optional. - Because an unlinked page has no entry-path key, the returned id is the only way back to it — so the CLI now prints it along with the exact
--pagecommand, and the tool description tells the model to keep it.
Why / Context
Workspaces should be optional, not required. There are cases where a page can't be tied to one at all — publishing from a chat session being the clearest.
That case was genuinely blocked. McpContext carries userId and organizationId but no workspace (packages/mcp/src/auth.ts:7-17), and the same is true of the in-memory client used by server-side integrations (packages/mcp/src/in-memory.ts:67). Yet pages_publish declared workspaceId as required. A session's only route was to call workspaces_list, pick something, and file the page under a workspace unrelated to the conversation. With no workspaces at all, it couldn't publish.
The guard's stated rationale was that an unanchored publish "mints a page no workspace lists and no later publish can find." Half of that was already untrue: page.list only inner-joins workspace_pages when you scope to a workspace (packages/trpc/src/router/page/page.ts:283), and the desktop Pages view queries it unscoped (PagesView.tsx:54), so an unlinked page shows up there today.
How It Works
No schema change. The workspace link already lives in its own join table (workspace_pages), not as a column on pages, so an unlinked page was always representable. resolveTargetPage already returns null for an unanchored publish and runPublish mints a page from there — the mechanism degrades on its own. The only thing in front of it was the isAnchoredPublish refine.
So the mechanism stays and the requirement goes:
- With
workspaceId+entryPath: unchanged. Republishing the same path resolves to a version. - Without: skip the lookup, mint a page, return the id.
pageIdis then the only key.
hasCompleteWorkspaceLink is kept, so the two fields remain all-or-nothing — you still can't send half the key.
Manual QA Checklist
CLI
-
superset pages publish report.htmlinside a workspace → v1, then again → v2 of the same page (unchanged behaviour) -
superset pages publish report.htmlwith no$SUPERSET_WORKSPACE_IDand no--workspace→ publishes, prints the page id and the--pageline - Following that printed
--page <id>command produces v2 of that page -
--workspace <id>without being in a workspace still anchors normally - Directory publish (assets) with no workspace works — assets stage against the page
resolvePageIdcreates
MCP / session
-
pages_publishwith noworkspaceIdsucceeds and returns a URL -
pages_publishwithworkspaceIdbut noentryPathstill fails with the pairing message - The published page appears in the desktop Pages view
Regression
- Page comment watching still registers when publishing from a terminal in a workspace
Testing
bun run typecheck— 42/42 passbun run lint:fix— cleanbun run test --filter=@superset/cli --filter=@superset/mcp --filter=@superset/trpc— 0 failbun run check:i18n— no catalog churn (CLI/MCP strings aren't Lingui-managed)bun run check:plugins— cleanbun test ./src/router/page/publish.integration.tsinpackages/trpc— new case passes
Not yet done: the Manual QA checklist above is unchecked — I verified this at the unit/schema level and by reading the call paths, but have not driven a real publish end-to-end from either the CLI or a session.
Design Decisions
- Why no session-scoped anchor instead:
chat_sessionsexists and its ownworkspaceIdis already nullable, so threading asessionIdthrough as a second anchor kind was the obvious alternative. It needs plumbing intoMcpContextplus a polymorphic rewrite ofworkspace_pages, and it buys little: a session already holds thepageIdin context for exactly its own lifetime, which is the versioning key. A page filed under a dead session is also less findable than one filed under nothing, since something then has to render that container. - Why not key on
(createdByUserId, entryPath): would make republish-by-path idempotent from anywhere, but needs a nullableworkspace_id, a changed primary key, and a partial unique index (Postgres doesn't collide NULLs in the existing one). Available later without undoing this.
Known Limitations
-
An unanchored republish creates a new page rather than a version. The failure mode is real: publish, get asked for a change, republish without the id → two pages, and the link already shared still shows the first. Mitigated by printing the id and the
--pagecommand, and by the tool description, but not prevented. -
An unanchored page can never be adopted into a workspace later.
runPublishonly inserts intoworkspace_pageswhen!input.pageId(packages/trpc/src/router/page/publish.ts:233), and the CLI sends...(pageId ? { pageId } : (link ?? {})), so--page <id>from inside a workspace never carries the link. Publish from a session, later move the work into a workspace and republish by id, and the page stays invisible tosuperset pages list --workspace <id>permanently.This is a deliberate non-fix, not an oversight. The behaviour predates this PR and is asserted on purpose by
pageId does not bind the entry_path to the targeted page—--pageis the explicit "works anywhere" escape hatch, and having it silently create workspace links would make it surprising. What this PR changes is reachability: unanchored pages couldn't exist before, so nothing could be stuck this way. The cost is narrow (the page is still listed org-wide, which is what the desktop Pages view queries, still has a stable URL, and still versions by id — only the workspace-scoped list misses it). Overturning a deliberate assertion belongs in its own PR with its own argument, and nobody has asked for adoption yet. -
Comment watching still requires a workspace. No regression — it also requires
SUPERSET_TERMINAL_ID, andpackages/host-service/src/terminal/env.ts:257-265sets both unconditionally in the same block. An unanchored publish therefore implies no terminal id, so there was never a watch session to skip.
Risks / Rollout
- Risk: low. Removes a validation refusal; no schema, no data migration, no change to any anchored path.
- Rollback: revert the commit. Pages created unanchored in the meantime remain valid rows — they'd simply become unpublishable-to again, still readable and listed.
Summary by cubic
Resolves SUPER-2334 by letting the CLI and pages_publish publish pages without a workspace instead of rejecting them, so a chat session or shell with no workspace to name can publish. Workspace-and-entryPath publishes still create versions; unanchored publishes create a new page unless a later publish uses its ID.
- Makes
workspaceIdoptional while requiring it andentryPathtogether when either is provided. - Reports whether the page is linked and provides a runnable
--pagecommand for unanchored publishes in CLI JSON and MCP results. - Deletes a newly created page after a failed directory publish only if it has no versions, preserving pages whose publish already committed.
- Includes the page id in the page-watch prompt so a watching agent republishes onto the same page instead of forking it, and fixes the prompt and a CLI error to name the
--threadflag (--threadIdsilently did nothing). - Returns
workspaceLinksfromget, so a lost source can be pulled back to its workspace path and versioned again. - Updates the page docs and skill guidance.
Summary by CodeRabbit
-
New Features
- Pages can be published without a workspace or existing page ID.
- Standalone publish results identify unanchored pages and provide a ready-to-run command for future versioning.
- Republishing without that command or a page ID creates a separate page.
- Workspace-based publishing continues to support versioning through the entry path.
- MCP page publishing provides equivalent standalone republishing guidance.
-
Documentation
- Updated publishing guidance to explain workspace-free pages and versioning behavior.