feat(pages): publish without a workspace

Merged
HA
2mo ago
+428-81

Links

Summary

  • workspaceId is 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 into refusal is gone, and pages_publish takes workspaceId as 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 --page command, 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. pageId is 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.html inside a workspace → v1, then again → v2 of the same page (unchanged behaviour)
  • superset pages publish report.html with no $SUPERSET_WORKSPACE_ID and no --workspace → publishes, prints the page id and the --page line
  • 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 resolvePageId creates

MCP / session

  • pages_publish with no workspaceId succeeds and returns a URL
  • pages_publish with workspaceId but no entryPath still 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 pass
  • bun run lint:fix — clean
  • bun run test --filter=@superset/cli --filter=@superset/mcp --filter=@superset/trpc — 0 fail
  • bun run check:i18n — no catalog churn (CLI/MCP strings aren't Lingui-managed)
  • bun run check:plugins — clean
  • bun test ./src/router/page/publish.integration.ts in packages/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_sessions exists and its own workspaceId is already nullable, so threading a sessionId through as a second anchor kind was the obvious alternative. It needs plumbing into McpContext plus a polymorphic rewrite of workspace_pages, and it buys little: a session already holds the pageId in 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 nullable workspace_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 --page command, and by the tool description, but not prevented.

  • An unanchored page can never be adopted into a workspace later. runPublish only inserts into workspace_pages when !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 to superset 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 — --page is 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, and packages/host-service/src/terminal/env.ts:257-265 sets 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 workspaceId optional while requiring it and entryPath together when either is provided.
  • Reports whether the page is linked and provides a runnable --page command 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 --thread flag (--threadId silently did nothing).
  • Returns workspaceLinks from get, 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.