2
0

ci(actions-manager): add build and release workflows
Some checks failed
Build and Test / build (push) Has been cancelled
Build and Test / docker (push) Has been cancelled

Add Gitea Actions workflows for automated CI/CD. Build workflow runs on push/PR to build, test, and push Docker images. Release workflow triggers on version tags to build multi-platform binaries, package NuGet client, create GitHub releases, and publish versioned Docker images. Also add REST API controller for AI services alongside existing gRPC endpoints.
This commit is contained in:
2026-01-19 11:08:08 -05:00
parent 17581918dd
commit 48c3ac9dac
4 changed files with 614 additions and 11 deletions

View File

@@ -0,0 +1,57 @@
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore GitCaddy.AI.sln
- name: Build
run: dotnet build GitCaddy.AI.sln -c Release --no-restore
- name: Test
run: dotnet test GitCaddy.AI.sln -c Release --no-build --verbosity normal
docker:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.REGISTRY_URL }}/gitcaddy/gitcaddy-ai:latest
${{ secrets.REGISTRY_URL }}/gitcaddy/gitcaddy-ai:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -0,0 +1,85 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Restore dependencies
run: dotnet restore GitCaddy.AI.sln
- name: Build
run: dotnet build GitCaddy.AI.sln -c Release --no-restore -p:Version=${{ steps.version.outputs.VERSION }}
- name: Publish Service
run: |
dotnet publish src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj -c Release -o publish/linux-x64 -r linux-x64 --self-contained -p:Version=${{ steps.version.outputs.VERSION }}
dotnet publish src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj -c Release -o publish/win-x64 -r win-x64 --self-contained -p:Version=${{ steps.version.outputs.VERSION }}
- name: Package artifacts
run: |
cd publish
tar -czf gitcaddy-ai-${{ steps.version.outputs.VERSION }}-linux-x64.tar.gz linux-x64/
zip -r gitcaddy-ai-${{ steps.version.outputs.VERSION }}-win-x64.zip win-x64/
- name: Pack NuGet Client
run: dotnet pack src/GitCaddy.AI.Client/GitCaddy.AI.Client.csproj -c Release -o nupkg -p:Version=${{ steps.version.outputs.VERSION }}
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
publish/*.tar.gz
publish/*.zip
nupkg/*.nupkg
generate_release_notes: true
docker:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.REGISTRY_URL }}/gitcaddy/gitcaddy-ai:latest
${{ secrets.REGISTRY_URL }}/gitcaddy/gitcaddy-ai:${{ steps.version.outputs.VERSION }}
build-args: |
BUILD_CONFIGURATION=Release
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -0,0 +1,464 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Service.Services;
using GitCaddy.AI.Service.Licensing;
using Microsoft.AspNetCore.Mvc;
namespace GitCaddy.AI.Service.Controllers;
/// <summary>
/// REST API controller for AI services.
/// Provides HTTP endpoints alongside the gRPC service.
/// </summary>
[ApiController]
[Route("api/v1")]
public class AIController : ControllerBase
{
private readonly ICodeReviewService _codeReviewService;
private readonly ICodeIntelligenceService _codeIntelligenceService;
private readonly IIssueService _issueService;
private readonly IDocumentationService _documentationService;
private readonly ILicenseValidator _licenseValidator;
private readonly ILogger<AIController> _logger;
public AIController(
ICodeReviewService codeReviewService,
ICodeIntelligenceService codeIntelligenceService,
IIssueService issueService,
IDocumentationService documentationService,
ILicenseValidator licenseValidator,
ILogger<AIController> logger)
{
_codeReviewService = codeReviewService;
_codeIntelligenceService = codeIntelligenceService;
_issueService = issueService;
_documentationService = documentationService;
_licenseValidator = licenseValidator;
_logger = logger;
}
/// <summary>
/// Health check endpoint
/// </summary>
[HttpGet("health")]
public async Task<IActionResult> Health()
{
var licenseResult = await _licenseValidator.ValidateAsync();
return Ok(new
{
healthy = licenseResult.IsValid,
version = typeof(AIController).Assembly.GetName().Version?.ToString() ?? "1.0.0",
provider_status = new Dictionary<string, string>
{
["default"] = "operational"
},
license = licenseResult.License != null ? new
{
tier = licenseResult.License.Tier,
customer = licenseResult.License.Customer,
expires_at = licenseResult.License.ExpiresAt,
features = licenseResult.License.Features,
seat_count = licenseResult.License.SeatCount,
is_trial = licenseResult.License.IsTrial
} : null
});
}
/// <summary>
/// Review a pull request
/// </summary>
[HttpPost("review/pull-request")]
public async Task<IActionResult> ReviewPullRequest([FromBody] ReviewPullRequestDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.ReviewPullRequestRequest
{
RepoId = request.RepoId,
PullRequestId = request.PullRequestId,
BaseBranch = request.BaseBranch ?? "",
HeadBranch = request.HeadBranch ?? "",
PrTitle = request.PRTitle ?? "",
PrDescription = request.PRDescription ?? "",
Options = new Proto.ReviewOptions
{
CheckSecurity = request.Options?.CheckSecurity ?? true,
CheckPerformance = request.Options?.CheckPerformance ?? true,
CheckStyle = request.Options?.CheckStyle ?? true,
CheckTests = request.Options?.CheckTests ?? true,
SuggestImprovements = request.Options?.SuggestImprovements ?? true,
FocusAreas = request.Options?.FocusAreas ?? "",
LanguageHints = request.Options?.LanguageHints ?? ""
}
};
foreach (var file in request.Files ?? [])
{
protoRequest.Files.Add(new Proto.FileDiff
{
Path = file.Path ?? "",
OldPath = file.OldPath ?? "",
Status = file.Status ?? "modified",
Patch = file.Patch ?? "",
Content = file.Content ?? "",
Language = file.Language ?? ""
});
}
var response = await _codeReviewService.ReviewPullRequestAsync(protoRequest, cancellationToken);
return Ok(MapReviewResponse(response));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to review pull request");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Triage an issue
/// </summary>
[HttpPost("issues/triage")]
public async Task<IActionResult> TriageIssue([FromBody] TriageIssueDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.TriageIssueRequest
{
RepoId = request.RepoId,
IssueId = request.IssueId,
Title = request.Title ?? "",
Body = request.Body ?? ""
};
protoRequest.ExistingLabels.AddRange(request.ExistingLabels ?? []);
protoRequest.AvailableLabels.AddRange(request.AvailableLabels ?? []);
var response = await _issueService.TriageIssueAsync(protoRequest, cancellationToken);
return Ok(new
{
priority = response.Priority,
category = response.Category,
suggested_labels = response.SuggestedLabels.ToList(),
suggested_assignees = response.SuggestedAssignees.ToList(),
summary = response.Summary,
is_duplicate = response.IsDuplicate,
duplicate_of = response.DuplicateOf
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to triage issue");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Suggest labels for an issue
/// </summary>
[HttpPost("issues/suggest-labels")]
public async Task<IActionResult> SuggestLabels([FromBody] SuggestLabelsDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.SuggestLabelsRequest
{
RepoId = request.RepoId,
Title = request.Title ?? "",
Body = request.Body ?? ""
};
protoRequest.AvailableLabels.AddRange(request.AvailableLabels ?? []);
var response = await _issueService.SuggestLabelsAsync(protoRequest, cancellationToken);
return Ok(new
{
suggestions = response.Suggestions.Select(s => new
{
label = s.Label,
confidence = s.Confidence,
reason = s.Reason
}).ToList()
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to suggest labels");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Explain code
/// </summary>
[HttpPost("code/explain")]
public async Task<IActionResult> ExplainCode([FromBody] ExplainCodeDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.ExplainCodeRequest
{
RepoId = request.RepoId,
FilePath = request.FilePath ?? "",
Code = request.Code ?? "",
StartLine = request.StartLine,
EndLine = request.EndLine,
Question = request.Question ?? ""
};
var response = await _codeIntelligenceService.ExplainCodeAsync(protoRequest, cancellationToken);
return Ok(new
{
explanation = response.Explanation,
key_concepts = response.KeyConcepts.ToList(),
references = response.References.Select(r => new
{
description = r.Description,
url = r.Url
}).ToList()
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to explain code");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Summarize changes
/// </summary>
[HttpPost("code/summarize")]
public async Task<IActionResult> SummarizeChanges([FromBody] SummarizeChangesDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.SummarizeChangesRequest
{
RepoId = request.RepoId,
Context = request.Context ?? ""
};
foreach (var file in request.Files ?? [])
{
protoRequest.Files.Add(new Proto.FileDiff
{
Path = file.Path ?? "",
OldPath = file.OldPath ?? "",
Status = file.Status ?? "modified",
Patch = file.Patch ?? "",
Content = file.Content ?? "",
Language = file.Language ?? ""
});
}
var response = await _codeIntelligenceService.SummarizeChangesAsync(protoRequest, cancellationToken);
return Ok(new
{
summary = response.Summary,
bullet_points = response.BulletPoints.ToList(),
impact_assessment = response.ImpactAssessment
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to summarize changes");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Generate documentation
/// </summary>
[HttpPost("docs/generate")]
public async Task<IActionResult> GenerateDocumentation([FromBody] GenerateDocumentationDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.GenerateDocumentationRequest
{
RepoId = request.RepoId,
FilePath = request.FilePath ?? "",
Code = request.Code ?? "",
DocType = request.DocType ?? "function",
Language = request.Language ?? "",
Style = request.Style ?? "markdown"
};
var response = await _documentationService.GenerateDocumentationAsync(protoRequest, cancellationToken);
return Ok(new
{
documentation = response.Documentation,
sections = response.Sections.Select(s => new
{
title = s.Title,
content = s.Content
}).ToList()
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate documentation");
return StatusCode(500, new { error = ex.Message });
}
}
/// <summary>
/// Generate commit message
/// </summary>
[HttpPost("docs/commit-message")]
public async Task<IActionResult> GenerateCommitMessage([FromBody] GenerateCommitMessageDto request, CancellationToken cancellationToken)
{
try
{
var protoRequest = new Proto.GenerateCommitMessageRequest
{
RepoId = request.RepoId,
Style = request.Style ?? "conventional"
};
foreach (var file in request.Files ?? [])
{
protoRequest.Files.Add(new Proto.FileDiff
{
Path = file.Path ?? "",
OldPath = file.OldPath ?? "",
Status = file.Status ?? "modified",
Patch = file.Patch ?? ""
});
}
var response = await _documentationService.GenerateCommitMessageAsync(protoRequest, cancellationToken);
return Ok(new
{
message = response.Message,
alternatives = response.Alternatives.ToList()
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate commit message");
return StatusCode(500, new { error = ex.Message });
}
}
private static object MapReviewResponse(Proto.ReviewPullRequestResponse response)
{
return new
{
summary = response.Summary,
comments = response.Comments.Select(c => new
{
path = c.Path,
line = c.Line,
end_line = c.EndLine,
body = c.Body,
severity = c.Severity.ToString().ToLower().Replace("comment_severity_", ""),
category = c.Category,
suggested_fix = c.SuggestedFix
}).ToList(),
verdict = response.Verdict.ToString().ToLower().Replace("review_verdict_", ""),
suggestions = response.Suggestions.ToList(),
security = new
{
issues = response.Security?.Issues.Select(i => new
{
path = i.Path,
line = i.Line,
issue_type = i.IssueType,
description = i.Description,
severity = i.Severity,
remediation = i.Remediation
}).ToList() ?? [],
risk_score = response.Security?.RiskScore ?? 0,
summary = response.Security?.Summary ?? ""
},
estimated_review_minutes = response.EstimatedReviewMinutes
};
}
}
// DTO classes for REST API
public class ReviewPullRequestDto
{
public long RepoId { get; set; }
public long PullRequestId { get; set; }
public string? BaseBranch { get; set; }
public string? HeadBranch { get; set; }
public List<FileDiffDto>? Files { get; set; }
public string? PRTitle { get; set; }
public string? PRDescription { get; set; }
public ReviewOptionsDto? Options { get; set; }
}
public class FileDiffDto
{
public string? Path { get; set; }
public string? OldPath { get; set; }
public string? Status { get; set; }
public string? Patch { get; set; }
public string? Content { get; set; }
public string? Language { get; set; }
}
public class ReviewOptionsDto
{
public bool CheckSecurity { get; set; } = true;
public bool CheckPerformance { get; set; } = true;
public bool CheckStyle { get; set; } = true;
public bool CheckTests { get; set; } = true;
public bool SuggestImprovements { get; set; } = true;
public string? FocusAreas { get; set; }
public string? LanguageHints { get; set; }
}
public class TriageIssueDto
{
public long RepoId { get; set; }
public long IssueId { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public List<string>? ExistingLabels { get; set; }
public List<string>? AvailableLabels { get; set; }
}
public class SuggestLabelsDto
{
public long RepoId { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public List<string>? AvailableLabels { get; set; }
}
public class ExplainCodeDto
{
public long RepoId { get; set; }
public string? FilePath { get; set; }
public string? Code { get; set; }
public int StartLine { get; set; }
public int EndLine { get; set; }
public string? Question { get; set; }
}
public class SummarizeChangesDto
{
public long RepoId { get; set; }
public List<FileDiffDto>? Files { get; set; }
public string? Context { get; set; }
}
public class GenerateDocumentationDto
{
public long RepoId { get; set; }
public string? FilePath { get; set; }
public string? Code { get; set; }
public string? DocType { get; set; }
public string? Language { get; set; }
public string? Style { get; set; }
}
public class GenerateCommitMessageDto
{
public long RepoId { get; set; }
public List<FileDiffDto>? Files { get; set; }
public string? Style { get; set; }
}

View File

@@ -41,6 +41,10 @@ builder.Services.AddGrpc(options =>
options.MaxSendMessageSize = 50 * 1024 * 1024;
});
// Add REST API controllers
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// Add health checks
builder.Services.AddHealthChecks();
@@ -63,18 +67,11 @@ else
app.MapGrpcService<GitCaddyAIServiceImpl>();
app.MapHealthChecks("/healthz");
// Map REST API controllers
app.MapControllers();
// HTTP endpoint for basic health check
app.MapGet("/", () => "GitCaddy AI Service is running. Use gRPC to connect.");
app.MapGet("/health", async (ILicenseValidator validator) =>
{
var result = await validator.ValidateAsync();
return Results.Json(new
{
healthy = result.IsValid,
version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
license = result.License
});
});
app.MapGet("/", () => "GitCaddy AI Service is running. Use gRPC or REST API to connect.");
Log.Information("GitCaddy AI Service starting on {Urls}", string.Join(", ", app.Urls));