Run setup or cleanup scripts (vibe-kanban) (#1428)

* I've successfully implemented the two new API endpoints for running setup and cleanup scripts on task attempts. Here's a summary of the changes:

## Summary

### Files Modified

1. **`crates/server/src/routes/task_attempts.rs`**
   - Added `RunScriptError` enum type for error handling when no script is configured
   - Added `run_setup_script` handler - POST endpoint to run a project's setup script
   - Added `run_cleanup_script` handler - POST endpoint to run a project's cleanup script
   - Registered both routes: `/run-setup-script` and `/run-cleanup-script`

2. **`crates/server/src/bin/generate_types.rs`**
   - Added `RunScriptError` to the list of types to export to TypeScript

3. **`shared/types.ts`** (auto-generated)
   - Now includes the `RunScriptError` TypeScript type

### New Endpoints

- **`POST /api/task-attempts/{id}/run-setup-script`** - Runs the project's setup script in the task attempt's worktree
- **`POST /api/task-attempts/{id}/run-cleanup-script`** - Runs the project's cleanup script in the task attempt's worktree

Both endpoints:
- Return `ExecutionProcess` on success with the created execution process
- Return `RunScriptError::NoScriptConfigured` if the project doesn't have the respective script configured
- Track analytics events (`setup_script_executed` / `cleanup_script_executed`)
- Use the existing `ScriptContext::SetupScript` / `ScriptContext::CleanupScript` and `ExecutionProcessRunReason::SetupScript` / `ExecutionProcessRunReason::CleanupScript` patterns

* Done! I've added the running process check to both `run_setup_script` and `run_cleanup_script` endpoints. Here's a summary of the changes:

## Summary

### Files Modified

1. **`crates/db/src/models/execution_process.rs`**
   - Added `has_running_non_dev_server_processes` method that checks if any running processes (excluding dev servers) exist for a task attempt

2. **`crates/server/src/routes/task_attempts.rs`**
   - Added `ProcessAlreadyRunning` variant to `RunScriptError` enum
   - Added check at the start of both `run_setup_script` and `run_cleanup_script` handlers to return an error if non-dev-server processes are already running

3. **`shared/types.ts`** (auto-generated)
   - `RunScriptError` now includes both `no_script_configured` and `process_already_running` variants

### Behavior

Both endpoints now:
1. First check if any non-dev-server processes (coding agents, setup/cleanup scripts) are running on the task attempt
2. If running processes exist, return `RunScriptError::ProcessAlreadyRunning`
3. Otherwise proceed with script execution as before

* Cleanup script changes for task attempt cdb9cbd9-a6e7-4936-8e75-6dd837c634f3

* Done! I've implemented the frontend changes for the setup/cleanup script dropdown. Here's a summary:

## Summary of Frontend Changes

### Files Modified

1. **`frontend/src/lib/api.ts`**
   - Added `RunScriptError` to imports from shared types
   - Added `runSetupScript` and `runCleanupScript` methods to `attemptsApi`

2. **`frontend/src/components/tasks/TaskFollowUpSection.tsx`**
   - Added imports for `Terminal` icon, `DropdownMenu`, and `Tooltip` components
   - Added `attemptsApi` import
   - Updated `useProject()` to also get `project` data
   - Added computed values: `hasSetupScript`, `hasCleanupScript`, `hasAnyScript`
   - Added handlers: `handleRunSetupScript`, `handleRunCleanupScript`
   - Added dropdown button with Terminal icon after the Attach button
   - Dropdown only shows if project has at least one script configured
   - Menu items are disabled with tooltips when scripts are not configured
   - Button is disabled when `isAttemptRunning` is true

3. **`frontend/src/i18n/locales/en/tasks.json`**
   - Added translation strings for the dropdown menu items and tooltips

### Behavior

- **Dropdown visibility**: Only shown if project has `setup_script` or `cleanup_script` configured
- **Button disabled state**: When `isAttemptRunning` is true (tooltip explains why)
- **Menu item disabled states**:
  - "Run setup script" disabled if no `setup_script` (tooltip explains)
  - "Run cleanup script" disabled if no `cleanup_script` (tooltip explains)
- **After clicking**: Script starts executing and `isAttemptRunning` becomes true automatically, preventing further interactions

* Done! I've added translations for all languages:

- **Spanish (es)**:
  - "Ejecutar script de configuración" / "Ejecutar script de limpieza"
  - Tooltips in Spanish

- **Japanese (ja)**:
  - "セットアップスクリプトを実行" / "クリーンアップスクリプトを実行"
  - Tooltips in Japanese

- **Korean (ko)**:
  - "설정 스크립트 실행" / "정리 스크립트 실행"
  - Tooltips in Korean

* Done! I've removed all the fallback strings from the `t()` calls. All 5 translation calls now use just the key:

- `{t('followUp.scriptsDisabledWhileRunning')}`
- `{t('followUp.runSetupScript')}`
- `{t('followUp.noSetupScript')}`
- `{t('followUp.runCleanupScript')}`
- `{t('followUp.noCleanupScript')}`
This commit is contained in:
Louis Knight-Webb
2025-12-04 17:57:43 +00:00
committed by GitHub
parent 0d2e77dceb
commit 32c689dfc5
10 changed files with 331 additions and 6 deletions

View File

@@ -115,6 +115,7 @@ fn generate_types_content() -> String {
server::routes::task_attempts::PushError::decl(),
server::routes::task_attempts::CreatePrError::decl(),
server::routes::task_attempts::BranchStatus::decl(),
server::routes::task_attempts::RunScriptError::decl(),
services::services::filesystem::DirectoryEntry::decl(),
services::services::filesystem::DirectoryListResponse::decl(),
services::services::config::Config::decl(),

View File

@@ -1507,6 +1507,156 @@ pub async fn attach_existing_pr(
}
}
#[derive(Debug, Serialize, Deserialize, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type", rename_all = "snake_case")]
pub enum RunScriptError {
NoScriptConfigured,
ProcessAlreadyRunning,
}
#[axum::debug_handler]
pub async fn run_setup_script(
Extension(task_attempt): Extension<TaskAttempt>,
State(deployment): State<DeploymentImpl>,
) -> Result<ResponseJson<ApiResponse<ExecutionProcess, RunScriptError>>, ApiError> {
// Check if any non-dev-server processes are already running
if ExecutionProcess::has_running_non_dev_server_processes(
&deployment.db().pool,
task_attempt.id,
)
.await?
{
return Ok(ResponseJson(ApiResponse::error_with_data(
RunScriptError::ProcessAlreadyRunning,
)));
}
// Ensure worktree exists
let _ = ensure_worktree_path(&deployment, &task_attempt).await?;
// Get parent task and project
let task = task_attempt
.parent_task(&deployment.db().pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
let project = task
.parent_project(&deployment.db().pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
// Check if setup script is configured
let Some(setup_script) = project.setup_script else {
return Ok(ResponseJson(ApiResponse::error_with_data(
RunScriptError::NoScriptConfigured,
)));
};
// Create and execute the setup script action
let executor_action = ExecutorAction::new(
ExecutorActionType::ScriptRequest(ScriptRequest {
script: setup_script,
language: ScriptRequestLanguage::Bash,
context: ScriptContext::SetupScript,
}),
None,
);
let execution_process = deployment
.container()
.start_execution(
&task_attempt,
&executor_action,
&ExecutionProcessRunReason::SetupScript,
)
.await?;
deployment
.track_if_analytics_allowed(
"setup_script_executed",
serde_json::json!({
"task_id": task.id.to_string(),
"project_id": project.id.to_string(),
"attempt_id": task_attempt.id.to_string(),
}),
)
.await;
Ok(ResponseJson(ApiResponse::success(execution_process)))
}
#[axum::debug_handler]
pub async fn run_cleanup_script(
Extension(task_attempt): Extension<TaskAttempt>,
State(deployment): State<DeploymentImpl>,
) -> Result<ResponseJson<ApiResponse<ExecutionProcess, RunScriptError>>, ApiError> {
// Check if any non-dev-server processes are already running
if ExecutionProcess::has_running_non_dev_server_processes(
&deployment.db().pool,
task_attempt.id,
)
.await?
{
return Ok(ResponseJson(ApiResponse::error_with_data(
RunScriptError::ProcessAlreadyRunning,
)));
}
// Ensure worktree exists
let _ = ensure_worktree_path(&deployment, &task_attempt).await?;
// Get parent task and project
let task = task_attempt
.parent_task(&deployment.db().pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
let project = task
.parent_project(&deployment.db().pool)
.await?
.ok_or(SqlxError::RowNotFound)?;
// Check if cleanup script is configured
let Some(cleanup_script) = project.cleanup_script else {
return Ok(ResponseJson(ApiResponse::error_with_data(
RunScriptError::NoScriptConfigured,
)));
};
// Create and execute the cleanup script action
let executor_action = ExecutorAction::new(
ExecutorActionType::ScriptRequest(ScriptRequest {
script: cleanup_script,
language: ScriptRequestLanguage::Bash,
context: ScriptContext::CleanupScript,
}),
None,
);
let execution_process = deployment
.container()
.start_execution(
&task_attempt,
&executor_action,
&ExecutionProcessRunReason::CleanupScript,
)
.await?;
deployment
.track_if_analytics_allowed(
"cleanup_script_executed",
serde_json::json!({
"task_id": task.id.to_string(),
"project_id": project.id.to_string(),
"attempt_id": task_attempt.id.to_string(),
}),
)
.await;
Ok(ResponseJson(ApiResponse::success(execution_process)))
}
#[axum::debug_handler]
pub async fn gh_cli_setup_handler(
Extension(task_attempt): Extension<TaskAttempt>,
@@ -1552,6 +1702,8 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
.route("/gh-cli-setup", post(gh_cli_setup_handler))
.route("/commit-compare", get(compare_commit_to_head))
.route("/start-dev-server", post(start_dev_server))
.route("/run-setup-script", post(run_setup_script))
.route("/run-cleanup-script", post(run_cleanup_script))
.route("/branch-status", get(get_task_attempt_branch_status))
.route("/diff/ws", get(stream_task_attempt_diff_ws))
.route("/merge", post(merge_task_attempt))