Skip to content

feat(cloud): publish a folder as a Deepnote app - #609

Draft
tkislan wants to merge 2 commits into
tomaskislan/blu-6519-sync-a-local-folder-with-deepnote-cloud-from-the-extensionfrom
tomaskislan/blu-6520-publish-a-folder-as-a-deepnote-app
Draft

tkislan wants to merge 2 commits into
tomaskislan/blu-6519-sync-a-local-folder-with-deepnote-cloud-from-the-extensionfrom
tomaskislan/blu-6520-publish-a-folder-as-a-deepnote-app

Conversation

@tkislan

@tkislan tkislan commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai ignore

Summary

Adds "Deepnote: Publish App…", run from the Command Palette or from a folder's context menu in the file explorer. It publishes the folder with publishApp from @deepnote/cloud-sync into a Deepnote project's app folder, then shows the app URL with "Open" and "Copy".

  • Project: when the folder is inside a project of a sync manifest, that project is used without asking. Otherwise the user picks one from the workspace's projects, sorted by name, with folder paths as descriptions.
  • API access: the user chooses on or off. The project and this choice are saved per folder in workspace state.
  • Republishing: a folder with saved settings shows one modal, with "Publish" and "Change Settings…". "Change Settings…" lists the current choices first, marked as current.
  • Results: a full success shows the URL. Failed file operations show a count with "Show Log" and no URL. A sync mirror that could not be fully updated gets its own warning. Errors go through showCloudError.
  • App files changed in Deepnote (PublishDivergedError): a modal lists the paths and offers two choices; dismissing it changes nothing.
    • "Force" publishes again with force: true.
    • "Sync First" runs "Sync with Deepnote Cloud" for the sync root with working files, and does not publish again. It then names the .files/_deepnote_static folder that now holds Deepnote's versions. If the sync did not download the project's files, it says so instead.
  • Sync command argument: deepnote.syncWithDeepnoteCloud takes an optional { root, allFiles } argument.
    • With it, the folder pick is skipped, every other check still runs, and the command resolves its WorkspaceSyncResult.
    • From the Command Palette and the explorer title bar it behaves as before, and passes allFiles: false.
  • One operation at a time: a lock (IDeepnoteCloudOperationLock) replaces the sync command's private in-progress flag.
    • Sync and every publish into a synced folder share it, so they never write .deepnote-sync.json at the same time.
    • Either one refuses with "A sync or app publish with Deepnote Cloud is already in progress." while the other runs.
    • A folder outside any sync root never takes the lock.
    • The divergence modal appears only after the lock is released.
  • Availability: the command is disabled in untrusted workspaces and in the web extension host. The context-menu entry has the same condition.
  • E2E:
    • The mock in test/e2e/helpers/mockDeepnoteCloud.ts now serves the routes publishApp calls: GET and PATCH /v2/projects/{id}, and POST and DELETE /v2/files. Multipart is parsed with Node's built-in Request.formData(), so no new dependency.
    • A new publishApp.e2e.test.ts in the cloud group:
      • publishes a folder from the explorer context menu;
      • republishes it with the saved settings;
      • publishes a folder inside a synced project, edits the app file in the mock, and resolves the divergence with "Force".

Closes #608

Builds on #601 (BLU-6519, sync), which builds on #563 (BLU-6518, the token service). Both must merge first.

