feat: review git sync pull requests with ai on the instance (#12056)

* feat: add ai review to deployment requests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: bound ai review fetches and surface unreadable items

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: add fix in ai session and model picker to deployment ai review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: add workspace instructions for deployment ai review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: read review instructions from the parent and bind ai review to the fork

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: pin the fix-all session preview to the fork's compare page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: show the protection-rule bypass toggle inline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: make the bypass toggle small and red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: drop workspace-settings instructions from deployment ai review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: folder-scoped review instructions for the deployment ai review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: review the selected items with ai on compare and deploy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep reviewing when review instructions cannot be listed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: say the review instruction floor is a prompt request, not enforcement

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: mark an ai review out of date when its items change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update ee-repo-ref to 77703cf32e58964906d87ee83ddda44b5b8d1b7e

This commit updates the EE repository reference after PR #926 was merged in windmill-ee-private.

Previous ee-repo-ref: e30ab9e69d1854fcaed5620c934ace32acb0a729

New ee-repo-ref: 77703cf32e58964906d87ee83ddda44b5b8d1b7e

Automated by sync-ee-ref workflow.

* feat: review git sync pull requests with wmill review

* fix: withhold retyped resources and encryption keys, fall back from the responses route

* fix: withhold fileset children, workspace-specific resources and pathspec-named files

* feat: include the ready-to-post review in wmill review --json

* feat: review a workspace synced into a repository subfolder

* docs: say what wmill review takes from flags rather than wmill.yaml

* feat: review git sync pull requests with ai on the instance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjsYvwbvQh6Traiw1J3fRM

* chore: update ee-repo-ref for the ai review fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjsYvwbvQh6Traiw1J3fRM

* feat: point git sync ai review at the published hub script

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjsYvwbvQh6Traiw1J3fRM

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-10-28 06:05:47 +01:00
committed by GitHub
co-authored by Claude Opus 5.5 windmill-internal-app[bot]
parent fbe776d611
commit 375bf03cdc
11 changed files with 95 additions and 13 deletions
+1 -1
View File
@@ -1 +1 @@
d535972f0b35c6a7f257720816bdaec18465adef
bdc4ba4af27f57dc978a7ddca2dccd5b0d67f627
+1
View File
@@ -311,6 +311,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
for git_sync_path in [
windmill_common::workspaces::LATEST_GIT_SYNC_SCRIPT_PATH,
windmill_common::workspaces::GIT_SYNC_PULL_SCRIPT_PATH,
windmill_common::workspaces::GIT_SYNC_REVIEW_SCRIPT_PATH,
] {
let git_sync_path = git_sync_path.to_string();
if !all_paths.contains(&git_sync_path) {
@@ -1153,6 +1153,8 @@ async fn reject_parent_only_git_sync_settings_on_fork<'a>(
Some("Promotion mode")
} else if r.fork_open_prs {
Some("Opening PRs for fork deploys")
} else if r.ai_review_prs {
Some("Reviewing pull requests with AI")
} else {
None
}
@@ -7988,7 +7990,7 @@ async fn update_workspace_settings(
.filter(|r| !r.use_individual_branch.unwrap_or(false))
.take(1)
.map(|mut r| {
// Auto-pull and fork PRs are parent-owned and must not be inherited:
// Auto-pull, fork PRs and PR reviews are parent-owned and must not be inherited:
// the fork would otherwise carry the parent's webhook id (turning off
// auto-pull on the fork would delete the parent's webhook). A fork
// still inherits the push-direction config, the installation, and the
@@ -7997,6 +7999,7 @@ async fn update_workspace_settings(
// fork unqualified for managed features until its first check.
r.auto_pull = None;
r.fork_open_prs = false;
r.ai_review_prs = false;
r.open_pr_error = None;
r
})
@@ -11341,6 +11344,7 @@ async fn attach_dev_workspace(
}
r.auto_pull = None;
r.fork_open_prs = false;
r.ai_review_prs = false;
r.open_pr_error = None;
}
let serialized =
+3
View File
@@ -38690,6 +38690,9 @@ components:
type: boolean
fork_open_prs:
type: boolean
ai_review_prs:
type: boolean
description: review every pull request against the tracked branch with the workspace's AI
open_pr_error:
type: string
description: server-owned, last failure opening a PR for a deploy branch of this repo
+11
View File
@@ -216,6 +216,11 @@ pub const LATEST_GIT_SYNC_SCRIPT_PATH: &str = "hub/30052/sync-script-to-git-repo
/// reverse proxies reject as double-encoding when the client re-encodes it).
pub const GIT_SYNC_PULL_SCRIPT_PATH: &str = "hub/30051/git-sync-init-repository-windmill";
/// Hub script that reviews a pull request with the workspace's AI (`ai_review_prs`): clones
/// the base branch, fetches the pull request's head and runs `wmill review --json` from the
/// `windmill-cli` it pins. The server posts what it returns.
pub const GIT_SYNC_REVIEW_SCRIPT_PATH: &str = "hub/30053/git-sync-review-pull-request-windmill";
/// Prefix used to identify fork workspaces. A workspace whose id starts with this string is a
/// fork of another workspace.
pub const WM_FORK_PREFIX: &str = "wm-fork-";
@@ -378,6 +383,12 @@ pub struct GitRepositorySettings {
/// deploy callback reads this from the parent). Off by default.
#[serde(default, skip_serializing_if = "is_false")]
pub fork_open_prs: bool,
/// Review every pull request against the tracked branch with the workspace's AI and post
/// the findings on it (`wmill review`, run by `GIT_SYNC_REVIEW_SCRIPT_PATH`). Arrives on
/// the pull request webhook, so it needs a credential the server holds. Parent-owned like
/// `fork_open_prs`; off by default.
#[serde(default, skip_serializing_if = "is_false")]
pub ai_review_prs: bool,
/// Server-owned: the last failure opening a PR for a deploy branch of this
/// repo (e.g. the GitHub App installation hasn't approved the pull-request
/// permission). Written by the deploy completion hook, cleared on the next
+4 -3
View File
@@ -21,12 +21,13 @@ pub use git_sync_ee::{
tally_deployed_object_changes,
};
// The CI-test check exists only on enterprise builds; `private` alone (the CE image)
// The CI-test check and the pull request review exist only on enterprise builds; `private` alone (the CE image)
// compiles git_sync_ee without them.
#[cfg(all(feature = "private", feature = "enterprise"))]
pub use git_sync_ee::{
ensure_ci_test_check_for_pr, evaluate_and_conclude_ci_test_checks,
post_ci_test_check_not_applicable, resolve_pr_head_workspace,
enqueue_git_pr_review, ensure_ci_test_check_for_pr, evaluate_and_conclude_ci_test_checks,
post_ci_test_check_not_applicable, post_git_pr_review_result, resolve_pr_head_workspace,
GitPrReviewMarker,
};
#[cfg(not(feature = "private"))]
@@ -1842,6 +1842,14 @@ pub async fn process_completed_job(
.await;
maybe_record_git_sync_pushed_head(db, &job_id, &workspace_id, result.get()).await;
maybe_open_git_sync_deploy_pr(db, &job_id, &workspace_id, result.get()).await;
windmill_git_sync::post_git_pr_review_result(
db,
&job_id,
&workspace_id,
true,
result.get(),
)
.await;
}
// A CI test job just finished: advance any open "Windmill CI tests" PR check for
// its workspace. Detached, since concluding a check calls GitHub and this loop
@@ -1953,6 +1961,8 @@ pub async fn process_completed_job(
if job.kind == JobKind::DeploymentCallback {
maybe_post_git_sync_check(db, &job.id, &job.workspace_id, false, result.get()).await;
maybe_reconcile_git_sync_auto_pull(db, &job.id, &job.workspace_id, false, "").await;
windmill_git_sync::post_git_pr_review_result(db, &job.id, &job.workspace_id, false, "")
.await;
}
// A failed CI test job also settles its check; same detached advance as on success.
#[cfg(all(feature = "enterprise", feature = "private"))]
+2 -2
View File
@@ -4,13 +4,13 @@
anonymous usage-stats payload. It answers "does anyone use this, and which variant do they pick"
without any identifying data leaving the instance.
It currently carries 89 registered actions across thirty-four features (`ai_session`, `ai_chat`,
It currently carries 91 registered actions across thirty-five features (`ai_session`, `ai_chat`,
`ai_fix`, `ai_agent`, `ai_decision`, `ai_agent_eval`, `app_sandbox`, `datatable`, `db_manager`, `flow_editor`,
`flow_run`, `flow_step`, `home`, `header_nav`, `run_form`, `debugger`, `trigger`, `command_script`,
`mcp`, `mcp_workspace`, `hub_script`, `usage_meter`, `sso_groups_claim`, `cloud_trial_offer`,
`job_token_scopes`, `worker_draining`, `worker_group`, `worker_resource_stats`,
`dev_workspace_isolation`, `slack_agent`, `variable_expiration`, `anthropic_federation`,
`deployment_request`, `fork_roles`). Nearly all of the
`deployment_request`, `fork_roles`, `git_sync_review`). Nearly all of the
product is uninstrumented, so new user-facing work is the opportunity to change that.
## When to instrument
+19 -4
View File
@@ -734,12 +734,27 @@ those PRs). A timeout on a repository pinned to a sync script older than the one
pushed commits names that as the reason. Known limit (accepted for v1): the fork's status is workspace-wide (all its
tested items), which for the one-fork-per-PR model equals the PR's scope.
### Phase 8: AI review of pull requests (`wmill review`), CLI implemented
### Phase 8: AI review of pull requests (`wmill review`), implemented
The deployment request panel's AI review, run on a pull request against a synced repository.
One review, three runners: the panel, `wmill review` in a customer's CI, and later an
instance-side job on the `pull_request` webhook (a git sync setting next to `fork_open_prs`,
running `wmill review --json` from a pinned hub script and posting through the installation).
One review, three runners: the panel, `wmill review` in a customer's CI, and an instance-side
job on the pull request webhook, enabled per repository with `ai_review_prs`.
- **Instance side.** On a pull request event against the tracked branch, the server enqueues
`GIT_SYNC_REVIEW_SCRIPT_PATH` as the read-only sync identity with an
`__git_sync_ai_review` marker (repository identity, number, head). The hub script clones the
base branch, fetches the host's head ref, returns `{skipped}` when the pull request moved
past that head, and otherwise runs `wmill review --json` from the `windmill-cli` it pins.
The completion hook (`post_git_pr_review_result`) posts the `post` payload that JSON
carries through the credential the server holds: a GitHub review per head, deduplicated by
the same marker `--post github` writes, or the one managed GitLab note; a failed job posts
a note saying so. The script is on the installation-token allowlist by slug.
- **Who gets reviewed.** Only a head in the base repository: a contributor fork could
otherwise spend the workspace's AI at will. The setting is parent-owned like
`fork_open_prs`, so forks never hold it.
- **Delivery.** A change to the review reaches the instance side as a `windmill-cli` release,
then a hub script version pinning it, then a bump of `GIT_SYNC_REVIEW_SCRIPT_PATH`, the same
chain as the sync scripts.
- **One source for the review.** Prompt, size budget, withheld kinds and answer parsing live
in `frontend/src/lib/components/deploymentRequest/aiReviewCore.ts`, which imports nothing
@@ -191,7 +191,8 @@ export function createGitSyncContext(workspace: string) {
exclude_types_override: repo.exclude_types_override,
auto_pull: repo.auto_pull,
promotion_open_prs: repo.promotion_open_prs,
fork_open_prs: repo.fork_open_prs
fork_open_prs: repo.fork_open_prs,
ai_review_prs: repo.ai_review_prs
}
}
@@ -561,7 +562,8 @@ export function createGitSyncContext(workspace: string) {
exclude_types_override: repoToSave.exclude_types_override,
auto_pull: repoToSave.auto_pull,
promotion_open_prs: repoToSave.promotion_open_prs,
fork_open_prs: repoToSave.fork_open_prs
fork_open_prs: repoToSave.fork_open_prs,
ai_review_prs: repoToSave.ai_review_prs
}
}
})
@@ -18,6 +18,7 @@
import { sendUserToast } from '$lib/toast'
import { apiErrorMessage } from '$lib/utils'
import { workspaceStore, userWorkspaces, enterpriseLicense } from '$lib/stores'
import { copilotInfo } from '$lib/aiStore'
import type { GitSyncRepository } from './GitSyncContext.svelte'
import GitSyncModeDisplay from './GitSyncModeDisplay.svelte'
import Toggle from '$lib/components/Toggle.svelte'
@@ -95,6 +96,9 @@
function setForkOpenPrs(v: boolean) {
if (repo) repo.fork_open_prs = v
}
function setAiReviewPrs(v: boolean) {
if (repo) repo.ai_review_prs = v
}
function setPromotionOpenPrs(v: boolean) {
if (repo) repo.promotion_open_prs = v
}
@@ -168,6 +172,20 @@
// Not the recorded status: that is keyed by resource path and outlives a
// repoint, while the origin follows the repository the URL names now.
let hasManagedCredential = $derived(isGithubApp || credentialOrigin !== undefined)
// Why the AI review cannot be turned on, or undefined when it can. Pull request events
// arrive on the auto-pull webhook, the review posts through the credential Windmill holds,
// and the job asks the workspace's default model.
const aiReviewBlocker = $derived(
!$enterpriseLicense
? 'Requires an enterprise license.'
: !hasManagedCredential
? 'Needs a credential Windmill holds for the repository: the GitHub App, or a GitLab project access token.'
: !repo?.auto_pull?.enabled
? 'Requires automatic deploy from Git to be enabled above: pull request events arrive on its webhook.'
: !$copilotInfo.defaultModel
? 'Set a default model in the workspace AI settings first.'
: undefined
)
const MS_PER_DAY = 86_400_000
@@ -893,6 +911,23 @@
on:change={(e) => setSyncForks(e.detail)}
/>
</div>
<div class="mt-1">
<Toggle
disabled={!repo.ai_review_prs && aiReviewBlocker != undefined}
checked={repo.ai_review_prs ?? false}
options={{
right: 'Review pull requests with AI',
rightTooltip:
aiReviewBlocker ??
"Windmill reviews each pull request against the tracked branch with the workspace's default AI model and posts the findings on it, following the review instructions on the tracked branch. Pull requests from forks of the repository are not reviewed."
}}
on:change={(e) => setAiReviewPrs(e.detail)}
>
{#snippet right()}
{#if !$enterpriseLicense}<EEOnly />{/if}
{/snippet}
</Toggle>
</div>
{/if}
{#if !hasManagedCredential && !loadingResourceInfo}
<div class="mt-2">