Reject automatic pull requests for Local runs

This commit is contained in:
Scott Werner 2026-09-02 17:15:52 -04:00
parent efcf8a0d93
commit 18cb752b61
4 changed files with 137 additions and 0 deletions

View file

@ -557,6 +557,8 @@ The `sandbox` transport runs the MCP server inside the workflow's sandbox. This
Automatically open a GitHub pull request when the workflow run completes successfully. Requires a [GitHub App](/integrations/github) to be configured.
Automatic pull requests require a clone-based Docker or Daytona environment. For in-place Local runs, leave pull requests disabled; run creation rejects a Local environment when `enabled = true`.
```toml title="run.toml"
[run.pull_request]
enabled = true

View file

@ -133,6 +133,10 @@ pub(crate) enum EnvironmentSelectionError {
NotFound { id: EnvironmentId },
#[error("{detail}")]
TargetUnsupported { detail: &'static str },
#[error(
"automatic pull requests require a clone-based Docker or Daytona environment; disable run.pull_request.enabled for Local execution"
)]
AutomaticPullRequestUnsupported,
#[error("{detail}")]
ProviderDisabled {
provider: SandboxProviderKind,

View file

@ -1041,6 +1041,11 @@ fn run_intent_admission_error(error: RunIntentAdmissionError) -> Response {
error.to_string(),
"target_environment_unsupported",
),
EnvironmentSelectionError::AutomaticPullRequestUnsupported => intent_error(
StatusCode::UNPROCESSABLE_ENTITY,
error.to_string(),
"pull_request_environment_unsupported",
),
EnvironmentSelectionError::ProviderDisabled { .. }
| EnvironmentSelectionError::MissingCredential { .. } => intent_error(
StatusCode::SERVICE_UNAVAILABLE,
@ -1118,6 +1123,15 @@ async fn validate_intent_environment(
if image_incompatible || target_incompatible {
return Err(EnvironmentSelectionError::TargetUnsupported { detail });
}
if configured_provider == SandboxProviderKind::Local
&& settings
.run
.pull_request
.as_ref()
.is_some_and(|pull_request| pull_request.enabled)
{
return Err(EnvironmentSelectionError::AutomaticPullRequestUnsupported);
}
if let Some(detail) =
run_manifest::sandbox_provider_policy_error(&state.server_settings(), effective_provider)
{

View file

@ -4251,6 +4251,123 @@ async fn post_runs_run_intent_canonicalizes_and_persists_a_local_folder_target()
assert!(projection.spec.definition_blob.is_some());
}
#[tokio::test]
async fn post_runs_run_intent_rejects_automatic_pull_requests_for_local_environment() {
let target = tempfile::tempdir().unwrap();
let state = local_test_app_state();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let workflow_version_id = store_workflow_version(
&state,
MINIMAL_DOT,
Some("_version = 1\n[run.pull_request]\nenabled = true\n"),
)
.await;
let response =
post_run_intent_response(&app, folder_intent(workflow_version_id, target.path())).await;
let body = response_json!(response, StatusCode::UNPROCESSABLE_ENTITY).await;
assert_eq!(
body["errors"][0]["code"],
"pull_request_environment_unsupported"
);
assert_eq!(
body["errors"][0]["detail"],
"automatic pull requests require a clone-based Docker or Daytona environment; disable run.pull_request.enabled for Local execution"
);
assert!(state.runs.lock().expect("runs lock poisoned").is_empty());
assert!(
state
.stores
.run_summaries
.list_identities()
.await
.unwrap()
.is_empty()
);
}
#[tokio::test]
async fn post_runs_run_intent_accepts_disabled_pull_requests_for_local_environment() {
let target = tempfile::tempdir().unwrap();
let state = local_test_app_state();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let workflow_version_id = store_workflow_version(
&state,
MINIMAL_DOT,
Some("_version = 1\n[run.pull_request]\nenabled = false\n"),
)
.await;
let body = post_run_manifest(&app, folder_intent(workflow_version_id, target.path())).await;
let run_id = body["id"].as_str().unwrap().parse::<RunId>().unwrap();
let projection = state
.stores
.runs
.open_run_reader(&run_id)
.await
.unwrap()
.state()
.await
.unwrap();
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Local
);
assert!(projection.spec.settings.run.pull_request.is_none());
}
#[tokio::test]
async fn post_runs_run_intent_accepts_automatic_pull_requests_for_configured_docker_dry_run() {
let state = test_app_state();
let app = crate::test_support::build_test_router(Arc::clone(&state));
let workflow_version_id = store_workflow_version(
&state,
MINIMAL_DOT,
Some("_version = 1\n[run.pull_request]\nenabled = true\n"),
)
.await;
let body = post_run_manifest(
&app,
json!({
"workflow_version_id": workflow_version_id,
"target": {
"kind": "git",
"repo": "fabro-sh/fabro",
"branch": "main"
},
"args": { "dry_run": true }
}),
)
.await;
let run_id = body["id"].as_str().unwrap().parse::<RunId>().unwrap();
let projection = state
.stores
.runs
.open_run_reader(&run_id)
.await
.unwrap()
.state()
.await
.unwrap();
assert_eq!(
projection.spec.settings.run.environment.provider,
EnvironmentProvider::Docker
);
assert_eq!(projection.spec.settings.run.execution.mode, RunMode::DryRun);
assert!(
projection
.spec
.settings
.run
.pull_request
.is_some_and(|pull_request| pull_request.enabled)
);
}
#[tokio::test]
async fn post_runs_run_intent_observes_folder_git_metadata_without_a_remote_call() {
let dir = tempfile::tempdir().unwrap();