Deviations from the issue

  • Sync First wording: the modal detail and the README say that Sync First syncs the whole synced folder, including every project's working files. syncWorkspace has no per-project filter, so a project with a working file over 100 MiB makes that sync fail. The issue's wording implied only the app files come down.
  • README: it also says the following, which the issue's README list left out:
    • files deleted from the folder stay published;
    • a folder outside a synced project overwrites cloud edits without asking;
    • .env.* files are refused too;
    • a .env token wins over the saved one.
  • Project names in notifications: "Sync First" neutralizes ]( in the project name before it goes into the non-modal warning, as the token service does for workspace names. Notifications render [label](command:…) as runnable links, and the name comes from the API.
  • working-files-changed conflicts: they are still skipped, but no longer logged as "unexpected". Sync already warns which files it kept.
  • Small departures from the issue's spec:
    • The mock answers PATCH validation errors with 400, as the API spec does, not 422.
    • result.errors are not logged a second time, because each one already arrived as a logged operation-failed event.
    • No commandPalette entry: enablement already hides the command there, as for "Sync with Deepnote Cloud".
    • The lock has no double-dispose guard, because both callers dispose once in finally.
    • The README table row drops "then open or copy its URL", so the table keeps its column width.
  • Unit tests:
    • The skipped-conflict Sync First case was cut, because it covers the same code path as the pushed case.
    • Added: a folder beside a synced project (Team/SalesArchive), an unreadable manifest, exactly ten changed paths, and the link neutralization.
  • E2E workarounds:
    • The suite turns off explorer.compactFolders, because a compacted Sales/app row publishes Sales.
    • It reads the outcome from the Notifications Center, because a toast can hide before a poll sees it.

Verification

  • npm run typecheck, npm run lint (no findings in the changed files), npm run format and npm run spell-check all pass.
  • npm test: 3025 passing, 0 failing.
  • E2E: the cloud group (19 tests, 3 of them new publish tests) passed in two consecutive runs with the final code, under Xvfb. The publish suite alone passed four more runs in a row.
  • Mutation testing: 26 single-line bugs were applied to the compiled handlers, the lock and the URL helper, one at a time.
    • Every one failed at least one test except two. One was an in-place sort with no observable effect. The other was the double-dispose guard, which was then removed.
    • The test cleanup pass ran another round on the source and closed the gaps it found.
  • The new link-neutralization test fails when the neutralization is reverted.

Not verified

  • No real Deepnote: no real workspace was used, and the issue's manual follow-up is not done. That covers a Vite app, divergence with Sync First, Cursor, and the public docs PR.
  • Sync First: covered by unit tests only. The E2E suite covers Force.
  • Windows: not run.
  • Known from review and not addressed here:
    • Web host before activation: !deepnote.webExtension is true until the extension activates. So in vscode.dev the context-menu entry can show before activation, and clicking it fails with "command not found". The existing commands use the same condition.
    • Unreadable manifest: the settings prompts run first, and the publish then fails with publishApp's message. That message suggests the CLI flag --no-sync-root.
    • Failed sharing step: it is counted in "file operation(s) failed".

@linear-code

linear-code Bot commented Nov 7, 2026

Copy link
Copy Markdown

BLU-6520

@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.02326% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 38%. Comparing base (fa8e800) to head (61f9311).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...ks/deepnote/cloud/publishAppCommandHandler.node.ts 92% 8 Missing and 3 partials ⚠️
src/notebooks/serviceRegistry.node.ts 0% 2 Missing ⚠️
...c/notebooks/deepnote/cloud/publishedAppUrl.node.ts 85% 0 Missing and 1 partial ⚠️
.../cloud/syncWithDeepnoteCloudCommandHandler.node.ts 92% 1 Missing ⚠️
Additional details and impacted files
@@                                               Coverage Diff                                                @@
##           tomaskislan/blu-6519-sync-a-local-folder-with-deepnote-cloud-from-the-extension    #609    +/-   ##
================================================================================================================
  Coverage                                                                               38%     38%            
================================================================================================================
  Files                                                                                  826     829     +3     
  Lines                                                                                41427   41631   +204     
  Branches                                                                              9115    9152    +37     
================================================================================================================
+ Hits                                                                                 15952   16140   +188     
- Misses                                                                               23395   23407    +12     
- Partials                                                                              2080    2084     +4     
Files with missing lines Coverage Δ
...books/deepnote/cloud/deepnoteCloudOperationLock.ts 100% <100%> (ø)
src/notebooks/deepnote/cloud/types.ts 100% <100%> (ø)
src/platform/common/constants.ts 100% <100%> (ø)
src/platform/common/utils/localize.ts 93% <100%> (+<1%) ⬆️
...c/notebooks/deepnote/cloud/publishedAppUrl.node.ts 85% <85%> (ø)
.../cloud/syncWithDeepnoteCloudCommandHandler.node.ts 94% <92%> (-2%) ⬇️
src/notebooks/serviceRegistry.node.ts 0% <0%> (ø)
...ks/deepnote/cloud/publishAppCommandHandler.node.ts 92% <92%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Add "Deepnote: Publish App…", from the Command Palette and the file
explorer's folder context menu. It publishes a folder with publishApp
from @deepnote/cloud-sync into a project's app folder and shows the app
URL with Open and Copy. The project comes from the sync manifest when
the folder is inside a synced project, and from the workspace's project
list otherwise. The project and the API-access choice are saved per
folder in workspace state.

When app files changed in Deepnote since the last sync, a modal offers
Force, which publishes again with force, and Sync First, which runs
"Sync with Deepnote Cloud" for the sync root with working files. For
this, deepnote.syncWithDeepnoteCloud takes an optional { root, allFiles }
argument and resolves its result.

A lock shared by sync and every publish into a synced folder replaces
the sync command's private in-progress flag, so the two never write
the sync manifest at the same time.
The mock serves the routes publishApp calls: GET and PATCH
/v2/projects/{id}, and POST and DELETE /v2/files. Working files live in
each project's files/ directory; sharing settings are kept in memory.
An upload to a taken path is stored at a new path, as Deepnote does.

The new suite publishes a folder from the explorer context menu,
republishes it with the saved settings, and resolves an app file edited
in the mock with Force. setApiToken moves to a shared helper.
@tkislan
tkislan force-pushed the tomaskislan/blu-6520-publish-a-folder-as-a-deepnote-app branch from bb5748f to 61f9311 Compare November 7, 2026 16:24
tkislan added a commit to deepnote/deepnote that referenced this pull request Nov 8, 2026
…hing

The mock now stores a project's static-file sharing and API access
settings in memory, and GET /v2/projects/{projectId} reports them, so
publishApp runs against it end to end. Enabling API access without
sharing gets a 400, and disabling sharing disables API access, as in
vscode-deepnote's e2e mock (deepnote/vscode-deepnote#609).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Y9ynBkZNNHhdxuKcXL5Es
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