Skip to content

Commit 0e298f7

Browse files
Alex Holmbergclaude
authored andcommitted
fix(11.3-01): detect correct repository from local git remote
The agent was picking the first repository from the project's connected repos, which happened to be the GitOps infrastructure repo instead of the application repo. Now the agent: 1. Detects the local git remote URL 2. Parses the repo name from the URL 3. Matches against connected repositories 4. Falls back to non-gitops repo if no match This matches the manual wizard behavior which shows: "Using detected repository: syncable-dev/ai-demo-project" Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5680cd8 commit 0e298f7

1 file changed

Lines changed: 88 additions & 2 deletions

File tree

src/agent/tools/platform/deploy_service.rs

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ use std::str::FromStr;
1313
use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
1414
use crate::analyzer::{AnalysisConfig, TechnologyCategory, analyze_project_with_config};
1515
use crate::platform::api::types::{
16-
CloudProvider, CreateDeploymentConfigRequest, build_cloud_runner_config,
16+
CloudProvider, CreateDeploymentConfigRequest, ProjectRepository, build_cloud_runner_config,
1717
};
1818
use crate::platform::api::{PlatformApiClient, PlatformApiError, TriggerDeploymentRequest};
1919
use crate::platform::PlatformSession;
2020
use crate::wizard::{
2121
RecommendationInput, recommend_deployment, get_provider_deployment_statuses,
2222
};
23+
use std::process::Command;
2324

2425
/// Arguments for the deploy service tool
2526
#[derive(Debug, Deserialize)]
@@ -534,7 +535,8 @@ User: "deploy this service"
534535
}
535536
};
536537

537-
let repo = match repositories.repositories.first() {
538+
// Smart repository selection: match local git remote or find non-gitops repo
539+
let repo = match find_matching_repository(&repositories.repositories, &self.project_path) {
538540
Some(r) => r,
539541
None => {
540542
return Ok(format_error_for_llm(
@@ -549,6 +551,13 @@ User: "deploy this service"
549551
}
550552
};
551553

554+
tracing::info!(
555+
"Deploy service: Using repository {} (id: {}), default_branch: {:?}",
556+
repo.repository_full_name,
557+
repo.repository_id,
558+
repo.default_branch
559+
);
560+
552561
// Use resolved environment ID from earlier
553562
if resolved_env_id.is_empty() {
554563
return Ok(format_error_for_llm(
@@ -726,6 +735,83 @@ fn get_service_name(path: &PathBuf) -> String {
726735
.unwrap_or_else(|| "service".to_string())
727736
}
728737

738+
/// Detect the git remote URL from a directory
739+
fn detect_git_remote(project_path: &PathBuf) -> Option<String> {
740+
let output = Command::new("git")
741+
.args(["remote", "get-url", "origin"])
742+
.current_dir(project_path)
743+
.output()
744+
.ok()?;
745+
746+
if output.status.success() {
747+
let url = String::from_utf8(output.stdout).ok()?;
748+
Some(url.trim().to_string())
749+
} else {
750+
None
751+
}
752+
}
753+
754+
/// Parse repository full name from git remote URL
755+
/// Handles both SSH (git@github.com:owner/repo.git) and HTTPS (https://github.com/owner/repo.git)
756+
fn parse_repo_from_url(url: &str) -> Option<String> {
757+
let url = url.trim();
758+
759+
// SSH format: git@github.com:owner/repo.git
760+
if url.starts_with("git@") {
761+
let parts: Vec<&str> = url.split(':').collect();
762+
if parts.len() == 2 {
763+
let path = parts[1].trim_end_matches(".git");
764+
return Some(path.to_string());
765+
}
766+
}
767+
768+
// HTTPS format: https://github.com/owner/repo.git
769+
if url.starts_with("https://") || url.starts_with("http://") {
770+
if let Some(path) = url.split('/').skip(3).collect::<Vec<_>>().join("/").strip_suffix(".git") {
771+
return Some(path.to_string());
772+
}
773+
// Without .git suffix
774+
let path: String = url.split('/').skip(3).collect::<Vec<_>>().join("/");
775+
if !path.is_empty() {
776+
return Some(path);
777+
}
778+
}
779+
780+
None
781+
}
782+
783+
/// Find repository matching local git remote, or fall back to non-gitops repo
784+
fn find_matching_repository<'a>(
785+
repositories: &'a [ProjectRepository],
786+
project_path: &PathBuf,
787+
) -> Option<&'a ProjectRepository> {
788+
// First, try to detect from local git remote
789+
if let Some(detected_name) = detect_git_remote(project_path).and_then(|url| parse_repo_from_url(&url)) {
790+
tracing::debug!("Detected local git remote: {}", detected_name);
791+
792+
if let Some(repo) = repositories.iter().find(|r| {
793+
r.repository_full_name.eq_ignore_ascii_case(&detected_name)
794+
}) {
795+
tracing::debug!("Matched detected repo: {}", repo.repository_full_name);
796+
return Some(repo);
797+
}
798+
}
799+
800+
// Fall back: find first non-GitOps repository
801+
// GitOps repos are typically infrastructure/config repos, not application repos
802+
if let Some(repo) = repositories.iter().find(|r| {
803+
r.is_primary_git_ops != Some(true) &&
804+
!r.repository_full_name.to_lowercase().contains("infrastructure") &&
805+
!r.repository_full_name.to_lowercase().contains("gitops")
806+
}) {
807+
tracing::debug!("Using non-gitops repo: {}", repo.repository_full_name);
808+
return Some(repo);
809+
}
810+
811+
// Last resort: first repo
812+
repositories.first()
813+
}
814+
729815
/// Format a PlatformApiError for LLM consumption
730816
fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
731817
match error {

0 commit comments

Comments
 (0)