mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat: allow git sync auto-pull, promotion and PRs on Pro licenses (#11173)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
64dffe6106
commit
02e47de8b4
+3
-15
@@ -4712,17 +4712,11 @@ const GIT_AUTO_PULL_LOCK_ID: i64 = 737_483_921;
|
||||
/// Poll every git-sync repository with auto-pull enabled and enqueue a pull when
|
||||
/// the tracked branch has new commits (repo → Windmill direction).
|
||||
///
|
||||
/// Runs on a single replica at a time (advisory lock) and only on
|
||||
/// Enterprise-licensed instances. Detection is `git ls-remote`; GitHub-App
|
||||
/// repositories are skipped here and sync via webhooks instead (phase 2).
|
||||
/// Runs on a single replica at a time (advisory lock). Detection is
|
||||
/// `git ls-remote`; GitHub-App repositories are skipped here and sync via
|
||||
/// webhooks instead (phase 2).
|
||||
#[cfg(feature = "private")]
|
||||
pub async fn poll_git_auto_pull(db: &Pool<Postgres>) {
|
||||
use windmill_common::ee_oss::{get_license_plan, LicensePlan};
|
||||
|
||||
if !matches!(get_license_plan().await, LicensePlan::Enterprise) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut lock_conn = match db.acquire().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -4792,12 +4786,6 @@ const GIT_CREDENTIAL_LOCK_ID: i64 = 737_483_923;
|
||||
/// sync down on its expiry date.
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
async fn maintain_git_credentials(db: &Pool<Postgres>) {
|
||||
use windmill_common::ee_oss::{get_license_plan, LicensePlan};
|
||||
|
||||
if !matches!(get_license_plan().await, LicensePlan::Enterprise) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction-scoped advisory lock, as for the schedule reconcile above: a
|
||||
// session lock on a pooled connection would ride back into the pool still
|
||||
// held if the sweep died before unlocking, and wedge the pass on every
|
||||
|
||||
@@ -1308,26 +1308,22 @@ async fn get_git_sync_deploy_mode(
|
||||
|
||||
let configured = !settings.repositories.is_empty();
|
||||
|
||||
// Auto-pull runs only on Enterprise-licensed instances (see poll_git_auto_pull);
|
||||
// without a caller branch there is nothing to match. Either way deploy_on_push
|
||||
// stays false and the caller falls back (git push via CI, or wmill sync push).
|
||||
// Auto-pull runs only in builds that compile the poller (`private`); without a
|
||||
// caller branch there is nothing to match. Either way deploy_on_push stays
|
||||
// false and the caller falls back (git push via CI, or wmill sync push).
|
||||
let Some(branch) = q.branch.as_deref() else {
|
||||
return Ok(Json(GitSyncDeployMode {
|
||||
configured,
|
||||
deploy_on_push: false,
|
||||
}));
|
||||
};
|
||||
let licensed = matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Enterprise
|
||||
);
|
||||
|
||||
// Count the auto-pull repos that would deploy this branch. We deliberately do
|
||||
// not check the caller's remote URL: with exactly one such repo the local
|
||||
// checkout is unambiguously it, and with several we can't tell which is the
|
||||
// caller's, so we report false and let the CLI ask the user.
|
||||
let mut matches = 0u32;
|
||||
if licensed && !root_deleted {
|
||||
if cfg!(feature = "private") && !root_deleted {
|
||||
for repo in &settings.repositories {
|
||||
let Some(auto_pull) = repo.auto_pull.as_ref() else {
|
||||
continue;
|
||||
@@ -3762,54 +3758,6 @@ fn cleanup_legacy_git_sync_settings_in_memory(
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
const CE_GIT_SYNC_MAX_USERS: i64 = 2;
|
||||
|
||||
/// Auto-pull is licensed per plan, not just per build: the poller only serves
|
||||
/// Enterprise plans at runtime, so the save path must reject the setting too —
|
||||
/// otherwise an EE binary without the plan could still register a webhook and
|
||||
/// receive webhook-driven pulls.
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_git_sync_ee_license(feature: &str) -> Result<()> {
|
||||
if !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Enterprise
|
||||
) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"{feature} requires an Enterprise license"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_auto_pull_license() -> Result<()> {
|
||||
check_git_sync_ee_license("Automatic pull from git").await
|
||||
}
|
||||
|
||||
/// In-app PR creation (promotion/fork deploy branches) drives GitHub API calls
|
||||
/// from the deploy completion hook; runtime-gate it like auto-pull.
|
||||
#[cfg(feature = "enterprise")]
|
||||
async fn check_open_prs_license<'a>(
|
||||
mut repos: impl Iterator<Item = &'a windmill_common::workspaces::GitRepositorySettings>,
|
||||
) -> Result<()> {
|
||||
if repos.any(|r| r.promotion_open_prs || r.fork_open_prs) {
|
||||
check_git_sync_ee_license("Opening pull requests from Windmill").await?;
|
||||
}
|
||||
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
|
||||
@@ -4061,18 +4009,6 @@ async fn edit_git_sync_config(
|
||||
));
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
if git_sync_settings
|
||||
.repositories
|
||||
.iter()
|
||||
.any(|r| r.auto_pull.as_ref().is_some_and(|a| a.enabled))
|
||||
{
|
||||
check_auto_pull_license().await?;
|
||||
}
|
||||
#[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"))]
|
||||
@@ -4310,19 +4246,6 @@ async fn edit_git_sync_repository(
|
||||
));
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
if new_config
|
||||
.repository
|
||||
.auto_pull
|
||||
.as_ref()
|
||||
.is_some_and(|a| a.enabled)
|
||||
{
|
||||
check_auto_pull_license().await?;
|
||||
}
|
||||
#[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))
|
||||
@@ -4428,13 +4351,6 @@ async fn edit_git_sync_repository(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// The request-side license gate above only saw the submitted config; the
|
||||
// preservation can resurrect an enabled auto_pull (None arm), so re-check
|
||||
// the effective state before it gets written and reconciled.
|
||||
#[cfg(feature = "enterprise")]
|
||||
if updated.auto_pull.as_ref().is_some_and(|a| a.enabled) {
|
||||
check_auto_pull_license().await?;
|
||||
}
|
||||
*existing_repo = updated;
|
||||
} else {
|
||||
// Repository doesn't exist, add it as a new repository
|
||||
|
||||
@@ -1254,18 +1254,6 @@ async fn maybe_open_git_sync_deploy_pr(
|
||||
if row.marker.is_none() {
|
||||
return;
|
||||
}
|
||||
// Runtime Enterprise gate, like the poller: the toggles may have been set
|
||||
// while a license was active (or written directly), and this hook drives
|
||||
// GitHub API calls with the installation token.
|
||||
if !matches!(
|
||||
windmill_common::ee_oss::get_license_plan().await,
|
||||
windmill_common::ee_oss::LicensePlan::Enterprise
|
||||
) {
|
||||
tracing::warn!(
|
||||
"git sync PR: skipping PR creation for {workspace_id}: requires an Enterprise license"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(repo_path) = row.repo_path else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -114,8 +114,9 @@
|
||||
try {
|
||||
await gitSyncContext.saveRepository(idx)
|
||||
} catch (e) {
|
||||
// The backend rejects promotion mode without an active EE plan; revert
|
||||
// the optimistic toggle instead of leaving it stuck on until reload.
|
||||
// The backend can reject promotion mode (non-EE build, outdated dev sync
|
||||
// script, dev repo not matching its parent's); revert the optimistic
|
||||
// toggle instead of leaving it stuck on until reload.
|
||||
if (repo) {
|
||||
repo.use_individual_branch = prevIndiv
|
||||
repo.group_by_folder = prevGbf
|
||||
|
||||
Reference in New Issue
Block a user