mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 00:03:09 +00:00
feat(git-sync): enable per-item promotion mode on dev workspaces (#10205)
* feat(git-sync): enable per-item promotion mode on dev workspaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: keep unrelated git-sync Alert copy at its original wrapping Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): fall back to parent_path on empty deploy path + bump ee ref computeGitSyncDeployBranch used ?? so a backend-serialized empty path (rename out of the repo filter) skipped the deploy branch and could commit to the tracked base; use || to fall back to parent_path like the backend. Bumps ee-repo-ref for the single-object promotion_open_prs fix (windmill-ee-private#679). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): route dev-promotion non-branchable objects off the tracked base user/group objects (and any unresolvable ref) returned null in promotion mode, so a dev-workspace deploy pushed them straight to the parent's tracked branch. Fall back to the dev's env-label branch instead; the backend opens no PR for them (isolated, not promoted). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(git-sync): dev-workspace promotion via a toggle on the inherited repo A dev workspace reuses the single repo it inherited from prod: a 'Promote to prod via Git' toggle flips it between sync mode (deploys to the dev branch) and promotion mode (per-item wm_deploy/** PRs to prod), with a per-item/per-folder sub-toggle. Removes the redundant separate-promotion-repo setup for dev workspaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(git-sync): dev-promotion regression test + widen git_sync_e2e path filter Adds a CLI integration case covering dev-workspace promotion (script -> wm_deploy branch; user/group -> env-label branch, main never touched). Widens the git-sync-test.yml relevance filter to the deploy-branch derivation, git-sync guard, and CLI git-deploy files so the e2e suite runs on PRs like this one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): gate dev promotion toggle on EE, fix card mode + workflow path filters Codex review: (1) show the dev promotion toggle only under an active EE license and revert the optimistic save if the backend rejects it; (2) derive the dev card's display mode from use_individual_branch so promotion copy shows in promotion mode; (3) mirror the new relevance paths into the workflow's top-level push/pull_request filters so it actually triggers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): only use the single-card dev promotion UX when the dev has one repo Codex review: an attached dev workspace keeps its own repositories rather than inheriting prod's. Gating the single-card + toggle + hidden-secondaries UX on repositories.length <= 1 makes a multi-repo attached dev fall back to the normal layout, so no active repo is hidden and an unrelated repo isn't presented as prod's promotion target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): runtime EE-plan gate for promotion mode, consistent with auto-pull/PR Codex review: promotion mode only had the CE compile rejection, while auto-pull and PR creation runtime-gate on the active plan (check_git_sync_ee_license). Add check_promotion_license and call it from both edit_git_sync_config and edit_git_sync_repository, plus the matching CE rejection on edit_git_sync_config so the two endpoints are symmetric. Promotion is now gated like every other git-sync EE setting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): dev promotion must reuse the parent workspace's repository Codex review: repository count doesn't prove a dev inherited prod's repo — an attached dev keeps its own. check_dev_promotion_targets_parent_repo resolves the promotion repo's URL and rejects enabling promotion unless it matches one the parent (prod) tracks, so branches/PRs can't target an unrelated repository. Called from both git-sync edit endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): dev promotion save-time check uses shared parent-repo matcher (url+branch) Delegates to windmill_common::git_sync_ee::dev_promotion_target_matches_parent so the settings gate and the deploy-time safety net share one url+branch identity check. Bumps ee-repo-ref for the EE deploy-time enforcement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump ee-repo-ref for private resolve_repo_url_and_branch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump ee-repo-ref for promotion-target matcher authz doc Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(git-sync): bump hub scripts to gitsync-cli versions, fix promotion tooltips Point LATEST_GIT_SYNC_SCRIPT_PATH (28790 -> 28796) and GIT_SYNC_PULL_SCRIPT_PATH / gitInitRepo (28789 -> 28795) at the hub versions pinning windmill-cli@1.763.1-gitsync.0, which carries the dev-workspace promotion routing. Slugs unchanged, so the GitHub-App token check and hub script cache are unaffected. Tooltips: enabling promotion pushes a PR-ready wm_deploy/** branch; Windmill only opens the pull request itself when automatic pull requests are enabled. Reword both toggles to stop promising a PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): dev promotion mirrors to the env-label branch, PR toggles exclusive by branch type Bump ee-repo-ref for the dispatcher changes: a promotion dev's deploys now also push to its env-label branch (one extra mirror job per batch, users/groups mirror-only), and `fork_open_prs` no longer applies to a dev in promotion mode where `promotion_open_prs` governs. Frontend: the fork-PR toggle tooltip states its actual coverage (wm-fork/** and the dev branch of a dev workspace) and that a promotion dev's own pull request toggle takes over for wm_deploy/** branches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): reject dev promotion on pre-28796 pinned sync scripts An older pinned sync script bundles a CLI that force-disables per-item branches on every fork, so enabling promotion on a dev workspace with such a pin would silently keep deploying to the env-label branch. Both git-sync edit endpoints now reject the combination with an actionable error; the EE dispatchers (via ee-repo-ref bump) demote inherited configs to promotion-off semantics so markers, branch keys and the mirror match the branch the CLI actually pushes. Roots and auto-managed repositories are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): serialize dev promotion toggle saves The promotion and per-folder toggles persist immediately via whole-repo saves; leaving them interactive while one is pending lets rapid flips race, and the earlier save (enabling runs extra backend checks) can commit last, silently reversing the state the UI shows. Both toggles now disable while a save is in flight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(git-sync): lock auto-PR toggle during promotion save, rename-out branch routing Frontend: the automatic-PR toggle is revealed by the promotion toggle's in-flight save; an edit made mid-save was absorbed into the saved baseline without reaching the backend. It now disables during that save. EE (ee-repo-ref bump): dispatcher debounce/concurrency keys and PR markers follow the CLI's parent_path fallback for rename-out items, so their wm_deploy/** branches debounce per-branch and open their PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(git-sync): condense comments to durable constraints Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update ee-repo-ref to 8bf73f803158bcbf7b8d55a36f4a1ebfcc1bbcd9 This commit updates the EE repository reference after PR #679 was merged in windmill-ee-private. Previous ee-repo-ref: c2cd718cb53d234f909f485bd7cd43ed9605ffd1 New ee-repo-ref: 8bf73f803158bcbf7b8d55a36f4a1ebfcc1bbcd9 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
windmill-internal-app[bot]
parent
9739d5a2c2
commit
2ce21c9ef8
@@ -826,21 +826,37 @@ async fn reject_dev_label_matching_tracked_branch(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject parent-only git-sync settings on a fork workspace. Auto-pull, fork
|
||||
/// PRs, and promotion mode are all configured at the parent: repo → fork sync is
|
||||
/// routed by the parent's webhook/poller (`sync_forks`), a fork-owned auto-pull
|
||||
/// would register a second webhook on the same GitHub repo per fork, and a
|
||||
/// fork's deploys always go to its `wm-fork/**` branch so a promotion repo could
|
||||
/// never take effect there.
|
||||
/// Reject parent-only git-sync settings on a fork workspace. Auto-pull and fork
|
||||
/// PRs are configured at the parent: repo → fork sync is routed by the parent's
|
||||
/// webhook/poller (`sync_forks`), and a fork-owned auto-pull would register a
|
||||
/// second webhook on the same GitHub repo per fork. Promotion mode is rejected
|
||||
/// on throwaway forks (their deploys always go to their `wm-fork/**` branch, so
|
||||
/// a promotion repo could never take effect) but allowed on a **dev workspace**,
|
||||
/// which deploys per-item `wm_deploy/**` branches that promote into the parent.
|
||||
async fn reject_parent_only_git_sync_settings_on_fork<'a>(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
mut repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
) -> Result<()> {
|
||||
let offending = repos.find_map(|r| {
|
||||
let row = sqlx::query!(
|
||||
"SELECT parent_workspace_id, is_dev_workspace FROM workspace WHERE id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
let is_fork = row
|
||||
.as_ref()
|
||||
.and_then(|r| r.parent_workspace_id.as_ref())
|
||||
.is_some()
|
||||
|| w_id.starts_with(windmill_common::workspaces::WM_FORK_PREFIX);
|
||||
if !is_fork {
|
||||
return Ok(());
|
||||
}
|
||||
let is_dev = row.map(|r| r.is_dev_workspace).unwrap_or(false);
|
||||
let offending = repos.into_iter().find_map(|r| {
|
||||
if r.auto_pull.as_ref().is_some_and(|a| a.enabled) {
|
||||
Some("Auto-pull")
|
||||
} else if r.use_individual_branch.unwrap_or(false) {
|
||||
} else if r.use_individual_branch.unwrap_or(false) && !is_dev {
|
||||
Some("Promotion mode")
|
||||
} else if r.fork_open_prs {
|
||||
Some("Opening PRs for fork deploys")
|
||||
@@ -848,17 +864,7 @@ async fn reject_parent_only_git_sync_settings_on_fork<'a>(
|
||||
None
|
||||
}
|
||||
});
|
||||
let Some(offending) = offending else {
|
||||
return Ok(());
|
||||
};
|
||||
let parent = sqlx::query_scalar!(
|
||||
"SELECT parent_workspace_id FROM workspace WHERE id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten();
|
||||
if parent.is_some() || w_id.starts_with(windmill_common::workspaces::WM_FORK_PREFIX) {
|
||||
if let Some(offending) = offending {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"{offending} cannot be configured on a fork workspace: it is managed from the parent workspace's git sync settings"
|
||||
)));
|
||||
@@ -3130,6 +3136,95 @@ async fn check_open_prs_license<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Promotion mode (`use_individual_branch`: per-item `wm_deploy/**` deploy
|
||||
/// branches) is an EE feature; runtime-gate it like auto-pull and PR creation
|
||||
/// so an enterprise binary without an active plan can't enable it via either
|
||||
/// git-sync edit endpoint.
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_promotion_license<'a>(
|
||||
mut repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
) -> Result<()> {
|
||||
if repos.any(|r| r.use_individual_branch.unwrap_or(false)) {
|
||||
check_git_sync_ee_license("Promotion mode").await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Promotion on a dev workspace needs the dev-aware sync script (hub >= 28796):
|
||||
/// an older pinned script bundles a CLI that force-disables per-item branches
|
||||
/// on every fork, so enabling promotion would silently keep deploying to the
|
||||
/// env-label branch. Reject with an actionable error instead (the dispatcher
|
||||
/// demotes inherited configs the same way). Roots run promotion on any script
|
||||
/// version, and auto-managed repositories (no pin) always use the latest.
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_dev_promotion_script_version<'a>(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
) -> Result<()> {
|
||||
let mut offending: Option<String> = None;
|
||||
for r in repos {
|
||||
if !r.use_individual_branch.unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
if !r.is_script_meets_min_version(28796)? {
|
||||
offending = Some(r.effective_script_path().to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some(offending) = offending else {
|
||||
return Ok(());
|
||||
};
|
||||
let is_dev = sqlx::query!(
|
||||
"SELECT parent_workspace_id, is_dev_workspace FROM workspace WHERE id = $1",
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.map(|r| r.is_dev_workspace)
|
||||
.unwrap_or(false);
|
||||
if !is_dev {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::BadRequest(format!(
|
||||
"Promotion mode on a dev workspace requires git sync script version 28796 or newer, \
|
||||
but this repository pins '{offending}'. Update the pinned sync script (or reset it to \
|
||||
auto-managed) first."
|
||||
)))
|
||||
}
|
||||
|
||||
/// A dev workspace's promotion must target its parent ("prod") workspace's own
|
||||
/// git repository (same URL and branch) — that is what "promote to prod" means.
|
||||
/// A fork-created dev inherits prod's repo; an **attached** dev keeps its own,
|
||||
/// which may be unrelated. Reject enabling promotion on a repo the parent does
|
||||
/// not track so the UI can't present an unrelated repo as prod's target. The
|
||||
/// deploy path re-checks the same invariant (a resource edit could break it
|
||||
/// after save), via the shared `dev_promotion_target_matches_parent`.
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
async fn check_dev_promotion_targets_parent_repo<'a>(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
) -> Result<()> {
|
||||
for r in repos.filter(|r| r.use_individual_branch.unwrap_or(false)) {
|
||||
if !windmill_common::git_sync_ee::dev_promotion_target_matches_parent(
|
||||
db,
|
||||
w_id,
|
||||
&r.git_repo_resource_path,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
"Promotion mode on a dev workspace must reuse the parent workspace's git repository \
|
||||
(same URL and branch), but this repository is not one the parent tracks — promotion \
|
||||
would target a repository the parent does not sync with."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_git_sync_access(_db: &DB, _w_id: &str) -> Result<()> {
|
||||
Ok(())
|
||||
@@ -3274,6 +3369,25 @@ async fn edit_git_sync_config(
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_open_prs_license(git_sync_settings.repositories.iter()).await?;
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_promotion_license(git_sync_settings.repositories.iter()).await?;
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_dev_promotion_script_version(&db, &w_id, git_sync_settings.repositories.iter())
|
||||
.await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
check_dev_promotion_targets_parent_repo(&db, &w_id, git_sync_settings.repositories.iter())
|
||||
.await?;
|
||||
// Promotion mode: EE only (mirrors edit_git_sync_repository).
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if git_sync_settings
|
||||
.repositories
|
||||
.iter()
|
||||
.any(|r| r.use_individual_branch.unwrap_or(false))
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
"Promotion mode is an Enterprise Edition feature".to_string(),
|
||||
));
|
||||
}
|
||||
// Preserve server-owned auto-pull state (webhook id/secret, synced sha, last
|
||||
// status) that the redacted GET response omits — otherwise a whole-config
|
||||
// save from the UI would drop the webhook secret (breaking delivery) or
|
||||
@@ -3486,6 +3600,13 @@ async fn edit_git_sync_repository(
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_open_prs_license(std::iter::once(&new_config.repository)).await?;
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_promotion_license(std::iter::once(&new_config.repository)).await?;
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_dev_promotion_script_version(&db, &w_id, std::iter::once(&new_config.repository)).await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
check_dev_promotion_targets_parent_repo(&db, &w_id, std::iter::once(&new_config.repository))
|
||||
.await?;
|
||||
|
||||
// Promotion mode: EE only
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
|
||||
Reference in New Issue
Block a user