fix: show related job when deployment is in progress (#7294)

* feat: show related job when deployment is in progress

- Added job_id column to deployment_metadata table to track current deployment jobs
- Updated backend to store job_id when creating dependency jobs for scripts and flows
- Modified deployment status API endpoints to include job_id in responses
- Updated frontend to display clickable job link in "Deployment in progress" badge
- Added OpenAPI schema updates for new job_id field

Resolves #7293

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* update

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
claude[bot]
2025-12-04 09:50:07 +00:00
committed by GitHub
parent fcde732485
commit e9f13065bf
14 changed files with 209 additions and 65 deletions
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, job_id)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL\n DO UPDATE SET job_id = EXCLUDED.job_id",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "0e621bba5913482b8235d7d8442b8f0e9012c265e150afd4aa41972bf7334ba2"
}
@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "SELECT f.lock_error_logs, dm.job_id\n FROM flow f\n LEFT JOIN deployment_metadata dm ON f.versions[array_upper(f.versions, 1)] = dm.flow_version\n AND f.workspace_id = dm.workspace_id AND f.path = dm.path\n WHERE f.path = $1 AND f.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock_error_logs",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "job_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "1de29cdd474cbd61e15b63d111e1c42aefee683e14cc738a809ecca17370e6ee"
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT s.lock, s.lock_error_logs, dm.job_id\n FROM script s\n LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash AND s.workspace_id = dm.workspace_id\n WHERE s.hash = $1 AND s.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "lock_error_logs",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "job_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
true,
true,
true
]
},
"hash": "513ed713afdbafb587026d1536c47a9bbaa6967e36777454746b8817d68219a5"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, job_id)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL\n DO UPDATE SET job_id = EXCLUDED.job_id",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Int8",
"Uuid"
]
},
"nullable": []
},
"hash": "7abde47077c38ccf005ce7180a383f97076b2cbe2f617f7af39550a0db157b2b"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock_error_logs FROM flow WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock_error_logs",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true
]
},
"hash": "97bf27f210572499b42ce04f19f116cc87ed06c49dcca04360250ddfd89d7ab3"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "lock",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "lock_error_logs",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
true,
true
]
},
"hash": "e3ac59fcf6193007a21c808a275d3d84fd76e44f9240f4292c5c1208096b8563"
}
@@ -0,0 +1,2 @@
-- Remove job_id column from deployment_metadata table
ALTER TABLE deployment_metadata DROP COLUMN IF EXISTS job_id;
@@ -0,0 +1,2 @@
-- Add job_id column to deployment_metadata table to track the current deployment job
ALTER TABLE deployment_metadata ADD COLUMN IF NOT EXISTS job_id UUID;
+6
View File
@@ -5950,6 +5950,9 @@ paths:
type: string
lock_error_logs:
type: string
job_id:
type: string
format: uuid
/w/{workspace}/jobs/list_selected_job_groups:
# We use post because sending a huge array as a query param can produce
@@ -6836,6 +6839,9 @@ paths:
properties:
lock_error_logs:
type: string
job_id:
type: string
format: uuid
/w/{workspace}/flows/get_triggers_count/{path}:
get:
+47 -5
View File
@@ -577,6 +577,20 @@ async fn create_flow(
.execute(&mut *new_tx)
.await?;
// Store the job_id in deployment_metadata for this flow deployment
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, flow_version, job_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL
DO UPDATE SET job_id = EXCLUDED.job_id",
w_id,
nf.path,
version,
dependency_job_uuid
)
.execute(&mut *new_tx)
.await?;
new_tx.commit().await?;
webhook.send_message(
w_id.clone(),
@@ -1115,6 +1129,25 @@ async fn update_flow(
))
})?;
// Store the job_id in deployment_metadata for this flow deployment
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, flow_version, job_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL
DO UPDATE SET job_id = EXCLUDED.job_id",
w_id,
nf.path,
version,
dependency_job_uuid
)
.execute(&mut *new_tx)
.await
.map_err(|e| {
error::Error::internal_err(format!(
"Error updating deployment_metadata with job_id: {e:#}"
))
})?;
if let Some(old_dep_job) = old_dep_job {
sqlx::query!(
"UPDATE v2_job_queue SET
@@ -1154,9 +1187,10 @@ async fn list_tokens(
list_tokens_internal(&db, &w_id, &path, true).await
}
#[derive(FromRow, Serialize)]
#[derive(Serialize)]
struct DeploymentStatus {
lock_error_logs: Option<String>,
job_id: Option<sqlx::types::Uuid>,
}
async fn get_deployment_status(
Extension(db): Extension<DB>,
@@ -1164,9 +1198,12 @@ async fn get_deployment_status(
) -> JsonResult<DeploymentStatus> {
let path = path.to_path();
let mut tx = db.begin().await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus,
"SELECT lock_error_logs FROM flow WHERE path = $1 AND workspace_id = $2",
let status_o = sqlx::query!(
"SELECT f.lock_error_logs, dm.job_id
FROM flow f
LEFT JOIN deployment_metadata dm ON f.versions[array_upper(f.versions, 1)] = dm.flow_version
AND f.workspace_id = dm.workspace_id AND f.path = dm.path
WHERE f.path = $1 AND f.workspace_id = $2",
path,
w_id,
)
@@ -1175,8 +1212,13 @@ async fn get_deployment_status(
let status = not_found_if_none(status_o, "DeploymentStatus", path)?;
let deployment_status = DeploymentStatus {
lock_error_logs: status.lock_error_logs,
job_id: status.job_id,
};
tx.commit().await?;
Ok(Json(status))
Ok(Json(deployment_status))
}
async fn get_flow_by_path(
+31 -7
View File
@@ -998,14 +998,14 @@ async fn create_script_internal<'c>(
}
let tx = PushIsolationLevel::Transaction(tx);
let (_, new_tx) = windmill_queue::push(
let (job_id, mut new_tx) = windmill_queue::push(
&db,
tx,
&w_id,
JobPayload::Dependencies {
hash,
language: ns.language,
path: ns.path,
path: ns.path.clone(),
dedicated_worker: ns.dedicated_worker,
},
windmill_queue::PushArgs::from(&args),
@@ -1034,6 +1034,21 @@ async fn create_script_internal<'c>(
None,
)
.await?;
// Store the job_id in deployment_metadata for this script deployment
sqlx::query!(
"INSERT INTO deployment_metadata (workspace_id, path, script_hash, job_id)
VALUES ($1, $2, $3, $4)
ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL
DO UPDATE SET job_id = EXCLUDED.job_id",
w_id,
ns.path,
hash.0,
job_id
)
.execute(&mut *new_tx)
.await?;
Ok((hash, new_tx, None))
} else {
if codebase.is_none() {
@@ -1750,19 +1765,22 @@ async fn raw_script_by_hash(
Ok(r.script.content)
}
#[derive(FromRow, Serialize)]
#[derive(Serialize)]
struct DeploymentStatus {
lock: Option<String>,
lock_error_logs: Option<String>,
job_id: Option<sqlx::types::Uuid>,
}
async fn get_deployment_status(
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<DeploymentStatus> {
let mut tx = db.begin().await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus,
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
let status_o = sqlx::query!(
"SELECT s.lock, s.lock_error_logs, dm.job_id
FROM script s
LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash AND s.workspace_id = dm.workspace_id
WHERE s.hash = $1 AND s.workspace_id = $2",
hash.0,
w_id,
)
@@ -1771,8 +1789,14 @@ async fn get_deployment_status(
let status = not_found_if_none(status_o, "DeploymentStatus", hash.to_string())?;
let deployment_status = DeploymentStatus {
lock: status.lock,
lock_error_logs: status.lock_error_logs,
job_id: status.job_id,
};
tx.commit().await?;
Ok(Json(status))
Ok(Json(deployment_status))
}
pub async fn require_is_writer(authed: &ApiAuthed, path: &str, w_id: &str, db: DB) -> Result<()> {
+1 -1
View File
@@ -3040,7 +3040,7 @@ pub async fn concurrency_key(
.await
.map(|x| {
if x.is_none() {
tracing::info!("No concurrency key found for job {id}, defaulting to empty string");
tracing::info!("No concurrency key found for job {id}");
}
return x;
})
@@ -75,6 +75,7 @@
let inputSelected: 'saved' | 'history' | undefined = $state(undefined)
let jsonView = $state(false)
let deploymentInProgress = $state(false)
let deploymentJobId: string | undefined = $state(undefined)
let intervalId: number | undefined = undefined
@@ -166,8 +167,11 @@
})
if (status.lock_error_logs == undefined || status.lock_error_logs != '') {
deploymentInProgress = false
deploymentJobId = undefined
flow.lock_error_logs = status.lock_error_logs
clearInterval(intervalId)
} else if (status.job_id) {
deploymentJobId = status.job_id
}
}
}
@@ -541,6 +545,13 @@
<HeaderBadge color="yellow">
<Loader2 size={12} class="inline animate-spin mr-1" />
Deployment in progress
{#if deploymentJobId}
<a
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
class="underline"
target="_blank">view job</a
>
{/if}
</HeaderBadge>
{/if}
{#if flow.lock_error_logs && flow.lock_error_logs != ''}
@@ -89,6 +89,7 @@
let topHash: string | undefined = $state()
let can_write = $state(false)
let deploymentInProgress = $state(false)
let deploymentJobId: string | undefined = $state(undefined)
let intervalId: number
let shareModal: ShareModal | undefined = $state()
let runForm: RunForm | undefined = $state()
@@ -156,9 +157,12 @@
})
if (status.lock != undefined || status.lock_error_logs != undefined) {
deploymentInProgress = false
deploymentJobId = undefined
script.lock = status.lock
script.lock_error_logs = status.lock_error_logs
clearInterval(intervalId)
} else if (status.job_id) {
deploymentJobId = status.job_id
}
}
}
@@ -689,6 +693,13 @@
<Badge color="yellow">
<Loader2 size={12} class="inline animate-spin mr-1" />
Deployment in progress
{#if deploymentJobId}
<a
href="/run/{deploymentJobId}?workspace={$workspaceStore}"
class="underline"
target="_blank">view job</a
>
{/if}
</Badge>
{/if}