From 192fecc86f54cf2a69ec7dddc466139b40933ac9 Mon Sep 17 00:00:00 2001 From: wendrul <53628737+wendrul@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:45:34 +0100 Subject: [PATCH] fix: create git branch right before creating the workspace fork to catch errors and have a coherent fork point (#7073) * Workspace forks: add endpoint to create a branch before creating a fork * Update hubPaths + create branch before creating fork on frontend * Update tmp ee-repo-ref * Remove debug hubPath * Prepare sqlx * Fix ee imports * Update ee-ref * Update ee-repo-ref final * Prepare sqlx --- ...1f7f387f5055c47f493271d26731336257384.json | 10 +- ...45e414dd646c912387ee63609d9f5611690f.json} | 4 +- ...f2ee9d2d3412d4a2c96c6ccc87ec1c382384.json} | 4 +- ...0cb549a34b96554ae1872355b90304f5dcb76.json | 4 +- ...95c7cda0cba0b119d4fdb8a0dea1b28a1d560.json | 22 ++++ ...069c0ad82b27b41171825928a938cfa8316f.json} | 4 +- backend/ee-repo-ref.txt | 2 +- backend/windmill-api/openapi.yaml | 26 ++++ backend/windmill-api/src/workspaces.rs | 27 +++- backend/windmill-git-sync/src/git_sync_oss.rs | 11 ++ backend/windmill-git-sync/src/lib.rs | 2 +- .../workspaceSettings/CreateWorkspace.svelte | 117 +++++++++++++++--- frontend/src/lib/hubPaths.json | 3 +- 13 files changed, 195 insertions(+), 41 deletions(-) rename backend/.sqlx/{query-e8e33f599eae064011232f9f715e676d0c8ae31982865cb2c8103ed735c42c69.json => query-6f49a47b0b77720b0e8a2ddbd46e45e414dd646c912387ee63609d9f5611690f.json} (54%) rename backend/.sqlx/{query-2e131a019051bdac7c9c65f7c504cfba31cfdd64a0f68001e123694ed5cde5ed.json => query-8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384.json} (55%) create mode 100644 backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json rename backend/.sqlx/{query-e572fa64eec9188368d7c271ac7ecd6b45dc161423abab22cddb2b13f6fb9833.json => query-f844026597d2e1c59a80dc8e1ddd069c0ad82b27b41171825928a938cfa8316f.json} (54%) diff --git a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json index e7ed0aee65..d29a18c691 100644 --- a/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json +++ b/backend/.sqlx/query-07168aaf14cb6beff0ad4274b441f7f387f5055c47f493271d26731336257384.json @@ -46,11 +46,11 @@ ] }, "nullable": [ - false, - false, - false, - false, - false, + true, + true, + true, + true, + true, true, true ] diff --git a/backend/.sqlx/query-e8e33f599eae064011232f9f715e676d0c8ae31982865cb2c8103ed735c42c69.json b/backend/.sqlx/query-6f49a47b0b77720b0e8a2ddbd46e45e414dd646c912387ee63609d9f5611690f.json similarity index 54% rename from backend/.sqlx/query-e8e33f599eae064011232f9f715e676d0c8ae31982865cb2c8103ed735c42c69.json rename to backend/.sqlx/query-6f49a47b0b77720b0e8a2ddbd46e45e414dd646c912387ee63609d9f5611690f.json index 84645b71b0..f7cad58326 100644 --- a/backend/.sqlx/query-e8e33f599eae064011232f9f715e676d0c8ae31982865cb2c8103ed735c42c69.json +++ b/backend/.sqlx/query-6f49a47b0b77720b0e8a2ddbd46e45e414dd646c912387ee63609d9f5611690f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", + "query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", "describe": { "columns": [], "parameters": { @@ -14,5 +14,5 @@ }, "nullable": [] }, - "hash": "e8e33f599eae064011232f9f715e676d0c8ae31982865cb2c8103ed735c42c69" + "hash": "6f49a47b0b77720b0e8a2ddbd46e45e414dd646c912387ee63609d9f5611690f" } diff --git a/backend/.sqlx/query-2e131a019051bdac7c9c65f7c504cfba31cfdd64a0f68001e123694ed5cde5ed.json b/backend/.sqlx/query-8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384.json similarity index 55% rename from backend/.sqlx/query-2e131a019051bdac7c9c65f7c504cfba31cfdd64a0f68001e123694ed5cde5ed.json rename to backend/.sqlx/query-8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384.json index 347ad02c31..e355026cf4 100644 --- a/backend/.sqlx/query-2e131a019051bdac7c9c65f7c504cfba31cfdd64a0f68001e123694ed5cde5ed.json +++ b/backend/.sqlx/query-8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", + "query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", "describe": { "columns": [], "parameters": { @@ -14,5 +14,5 @@ }, "nullable": [] }, - "hash": "2e131a019051bdac7c9c65f7c504cfba31cfdd64a0f68001e123694ed5cde5ed" + "hash": "8e750d4b3af9b5844c11b1b92741f2ee9d2d3412d4a2c96c6ccc87ec1c382384" } diff --git a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json index 54e94cfb8f..99269c9851 100644 --- a/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json +++ b/backend/.sqlx/query-b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76.json @@ -18,8 +18,8 @@ "Left": [] }, "nullable": [ - false, - true + true, + false ] }, "hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76" diff --git a/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json b/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json new file mode 100644 index 0000000000..182fa4e788 --- /dev/null +++ b/backend/.sqlx/query-be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n (elem->>'installation_id')::bigint as installation_id\n FROM workspace_settings,\n LATERAL jsonb_array_elements(git_app_installations) AS elem\n WHERE workspace_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "installation_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "be00ac55e8668a0ed3befda7d8595c7cda0cba0b119d4fdb8a0dea1b28a1d560" +} diff --git a/backend/.sqlx/query-e572fa64eec9188368d7c271ac7ecd6b45dc161423abab22cddb2b13f6fb9833.json b/backend/.sqlx/query-f844026597d2e1c59a80dc8e1ddd069c0ad82b27b41171825928a938cfa8316f.json similarity index 54% rename from backend/.sqlx/query-e572fa64eec9188368d7c271ac7ecd6b45dc161423abab22cddb2b13f6fb9833.json rename to backend/.sqlx/query-f844026597d2e1c59a80dc8e1ddd069c0ad82b27b41171825928a938cfa8316f.json index 2ee8c422fd..2aab93c8bb 100644 --- a/backend/.sqlx/query-e572fa64eec9188368d7c271ac7ecd6b45dc161423abab22cddb2b13f6fb9833.json +++ b/backend/.sqlx/query-f844026597d2e1c59a80dc8e1ddd069c0ad82b27b41171825928a938cfa8316f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, app_version) WHERE app_version IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", + "query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, app_version) WHERE app_version IS NOT NULL DO UPDATE SET callback_job_ids = EXCLUDED.callback_job_ids, deployment_msg = EXCLUDED.deployment_msg", "describe": { "columns": [], "parameters": { @@ -14,5 +14,5 @@ }, "nullable": [] }, - "hash": "e572fa64eec9188368d7c271ac7ecd6b45dc161423abab22cddb2b13f6fb9833" + "hash": "f844026597d2e1c59a80dc8e1ddd069c0ad82b27b41171825928a938cfa8316f" } diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 90e4b6fb36..3629450170 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -3674871005d1dc3b92fd5acfd76009a95a43288a \ No newline at end of file +ab6116235f7869cef306996059f8663f7e831b8a diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 64876f6b1b..6251954fdd 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -728,6 +728,32 @@ paths: schema: type: string + /w/{workspace}/workspaces/create_workspace_fork_branch: + post: + summary: create forked workspace branch with git sync + operationId: createWorkspaceForkGitBranch + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new forked workspace + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateWorkspaceFork" + responses: + "201": + description: forked workspace branch created + content: + application/json: + schema: + type: array + items: + type: string + format: uuid + /w/{workspace}/workspaces/create_fork: post: summary: create forked workspace diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index d785116d40..c7053209da 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -50,7 +50,7 @@ use windmill_common::{ oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH, utils::{paginate, rd_string, require_admin, Pagination}, }; -use windmill_git_sync::{handle_deployment_metadata, DeployedObject}; +use windmill_git_sync::{handle_fork_branch_creation, handle_deployment_metadata, DeployedObject}; use windmill_worker::scoped_dependency_map::{DependencyMap, ScopedDependencyMap}; #[cfg(feature = "enterprise")] @@ -163,6 +163,7 @@ pub fn workspaced_service() -> Router { post(acknowledge_all_critical_alerts), ) .route("/critical_alerts/mute", post(mute_critical_alerts)) + .route("/create_workspace_fork_branch", post(create_workspace_fork_branch)) .route("/operator_settings", post(update_operator_settings)); #[cfg(all(feature = "stripe", feature = "enterprise"))] @@ -3026,19 +3027,33 @@ async fn deprecated_create_workspace_fork(_authed: ApiAuthed) -> Result return Err(Error::BadRequest("This API endpoint has been relocated. Your Windmill CLI version is outdated and needs to be updated.".to_string())); } +/// Return the uuids of the git sync jobs to create the branch before creating the fork +async fn create_workspace_fork_branch( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, + Json(nw): Json, +) -> JsonResult> { + if *CLOUD_HOSTED { + return Err(Error::BadRequest(format!( + "Forking workspaces is not available on app.windmill.dev" + ))); + } + + Ok(Json( + handle_fork_branch_creation(&authed.email, &authed.username, &db, &w_id, &nw.id).await?, + )) +} + async fn create_workspace_fork( authed: ApiAuthed, Extension(db): Extension, Path(parent_workspace_id): Path, Json(nw): Json, ) -> Result { - // if *CREATE_WORKSPACE_REQUIRE_SUPERADMIN { - // require_super_admin(&db, &authed.email).await?; - // } - if *CLOUD_HOSTED { return Err(Error::BadRequest(format!( - "Forking workspaces is not available on Cloud" + "Forking workspaces is not available on app.windmill.dev" ))); } diff --git a/backend/windmill-git-sync/src/git_sync_oss.rs b/backend/windmill-git-sync/src/git_sync_oss.rs index 70e2b53640..795e238e9d 100644 --- a/backend/windmill-git-sync/src/git_sync_oss.rs +++ b/backend/windmill-git-sync/src/git_sync_oss.rs @@ -21,3 +21,14 @@ pub async fn handle_deployment_metadata<'c>( // Git sync is an enterprise feature and not part of the open-source version return Ok(()); } + +#[cfg(not(feature = "private"))] +pub async fn handle_fork_branch_creation<'c>( + _email: &str, + _created_by: &str, + _db: &DB, + _w_id: &str, + _fork_workspace_id: &str, +) -> Result> { + return Ok(vec![]); +} diff --git a/backend/windmill-git-sync/src/lib.rs b/backend/windmill-git-sync/src/lib.rs index 380cebb70b..42ceec8cda 100644 --- a/backend/windmill-git-sync/src/lib.rs +++ b/backend/windmill-git-sync/src/lib.rs @@ -12,7 +12,7 @@ use windmill_common::{scripts::ScriptHash, DB}; pub mod git_sync_ee; pub mod git_sync_oss; -pub use git_sync_oss::handle_deployment_metadata; +pub use git_sync_oss::{handle_deployment_metadata, handle_fork_branch_creation}; #[derive(Clone, Debug)] pub enum DeployedObject { diff --git a/frontend/src/lib/components/workspaceSettings/CreateWorkspace.svelte b/frontend/src/lib/components/workspaceSettings/CreateWorkspace.svelte index 72d5f12dc2..b230658e36 100644 --- a/frontend/src/lib/components/workspaceSettings/CreateWorkspace.svelte +++ b/frontend/src/lib/components/workspaceSettings/CreateWorkspace.svelte @@ -27,9 +27,11 @@ import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' import { AI_PROVIDERS } from '$lib/components/copilot/lib' - import { GitFork } from 'lucide-svelte' + import { GitFork, LoaderCircle } from 'lucide-svelte' import PrefixedInput from '../PrefixedInput.svelte' import TextInput from '../text_input/TextInput.svelte' + import { jobManager } from '$lib/services/JobManager' + import Alert from '../common/alert/Alert.svelte' interface Props { isFork?: boolean @@ -76,19 +78,78 @@ const WM_FORK_PREFIX = 'wm-fork-' + let forkCreationLoading = $state(false) + let forkCreationError = $state("") + let errorMsgs: string[] = $state([]) + async function createOrForkWorkspace() { const prefixed_id = `${WM_FORK_PREFIX}${id}` if (isFork) { if ($workspaceStore) { - await WorkspaceService.createWorkspaceFork({ + forkCreationLoading = true + errorMsgs = [] + forkCreationError = "" + + let gitSyncJobIds = await WorkspaceService.createWorkspaceForkGitBranch({ workspace: $workspaceStore!, requestBody: { id: prefixed_id, name, - color: colorEnabled && workspaceColor ? workspaceColor : undefined, + color: colorEnabled && workspaceColor ? workspaceColor : undefined } }) + try { + await Promise.all( + gitSyncJobIds.map((jobId) => + jobManager.runWithProgress(() => Promise.resolve(jobId), { + workspace: $workspaceStore, + timeout: 60000, + timeoutMessage: `Deploy fork job timed out after 60s`, + onProgress: (status) => { + if (status.status === 'failure') { + errorMsgs.push(status.error ?? 'Deploy fork job failed') + } + } + }) + ) + ) + } catch (error) { + forkCreationLoading = false + sendUserToast( + `Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs} - ${error}`, + true + ) + return + } + if (errorMsgs.length != 0) { + forkCreationError = "Failed to create a branch for this fork on the git sync repo(s)" + forkCreationLoading = false + sendUserToast( + `Could not fork workspace ${$workspaceStore} because branch creation failed: ${errorMsgs}`, + true + ) + return + } + + try { + await WorkspaceService.createWorkspaceFork({ + workspace: $workspaceStore!, + requestBody: { + id: prefixed_id, + name, + color: colorEnabled && workspaceColor ? workspaceColor : undefined + } + }) + } catch (e) { + forkCreationError = `Failed to create fork '${prefixed_id}'` + errorMsgs.push(e?.body ?? e ?? "Unknown error") + forkCreationLoading = false + sendUserToast(`Could not create fork '${prefixed_id}' ${e}`, true) + return + } + + forkCreationLoading = false sendUserToast(`Successfully forked workspace ${$workspaceStore} as: wm-fork-${id}`) } else { sendUserToast('No workspace selected, cannot fork non-existent workspace', true) @@ -256,6 +317,15 @@ {/if} + {#if errorMsgs.length != 0} + +
    + {#each errorMsgs as errorMsg} +
  • - {errorMsg}
  • + {/each} +
+
+ {/if}