feat: Repo level management (#1828)
* move dev server scripts onto project repos * wip: move scripts onto Repo structs * wip: repo settings page * i18n and fixes * fix refresh * i18n * nits * clickable repo cards * view logs for all dev servers * updates to workspaces * Let's make some changes to the way applications are tested: (vibe-kanban 4592de6c) - When the user starts a dev server, we should automatically open the `PreviewPanel.tsx` - In the preview panel, if no dev server script is set for any of the repos in the workspace show a message along these lines: Vibe Kanban can run dev servers to help you test your changes. You can set this up in the repo settings area. You can learn more about testing applications here: https://www.vibekanban.com/docs/core-features/testing-your-application - We should also not show the `PreviewControlsContainer.tsx` if none of the repos have a dev server script `vibe-kanban/frontend/src/components/panels/PreviewPanel.tsx` `vibe-kanban/frontend/src/components/ui-new/actions/index.ts` `vibe-kanban/frontend/src/components/ui-new/containers/PreviewControlsContainer.tsx` --------- Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
committed by
GitHub
parent
4e20df9823
commit
c5554610a9
@@ -21,9 +21,9 @@ fn generate_types_content() -> String {
|
||||
db::models::project::SearchResult::decl(),
|
||||
db::models::project::SearchMatchType::decl(),
|
||||
db::models::repo::Repo::decl(),
|
||||
db::models::repo::UpdateRepo::decl(),
|
||||
db::models::project_repo::ProjectRepo::decl(),
|
||||
db::models::project_repo::CreateProjectRepo::decl(),
|
||||
db::models::project_repo::UpdateProjectRepo::decl(),
|
||||
db::models::workspace_repo::WorkspaceRepo::decl(),
|
||||
db::models::workspace_repo::CreateWorkspaceRepo::decl(),
|
||||
db::models::workspace_repo::RepoWithTargetBranch::decl(),
|
||||
@@ -134,7 +134,6 @@ fn generate_types_content() -> String {
|
||||
server::routes::task_attempts::pr::PrError::decl(),
|
||||
server::routes::task_attempts::BranchStatus::decl(),
|
||||
server::routes::task_attempts::RunScriptError::decl(),
|
||||
server::routes::task_attempts::DeleteWorkspaceError::decl(),
|
||||
server::routes::task_attempts::pr::AttachPrResponse::decl(),
|
||||
server::routes::task_attempts::pr::AttachExistingPrRequest::decl(),
|
||||
server::routes::task_attempts::pr::PrCommentsResponse::decl(),
|
||||
|
||||
@@ -14,7 +14,7 @@ use axum::{
|
||||
};
|
||||
use db::models::{
|
||||
project::{CreateProject, Project, ProjectError, SearchResult, UpdateProject},
|
||||
project_repo::{CreateProjectRepo, ProjectRepo, UpdateProjectRepo},
|
||||
project_repo::{CreateProjectRepo, ProjectRepo},
|
||||
repo::Repo,
|
||||
};
|
||||
use deployment::Deployment;
|
||||
@@ -568,20 +568,6 @@ pub async fn get_project_repository(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_project_repository(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path((project_id, repo_id)): Path<(Uuid, Uuid)>,
|
||||
Json(payload): Json<UpdateProjectRepo>,
|
||||
) -> Result<ResponseJson<ApiResponse<ProjectRepo>>, ApiError> {
|
||||
match ProjectRepo::update(&deployment.db().pool, project_id, repo_id, &payload).await {
|
||||
Ok(project_repo) => Ok(ResponseJson(ApiResponse::success(project_repo))),
|
||||
Err(db::models::project_repo::ProjectRepoError::NotFound) => Err(ApiError::BadRequest(
|
||||
"Repository not found in project".to_string(),
|
||||
)),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
let project_id_router = Router::new()
|
||||
.route(
|
||||
@@ -609,9 +595,7 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
.route("/", get(get_projects).post(create_project))
|
||||
.route(
|
||||
"/{project_id}/repositories/{repo_id}",
|
||||
get(get_project_repository)
|
||||
.put(update_project_repository)
|
||||
.delete(delete_project_repository),
|
||||
get(get_project_repository).delete(delete_project_repository),
|
||||
)
|
||||
.route("/stream/ws", get(stream_projects_ws))
|
||||
.nest("/{id}", project_id_router);
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
response::Json as ResponseJson,
|
||||
routing::{get, post},
|
||||
};
|
||||
use db::models::repo::Repo;
|
||||
use db::models::repo::{Repo, UpdateRepo};
|
||||
use deployment::Deployment;
|
||||
use serde::Deserialize;
|
||||
use services::services::git::GitBranch;
|
||||
@@ -92,6 +92,33 @@ pub async fn get_repos_batch(
|
||||
Ok(ResponseJson(ApiResponse::success(repos)))
|
||||
}
|
||||
|
||||
pub async fn get_repos(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<Repo>>>, ApiError> {
|
||||
let repos = Repo::list_all(&deployment.db().pool).await?;
|
||||
Ok(ResponseJson(ApiResponse::success(repos)))
|
||||
}
|
||||
|
||||
pub async fn get_repo(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path(repo_id): Path<Uuid>,
|
||||
) -> Result<ResponseJson<ApiResponse<Repo>>, ApiError> {
|
||||
let repo = deployment
|
||||
.repo()
|
||||
.get_by_id(&deployment.db().pool, repo_id)
|
||||
.await?;
|
||||
Ok(ResponseJson(ApiResponse::success(repo)))
|
||||
}
|
||||
|
||||
pub async fn update_repo(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path(repo_id): Path<Uuid>,
|
||||
ResponseJson(payload): ResponseJson<UpdateRepo>,
|
||||
) -> Result<ResponseJson<ApiResponse<Repo>>, ApiError> {
|
||||
let repo = Repo::update(&deployment.db().pool, repo_id, &payload).await?;
|
||||
Ok(ResponseJson(ApiResponse::success(repo)))
|
||||
}
|
||||
|
||||
pub async fn open_repo_in_editor(
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Path(repo_id): Path<Uuid>,
|
||||
@@ -141,9 +168,10 @@ pub async fn open_repo_in_editor(
|
||||
|
||||
pub fn router() -> Router<DeploymentImpl> {
|
||||
Router::new()
|
||||
.route("/repos", post(register_repo))
|
||||
.route("/repos", get(get_repos).post(register_repo))
|
||||
.route("/repos/init", post(init_repo))
|
||||
.route("/repos/batch", post(get_repos_batch))
|
||||
.route("/repos/{repo_id}", get(get_repo).put(update_repo))
|
||||
.route("/repos/{repo_id}/branches", get(get_repo_branches))
|
||||
.route("/repos/{repo_id}/open-editor", post(open_repo_in_editor))
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ use axum::{
|
||||
};
|
||||
use db::models::{
|
||||
execution_process::{ExecutionProcess, ExecutionProcessRunReason},
|
||||
project_repo::ProjectRepo,
|
||||
scratch::{Scratch, ScratchType},
|
||||
session::{CreateSession, Session},
|
||||
workspace::{Workspace, WorkspaceError},
|
||||
workspace_repo::WorkspaceRepo,
|
||||
};
|
||||
use deployment::Deployment;
|
||||
use executors::{
|
||||
@@ -26,7 +26,6 @@ use executors::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use services::services::container::ContainerService;
|
||||
use sqlx::Error as SqlxError;
|
||||
use ts_rs::TS;
|
||||
use utils::response::ApiResponse;
|
||||
use uuid::Uuid;
|
||||
@@ -144,18 +143,6 @@ pub async fn follow_up(
|
||||
variant: payload.variant,
|
||||
};
|
||||
|
||||
// Get parent task
|
||||
let task = workspace
|
||||
.parent_task(pool)
|
||||
.await?
|
||||
.ok_or(SqlxError::RowNotFound)?;
|
||||
|
||||
// Get parent project
|
||||
let project = task
|
||||
.parent_project(pool)
|
||||
.await?
|
||||
.ok_or(SqlxError::RowNotFound)?;
|
||||
|
||||
// If retry settings provided, perform replace-logic before proceeding
|
||||
if let Some(proc_id) = payload.retry_process_id {
|
||||
// Validate process belongs to this session
|
||||
@@ -196,10 +183,8 @@ pub async fn follow_up(
|
||||
|
||||
let prompt = payload.prompt;
|
||||
|
||||
let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
|
||||
let cleanup_action = deployment
|
||||
.container()
|
||||
.cleanup_actions_for_repos(&project_repos);
|
||||
let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
|
||||
let cleanup_action = deployment.container().cleanup_actions_for_repos(&repos);
|
||||
|
||||
let working_dir = workspace
|
||||
.agent_working_dir
|
||||
|
||||
@@ -26,7 +26,6 @@ use db::models::{
|
||||
coding_agent_turn::CodingAgentTurn,
|
||||
execution_process::{ExecutionProcess, ExecutionProcessRunReason, ExecutionProcessStatus},
|
||||
merge::{Merge, MergeStatus, PrMerge, PullRequestInfo},
|
||||
project_repo::ProjectRepo,
|
||||
repo::{Repo, RepoError},
|
||||
session::{CreateSession, Session},
|
||||
task::{Task, TaskRelationships, TaskStatus},
|
||||
@@ -1230,33 +1229,18 @@ pub async fn start_dev_server(
|
||||
}
|
||||
}
|
||||
|
||||
// Get dev script from project (dev_script is project-level, not per-repo)
|
||||
let dev_script = match &project.dev_script {
|
||||
Some(script) if !script.is_empty() => script.clone(),
|
||||
_ => {
|
||||
return Ok(ResponseJson(ApiResponse::error(
|
||||
"No dev server script configured for this project",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
|
||||
let repos_with_dev_script: Vec<_> = repos
|
||||
.iter()
|
||||
.filter(|r| r.dev_server_script.as_ref().is_some_and(|s| !s.is_empty()))
|
||||
.collect();
|
||||
|
||||
let working_dir = project
|
||||
.dev_script_working_dir
|
||||
.as_ref()
|
||||
.filter(|dir| !dir.is_empty())
|
||||
.cloned();
|
||||
if repos_with_dev_script.is_empty() {
|
||||
return Ok(ResponseJson(ApiResponse::error(
|
||||
"No dev server script configured for any repository in this workspace",
|
||||
)));
|
||||
}
|
||||
|
||||
let executor_action = ExecutorAction::new(
|
||||
ExecutorActionType::ScriptRequest(ScriptRequest {
|
||||
script: dev_script,
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::DevServer,
|
||||
working_dir,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
// Get or create a session for dev server
|
||||
let session = match Session::find_latest_by_workspace_id(pool, workspace.id).await? {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -1272,15 +1256,27 @@ pub async fn start_dev_server(
|
||||
}
|
||||
};
|
||||
|
||||
deployment
|
||||
.container()
|
||||
.start_execution(
|
||||
&workspace,
|
||||
&session,
|
||||
&executor_action,
|
||||
&ExecutionProcessRunReason::DevServer,
|
||||
)
|
||||
.await?;
|
||||
for repo in repos_with_dev_script {
|
||||
let executor_action = ExecutorAction::new(
|
||||
ExecutorActionType::ScriptRequest(ScriptRequest {
|
||||
script: repo.dev_server_script.clone().unwrap(),
|
||||
language: ScriptRequestLanguage::Bash,
|
||||
context: ScriptContext::DevServer,
|
||||
working_dir: Some(repo.name.clone()),
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
deployment
|
||||
.container()
|
||||
.start_execution(
|
||||
&workspace,
|
||||
&session,
|
||||
&executor_action,
|
||||
&ExecutionProcessRunReason::DevServer,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
deployment
|
||||
.track_if_analytics_allowed(
|
||||
@@ -1373,7 +1369,6 @@ pub async fn run_setup_script(
|
||||
.ensure_container_exists(&workspace)
|
||||
.await?;
|
||||
|
||||
// Get parent task and project
|
||||
let task = workspace
|
||||
.parent_task(pool)
|
||||
.await?
|
||||
@@ -1383,11 +1378,9 @@ pub async fn run_setup_script(
|
||||
.parent_project(pool)
|
||||
.await?
|
||||
.ok_or(SqlxError::RowNotFound)?;
|
||||
let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
|
||||
let executor_action = match deployment
|
||||
.container()
|
||||
.setup_actions_for_repos(&project_repos)
|
||||
{
|
||||
|
||||
let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
|
||||
let executor_action = match deployment.container().setup_actions_for_repos(&repos) {
|
||||
Some(action) => action,
|
||||
None => {
|
||||
return Ok(ResponseJson(ApiResponse::error_with_data(
|
||||
@@ -1457,7 +1450,6 @@ pub async fn run_cleanup_script(
|
||||
.ensure_container_exists(&workspace)
|
||||
.await?;
|
||||
|
||||
// Get parent task and project
|
||||
let task = workspace
|
||||
.parent_task(pool)
|
||||
.await?
|
||||
@@ -1467,11 +1459,9 @@ pub async fn run_cleanup_script(
|
||||
.parent_project(pool)
|
||||
.await?
|
||||
.ok_or(SqlxError::RowNotFound)?;
|
||||
let project_repos = ProjectRepo::find_by_project_id_with_names(pool, project.id).await?;
|
||||
let executor_action = match deployment
|
||||
.container()
|
||||
.cleanup_actions_for_repos(&project_repos)
|
||||
{
|
||||
|
||||
let repos = WorkspaceRepo::find_repos_for_workspace(pool, workspace.id).await?;
|
||||
let executor_action = match deployment.container().cleanup_actions_for_repos(&repos) {
|
||||
Some(action) => action,
|
||||
None => {
|
||||
return Ok(ResponseJson(ApiResponse::error_with_data(
|
||||
@@ -1580,34 +1570,19 @@ pub async fn get_first_user_message(
|
||||
Ok(ResponseJson(ApiResponse::success(message)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type", rename_all = "snake_case")]
|
||||
pub enum DeleteWorkspaceError {
|
||||
HasRunningProcesses,
|
||||
}
|
||||
|
||||
pub async fn delete_workspace(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
) -> Result<
|
||||
(
|
||||
StatusCode,
|
||||
ResponseJson<ApiResponse<(), DeleteWorkspaceError>>,
|
||||
),
|
||||
ApiError,
|
||||
> {
|
||||
) -> Result<(StatusCode, ResponseJson<ApiResponse<()>>), ApiError> {
|
||||
let pool = &deployment.db().pool;
|
||||
|
||||
// Check for running execution processes
|
||||
if ExecutionProcess::has_running_non_dev_server_processes_for_workspace(pool, workspace.id)
|
||||
.await?
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::CONFLICT,
|
||||
ResponseJson(ApiResponse::error_with_data(
|
||||
DeleteWorkspaceError::HasRunningProcesses,
|
||||
)),
|
||||
return Err(ApiError::Conflict(
|
||||
"Cannot delete workspace while processes are running. Stop all processes first."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user