diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index fa112ef4e1..6a603a1fa7 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -1422,6 +1422,35 @@ pub(crate) async fn tarball_workspace( .await?; // Use v2 format only if explicitly requested, otherwise use v1 (legacy) for backward compatibility + // Server-owned auto-pull state (the HMAC webhook secret + hook id/error and + // the synced-sha / last-pull status) must never leave the server: keep it out + // of export archives and synced repos, and don't let a re-imported workspace + // inherit another install's hook/sync state. Mirrors the GET-settings redaction. + fn redact_git_sync_for_export(git_sync: Option) -> Option { + let mut git_sync = git_sync?; + if let Some(repos) = git_sync + .get_mut("repositories") + .and_then(|r| r.as_array_mut()) + { + for repo in repos { + if let Some(auto_pull) = + repo.get_mut("auto_pull").and_then(|a| a.as_object_mut()) + { + for field in [ + "webhook_secret", + "webhook_id", + "webhook_error", + "last_synced_sha", + "last_pull_status", + ] { + auto_pull.remove(field); + } + } + } + } + Some(git_sync) + } + let settings_str = if settings_version.as_deref() == Some("v2") { let settings = SimplifiedSettings { auto_invite: row.auto_invite, @@ -1431,7 +1460,7 @@ pub(crate) async fn tarball_workspace( success_handler: row.success_handler, ai_config: row.ai_config, large_file_storage: row.large_file_storage, - git_sync: row.git_sync, + git_sync: redact_git_sync_for_export(row.git_sync), default_app: row.default_app, default_scripts: row.default_scripts, name: row.name.clone().unwrap_or_default(), @@ -1502,7 +1531,7 @@ pub(crate) async fn tarball_workspace( error_handler_muted_on_cancel, ai_config: row.ai_config, large_file_storage: row.large_file_storage, - git_sync: row.git_sync, + git_sync: redact_git_sync_for_export(row.git_sync), default_app: row.default_app, default_scripts: row.default_scripts, name: row.name.unwrap_or_default(), diff --git a/docs/git-sync-pull-design.md b/docs/git-sync-pull-design.md index 18319d6abe..4707f32cc7 100644 --- a/docs/git-sync-pull-design.md +++ b/docs/git-sync-pull-design.md @@ -87,7 +87,7 @@ flowchart LR R[Repo] -- "push event" --> W[repo webhook
created via API] end subgraph Instance - W -- "HMAC-verified POST" --> E["/api/w/:ws/github_app/push_webhook"] + W -- "HMAC-verified POST" --> E["/api/w/:ws/github_app/webhook"] P[poller
git ls-remote / schedule] --> REC E --> REC[reconcile:
ref match? sha moved?
debounce] REC --> J[pull job
existing hub script, pull:true] @@ -116,8 +116,8 @@ Flow when a repo is connected (or auto-pull is enabled on an existing repo): 1. Instance generates a per-repo secret, stored in the repo's git-sync settings. 2. Instance creates the webhook via API: events `["push"]` (later `"pull_request"`, §11), URL - `{base_url}/api/w/{workspace}/github_app/push_webhook/{repo_settings_id}`, - secret set. + `{base_url}/api/w/{workspace}/github_app/webhook` (host-aware, so managed and + self-managed/GHES apps use the same per-workspace receiver), secret set. 3. GitHub immediately delivers a `ping` event. If the ping is not received within ~10 s, the instance deletes the hook and falls back to polling, surfacing "instance not reachable from GitHub — using polling (interval Xm)" @@ -139,9 +139,9 @@ the instance — no repo hooks needed, one webhook covers all installed repos: - `GhesAppSettings` gains a "Webhook secret" field; the setup checklist changes from "Uncheck Active under Webhook" to "set webhook URL to - `{base_url}/api/github_app/webhook` (instance-global endpoint), subscribe to - Push events, paste this generated secret". -- The instance-global endpoint verifies HMAC with the instance-level secret and + `{base_url}/api/w/{workspace}/github_app/webhook` (the per-workspace receiver), + subscribe to Push events, paste this generated secret". +- That per-workspace endpoint verifies HMAC with the configured secret and routes by repo full name + installation to matching workspaces (the routing data is in `workspace_settings`). - GHES typically shares a network with the instance, so this works **air-gapped** @@ -237,10 +237,8 @@ workspace + repo path — decide at implementation review). New instance endpoints (EE): -- `POST /api/w/{workspace}/github_app/push_webhook/{repo_settings_id}` — - per-repo hook receiver (managed app), HMAC-verified, returns 202. -- `POST /api/github_app/webhook` — instance-global receiver (self-managed app), - HMAC-verified, routes internally. +- `POST /api/w/{workspace}/github_app/webhook` — per-workspace hook receiver for + managed and self-managed apps (host-aware), HMAC-verified, returns 202. - Both are unauthenticated-but-verified endpoints; rate-limited; bodies are treated as hints only (§4). @@ -420,9 +418,9 @@ now too, to avoid a second nag for phases 3–4). Update permission-purpose copy Backend (EE): -- Receiver endpoints (§8): `POST /api/w/{workspace}/github_app/push_webhook/{id}` - (managed) and `POST /api/github_app/webhook` (instance-global, GHES), both - HMAC-verified — reuse `X-Hub-Signature-256` validation from +- Receiver endpoint (§8): `POST /api/w/{workspace}/github_app/webhook` + (per-workspace, managed and self-managed), HMAC-verified — reuse + `X-Hub-Signature-256` validation from `windmill-trigger-http/src/http_trigger_auth.rs`; routes added to `git_sync_ee.rs` `workspaced_service` / `global_service`. - Webhook create/delete via installation token — reuse the REST pattern from