Git Service Feature
This feature provides git repository cloning and management for projects.
Overview
The Git Service:
- Clones repositories using the project’s encrypted git token
- Organizes repos by tenant and project ID:
{baseReposPath}/{tenantId}/{projectId}/{repoName} - Uses a shared folder accessible by both the API and Python parser
- Automatically clones repositories when project config is updated
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Shared Volume │
│ /app/repos (or local path) │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ {tenantId}/ ││
│ │ └── {projectId}/ ││
│ │ ├── repo1/ ││
│ │ ├── repo2/ ││
│ │ └── repo3/ ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
▲ ▲
│ │
┌────┴────┐ ┌────┴────┐
│ MILTON │ │ Python │
│ API │ │ Parser │
└─────────┘ └─────────┘
How It Works
-
Config Update Triggers Clone: When
PUT /api/projects/{projectId}/configis called with repositories, aCloneRepositoriesCommandis published. -
Async Processing: The command is processed by
CloneRepositoriesHandleron a dedicatedgit-operationsqueue. -
Token Decryption: The
IGitCredentialServiceretrieves and decrypts the git token for authentication. -
Clone Operation:
IGitServiceclones each repository to the project’s folder.
Services
IGitService
Main interface for git operations:
public interface IGitService
{
Task<GitCloneResult> CloneRepositoryAsync(
Guid tenantId, int projectId, string repositoryUrl,
string? gitToken = null, CancellationToken ct = default);
Task<List<GitCloneResult>> CloneAllRepositoriesAsync(
Guid tenantId, int projectId, CancellationToken ct = default);
string GetProjectRepositoriesPath(Guid tenantId, int projectId);
Task DeleteProjectRepositoriesAsync(
Guid tenantId, int projectId, CancellationToken ct = default);
}Usage Example
public class MyService
{
private readonly IGitService _gitService;
private readonly ITenantService _tenantService;
public async Task ProcessRepositoriesAsync(int projectId)
{
var tenantId = _tenantService.GetCurrentTenant();
// Get path to cloned repos
var reposPath = _gitService.GetProjectRepositoriesPath(tenantId, projectId);
// Enumerate cloned repositories
foreach (var repoDir in Directory.GetDirectories(reposPath))
{
// Process each repository...
}
}
}Configuration
Repository Path
Set via configuration or environment variable:
{
"Git": {
"RepositoriesPath": "C:/path/to/repos"
}
}Or via Aspire (set automatically by AppHost):
Git__RepositoriesPath=/app/repos
Queue Configuration
The git-operations queue is configured in Program.cs:
opts.LocalQueue("git-operations")
.UseDurableInbox() // Persist messages for reliability
.Sequential() // Process one at a time
.MaximumParallelMessages(1); // Avoid overwhelming git serversPython Parser Integration
When adding the Python parser as a container, mount the same repos folder:
// In AppHost.cs
var parser = builder.AddContainer("parser", "milton-parser")
.WithBindMount(reposPath, "/app/repos")
.WithEnvironment("REPOS_PATH", "/app/repos")
.WithReference(api);The Python parser can then access cloned repos at:
/app/repos/{tenantId}/{projectId}/{repoName}/
Wolverine Message Flow
┌─────────────────────┐
│ UpdateProjectConfig │
│ Endpoint │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ UpdateProjectConfig │
│ Handler │
│ (saves config) │
└─────────┬───────────┘
│
│ SendAsync(CloneRepositoriesCommand)
▼
┌─────────────────────┐
│ git-operations │
│ Local Queue │
│ (durable) │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ CloneRepositories │
│ Handler │
│ (clones all repos) │
└─────────────────────┘
Error Handling
- Failed clones are logged but don’t fail the entire operation
- Results include success/failure status for each repository
- The durable queue ensures operations survive application restarts