diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 6a6c6d281a..81ca8bb347 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -1a7e81206e8768722f0d0acfb3838036078e70be +9ac2ffdee130fcff7ed45d137d8606ae983da837 diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index b6be641fcd..1c68e275ca 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -4622,7 +4622,7 @@ const GIT_CREDENTIAL_LOCK_ID: i64 = 737_483_923; /// expiry, so a token dies visibly (and usually not at all) rather than taking /// sync down on its expiry date. #[cfg(all(feature = "enterprise", feature = "private"))] -pub async fn maintain_git_credentials(db: &Pool) { +async fn maintain_git_credentials(db: &Pool) { use windmill_common::ee_oss::{get_license_plan, LicensePlan}; if !matches!(get_license_plan().await, LicensePlan::Enterprise) { diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index bfa4715166..4b18d7f0e7 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -4117,13 +4117,10 @@ async fn edit_git_sync_config( // `sync_repo_webhook` writes back the webhook fields it changes itself: // the remote hook and the record of it have to move together, so // persisting them out here would let one land without the other. - if let Err(e) = windmill_common::git_sync_ee::sync_repo_webhook(&db, &w_id, repo).await - { - tracing::warn!("git auto-pull: webhook sync error: {}", e); - } - // Check the credential while the operator is still on the settings page, - // so a token that is short-lived or missing a scope is visible now rather - // than when it expires. + // Before the webhook reconcile, which decides whether this repo can have + // one from the credential this records. Also puts a short-lived or + // under-scoped token in front of the operator while they are still on the + // settings page, rather than when it expires. if let Err(e) = windmill_common::git_sync_ee::refresh_git_credential_status( &db, &w_id, @@ -4133,6 +4130,10 @@ async fn edit_git_sync_config( { tracing::warn!("git credential check error: {}", e); } + if let Err(e) = windmill_common::git_sync_ee::sync_repo_webhook(&db, &w_id, repo).await + { + tracing::warn!("git auto-pull: webhook sync error: {}", e); + } } for (path, hook_id) in removed_webhooks { if let Ok(url) = @@ -4365,9 +4366,6 @@ async fn edit_git_sync_repository( .iter_mut() .find(|r| r.git_repo_resource_path == new_config.git_repo_resource_path) { - if let Err(e) = windmill_common::git_sync_ee::sync_repo_webhook(&db, &w_id, repo).await { - tracing::warn!("git auto-pull: webhook sync error: {}", e); - } if let Err(e) = windmill_common::git_sync_ee::refresh_git_credential_status( &db, &w_id, @@ -4377,6 +4375,9 @@ async fn edit_git_sync_repository( { tracing::warn!("git credential check error: {}", e); } + if let Err(e) = windmill_common::git_sync_ee::sync_repo_webhook(&db, &w_id, repo).await { + tracing::warn!("git auto-pull: webhook sync error: {}", e); + } } // Trigger git sync for individual repository update/add diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 4c39443f2e..849f1eb6eb 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -34005,6 +34005,9 @@ components: type: array items: type: string + token_fingerprint: + type: string + description: one-way digest identifying which credential this status describes rotatable: type: boolean checked_at: diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index b6337351a4..cf0a8bef2b 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -958,6 +958,15 @@ pub async fn run_server( #[cfg(not(feature = "enterprise"))] Router::new() }) + .nest("/w/{workspace_id}/git_sync", { + #[cfg(feature = "enterprise")] + { + git_sync_oss::workspaced_git_sync_service() + } + + #[cfg(not(feature = "enterprise"))] + Router::new() + }) .nest( "/w/{workspace_id}/resources_u", public_service().layer(cors.clone()), diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 28fc2d9b03..d61a49d6b5 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -427,8 +427,13 @@ pub struct GitCredentialStatus { pub expires_at: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub scopes: Vec, + /// Which credential this status describes, as a one-way digest. Lets a later + /// check tell "the token I know about stopped working" from "someone put a + /// different credential here", which look identical at the API otherwise. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub token_fingerprint: Option, /// Rotation needs both a scope that permits it (`api` or `self_rotate`) and a - /// URL held in a variable we can write back to. + /// URL Windmill can write back to. `scopes` says which of the two is missing. pub rotatable: bool, /// Unix timestamp (seconds) of the last check. pub checked_at: i64, diff --git a/frontend/src/lib/components/git_sync/GitSyncRepositoryCard.svelte b/frontend/src/lib/components/git_sync/GitSyncRepositoryCard.svelte index bca71c8aee..5bb4665030 100644 --- a/frontend/src/lib/components/git_sync/GitSyncRepositoryCard.svelte +++ b/frontend/src/lib/components/git_sync/GitSyncRepositoryCard.svelte @@ -204,10 +204,16 @@ } } if (days > 30) return undefined + // Two different things stop a renewal, and they need opposite advice: a + // token that may not rotate itself, or a token Windmill cannot store the + // replacement for. Scopes say which. + const canSelfRotate = (credential.scopes ?? []).some((s) => s === 'api' || s === 'self_rotate') return { type: days <= 7 ? ('error' as const) : days <= 14 ? ('warning' as const) : ('info' as const), title: `Repository token ${when}`, - body: 'Give the token the api or self_rotate scope and Windmill will renew it on its own. Otherwise, replace it before it expires to keep sync running.' + body: canSelfRotate + ? 'Windmill cannot renew it because it cannot write the new token back to where this URL is stored. Move the URL into a Windmill variable, or replace the token before it expires.' + : 'Give the token the api or self_rotate scope and Windmill will renew it on its own. Otherwise, replace it before it expires to keep sync running.' } })