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
This commit is contained in:
wendrul
2025-11-06 17:45:34 +01:00
committed by GitHub
parent 9d1f9bd39b
commit 192fecc86f
13 changed files with 195 additions and 41 deletions
@@ -46,11 +46,11 @@
]
},
"nullable": [
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true
]
@@ -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"
}
@@ -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"
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
false,
true
true,
false
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -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"
}
@@ -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"
}
+1 -1
View File
@@ -1 +1 @@
3674871005d1dc3b92fd5acfd76009a95a43288a
ab6116235f7869cef306996059f8663f7e831b8a
+26
View File
@@ -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
+21 -6
View File
@@ -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<String>
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<DB>,
Path(w_id): Path<String>,
Json(nw): Json<CreateWorkspaceFork>,
) -> JsonResult<Vec<Uuid>> {
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<DB>,
Path(parent_workspace_id): Path<String>,
Json(nw): Json<CreateWorkspaceFork>,
) -> Result<String> {
// 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"
)));
}
@@ -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<Vec<uuid::Uuid>> {
return Ok(vec![]);
}
+1 -1
View File
@@ -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 {
@@ -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 @@
</span>
</div>
{/if}
{#if errorMsgs.length != 0}
<Alert class="p-2" title={forkCreationError} type="error">
<ul class="pl-2 pr-4 break-words">
{#each errorMsgs as errorMsg}
<li><pre class="whitespace-pre-wrap">- {errorMsg}</pre></li>
{/each}
</ul>
</Alert>
{/if}
<label class="flex flex-col gap-1">
{#if isFork}
<span class="text-xs font-semibold text-emphasis">Fork name</span>
@@ -429,24 +499,33 @@
</div>
{/if}
<div class="flex flex-wrap flex-row justify-between pt-12 gap-4">
<Button variant="default" size="sm" href="{base}/user/workspaces"
>&leftarrow; Back to workspaces</Button
>
<Button
variant="accent"
disabled={checking ||
errorId != '' ||
!name ||
(!automateUsernameCreation && (errorUser != '' || !username)) ||
!id}
on:click={createOrForkWorkspace}
disabled={forkCreationLoading}
variant="default"
size="sm"
href="{base}/user/workspaces">&leftarrow; Back to workspaces</Button
>
{#if isFork}
Fork workspace
{:else}
Create workspace
{/if}
</Button>
{#if !forkCreationLoading}
<Button
variant="accent"
disabled={checking ||
errorId != '' ||
!name ||
(!automateUsernameCreation && (errorUser != '' || !username)) ||
!id}
on:click={createOrForkWorkspace}
>
{#if isFork}
Fork workspace
{:else}
Create workspace
{/if}
</Button>
{:else}
<Button variant="accent" disabled={true}>
<LoaderCircle class="animate-spin" /> Creating branch
</Button>
{/if}
</div>
</div>
</CenteredModal>
+2 -1
View File
@@ -15,7 +15,8 @@
"gitSync_13": "hub/19801/sync-script-to-git-repo-windmill",
"gitSync_14": "hub/19803/sync-script-to-git-repo-windmill",
"gitSync_15": "hub/19816/sync-script-to-git-repo-windmill",
"gitSync": "hub/19818/sync-script-to-git-repo-windmill",
"gitSync_16": "hub/19818/sync-script-to-git-repo-windmill",
"gitSync": "hub/28073/sync-script-to-git-repo-windmill",
"gitSyncTest_0": "hub/9073/git-repo-test-read-write-windmill",
"gitSyncTest_1": "hub/11499/git-repo-test-read-write-windmill",
"gitSyncTest_2": "hub/11667/git-repo-test-read-write-windmill",