feat(ui): add ai settings web interface for repos and orgs
Add comprehensive web UI for configuring AI features at repository and organization levels, completing the activation workflow for AI operations. Repository AI Settings (repo/settings/ai): - Enable/disable AI unit for the repository - Toggle Tier 1 operations (auto-respond, auto-review, auto-triage, workflow inspection) - Configure Tier 2 agent mode with trigger labels and runtime limits - Set escalation rules (label, team assignment) - Override provider/model preferences - Add custom instructions for different operation types Organization AI Settings (org/settings/ai): - Configure org-level AI provider and model - Set encrypted API key (with masked display) - Define rate limits (max operations per hour) - Whitelist allowed operations - Enable/disable agent mode for org repositories Both interfaces include proper permission checks, form validation, and cascade resolution display (showing inherited vs. overridden values). Adds navigation entries to settings sidebars and full i18n support.
This commit is contained in:
@@ -8,16 +8,25 @@ import (
|
||||
"fmt"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
actions_model "code.gitcaddy.com/server/v3/models/actions"
|
||||
issues_model "code.gitcaddy.com/server/v3/models/issues"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
"code.gitcaddy.com/server/v3/modules/gitrepo"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
"code.gitcaddy.com/server/v3/modules/reqctx"
|
||||
actions_service "code.gitcaddy.com/server/v3/services/actions"
|
||||
issue_service "code.gitcaddy.com/server/v3/services/issue"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
// AgentWorkflowFile is the workflow file name expected in the repo for AI agent operations
|
||||
const AgentWorkflowFile = "ai-agent.yml"
|
||||
|
||||
// triggerAgentWorkflow triggers a Tier 2 agent Actions workflow for advanced AI work.
|
||||
// It posts a comment on the issue explaining that the agent is being dispatched,
|
||||
// and returns the action run ID for tracking.
|
||||
// It dispatches the ai-agent.yml workflow in the repo with issue context as inputs,
|
||||
// and posts a comment on the issue explaining that the agent is being dispatched.
|
||||
func triggerAgentWorkflow(ctx context.Context, repo *repo_model.Repository, aiCfg *repo_model.AIConfig, opLog *ai_model.OperationLog) (int64, error) {
|
||||
issue, err := issues_model.GetIssueByID(ctx, opLog.TargetID)
|
||||
if err != nil {
|
||||
@@ -25,32 +34,82 @@ func triggerAgentWorkflow(ctx context.Context, repo *repo_model.Repository, aiCf
|
||||
}
|
||||
issue.Repo = repo
|
||||
|
||||
// Open git repo for dispatch
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to open git repo: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
// Resolve default branch
|
||||
ref := repo.DefaultBranch
|
||||
if ref == "" {
|
||||
ref = "main"
|
||||
}
|
||||
|
||||
botUser := user_model.NewAIUser()
|
||||
|
||||
// Post a comment that the agent is being dispatched
|
||||
comment := fmt.Sprintf(
|
||||
// Truncate issue body for workflow input (Actions inputs have length limits)
|
||||
issueBody := issue.Content
|
||||
if len(issueBody) > 2000 {
|
||||
issueBody = issueBody[:2000] + "\n\n[truncated]"
|
||||
}
|
||||
|
||||
// Dispatch the workflow (DispatchActionWorkflow requires reqctx.RequestContext)
|
||||
dispatchCtx, finished := reqctx.NewRequestContext(ctx, "ai-agent-dispatch")
|
||||
defer finished()
|
||||
err = actions_service.DispatchActionWorkflow(reqctx.FromContext(dispatchCtx), botUser, repo, gitRepo, AgentWorkflowFile, ref,
|
||||
func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
|
||||
// Populate inputs defined in the workflow
|
||||
for name, config := range workflowDispatch.Inputs {
|
||||
switch name {
|
||||
case "issue_number":
|
||||
inputs[name] = fmt.Sprintf("%d", issue.Index)
|
||||
case "issue_title":
|
||||
inputs[name] = issue.Title
|
||||
case "issue_body":
|
||||
inputs[name] = issueBody
|
||||
case "max_run_minutes":
|
||||
inputs[name] = fmt.Sprintf("%d", getAgentMaxRunMinutes(aiCfg))
|
||||
case "system_instructions":
|
||||
inputs[name] = aiCfg.SystemInstructions
|
||||
default:
|
||||
inputs[name] = config.Default
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to dispatch agent workflow: %w", err)
|
||||
}
|
||||
|
||||
// Find the run that was just created to get its ID
|
||||
var runID int64
|
||||
latestRun, err := actions_model.GetLatestRun(ctx, repo.ID)
|
||||
if err == nil && latestRun != nil {
|
||||
runID = latestRun.ID
|
||||
}
|
||||
|
||||
// Post a comment on the issue
|
||||
commentBody := fmt.Sprintf(
|
||||
"An AI agent has been dispatched to investigate and work on this issue. "+
|
||||
"I'll create a pull request with the proposed changes once complete.\n\n"+
|
||||
"**Operation:** agent-fix\n"+
|
||||
"**Max runtime:** %d minutes",
|
||||
"**Operation:** `agent-fix`\n"+
|
||||
"**Max runtime:** %d minutes\n"+
|
||||
"**Workflow:** `%s`",
|
||||
getAgentMaxRunMinutes(aiCfg),
|
||||
AgentWorkflowFile,
|
||||
)
|
||||
if runID > 0 {
|
||||
commentBody += fmt.Sprintf("\n**Run:** [#%d](%s/actions/runs/%d)", runID, repo.HTMLURL(), runID)
|
||||
}
|
||||
|
||||
if _, err := issue_service.CreateIssueComment(ctx, botUser, repo, issue, comment, nil); err != nil {
|
||||
if _, err := issue_service.CreateIssueComment(ctx, botUser, repo, issue, commentBody, nil); err != nil {
|
||||
log.Error("Agent: failed to post dispatch comment on issue #%d: %v", issue.Index, err)
|
||||
}
|
||||
|
||||
// TODO: Implement actual workflow dispatch via services/actions.DispatchActionWorkflow()
|
||||
// This requires:
|
||||
// 1. A workflow template stored in the repo or generated dynamically
|
||||
// 2. Injecting the resolved API key and model as workflow inputs/secrets
|
||||
// 3. Labeling the job to run on ai-runner labeled runners
|
||||
//
|
||||
// For now, log the intent and return 0 as a placeholder run ID.
|
||||
// The full implementation will be completed when Actions runner integration is ready.
|
||||
log.Info("Agent: would dispatch workflow for issue #%d in repo %s (placeholder)", issue.Index, repo.FullName())
|
||||
|
||||
return 0, nil
|
||||
return runID, nil
|
||||
}
|
||||
|
||||
// getAgentMaxRunMinutes returns the configured max run time, with a default fallback
|
||||
@@ -60,3 +119,125 @@ func getAgentMaxRunMinutes(aiCfg *repo_model.AIConfig) int {
|
||||
}
|
||||
return 30 // default 30 minutes
|
||||
}
|
||||
|
||||
// AgentWorkflowTemplate is the recommended workflow file content for AI agent operations.
|
||||
// Repo owners should place this in .gitea/workflows/ai-agent.yml
|
||||
const AgentWorkflowTemplate = `# GitCaddy AI Agent Workflow
|
||||
# Place this file in .gitea/workflows/ai-agent.yml to enable Tier 2 AI operations.
|
||||
# Requires a runner with the 'ai-runner' label and Claude Code CLI installed.
|
||||
|
||||
name: AI Agent Fix
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
description: 'Issue number to fix'
|
||||
required: true
|
||||
issue_title:
|
||||
description: 'Issue title'
|
||||
required: true
|
||||
issue_body:
|
||||
description: 'Issue body/description'
|
||||
required: false
|
||||
default: ''
|
||||
max_run_minutes:
|
||||
description: 'Maximum run time in minutes'
|
||||
required: false
|
||||
default: '30'
|
||||
system_instructions:
|
||||
description: 'Custom system instructions for the AI'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
ai-fix:
|
||||
runs-on: [ai-runner]
|
||||
timeout-minutes: ${{ inputs.max_run_minutes }}
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create working branch
|
||||
run: |
|
||||
git checkout -b ai/fix-issue-${{ inputs.issue_number }}
|
||||
|
||||
- name: Run Claude Code
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.AI_API_KEY }}
|
||||
ISSUE_NUMBER: ${{ inputs.issue_number }}
|
||||
ISSUE_TITLE: ${{ inputs.issue_title }}
|
||||
ISSUE_BODY: ${{ inputs.issue_body }}
|
||||
SYSTEM_INSTRUCTIONS: ${{ inputs.system_instructions }}
|
||||
run: |
|
||||
PROMPT="Investigate and fix issue #${ISSUE_NUMBER}: ${ISSUE_TITLE}"
|
||||
if [ -n "${ISSUE_BODY}" ]; then
|
||||
PROMPT="${PROMPT}\n\nDescription:\n${ISSUE_BODY}"
|
||||
fi
|
||||
|
||||
SYSTEM="You are an AI agent working on a code repository. Your goal is to investigate the issue, understand the codebase, make the necessary changes, and ensure the fix is correct."
|
||||
if [ -n "${SYSTEM_INSTRUCTIONS}" ]; then
|
||||
SYSTEM="${SYSTEM}\n\nAdditional instructions from the repository maintainer:\n${SYSTEM_INSTRUCTIONS}"
|
||||
fi
|
||||
|
||||
claude --print \
|
||||
--system-prompt "${SYSTEM}" \
|
||||
--prompt "${PROMPT}" \
|
||||
--allowedTools "Edit,Read,Write,Bash,Grep,Glob" \
|
||||
--max-turns 50
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git config user.name "GitCaddy AI"
|
||||
git config user.email "ai@gitcaddy.com"
|
||||
git add -A
|
||||
git commit -m "fix: address issue #${{ inputs.issue_number }}
|
||||
|
||||
AI-generated fix for: ${{ inputs.issue_title }}
|
||||
|
||||
Co-Authored-By: GitCaddy AI <ai@gitcaddy.com>"
|
||||
git push origin ai/fix-issue-${{ inputs.issue_number }}
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.has_changes == 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Use gitea API to create PR (works on Gitea instances)
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/pulls" \
|
||||
-d "{
|
||||
\"title\": \"fix: address issue #${{ inputs.issue_number }}\",
|
||||
\"body\": \"AI-generated fix for #${{ inputs.issue_number }}: ${{ inputs.issue_title }}\n\nThis PR was created automatically by the GitCaddy AI agent.\",
|
||||
\"head\": \"ai/fix-issue-${{ inputs.issue_number }}\",
|
||||
\"base\": \"$(git remote show origin | grep 'HEAD branch' | cut -d: -f2 | tr -d ' ')\"
|
||||
}"
|
||||
|
||||
- name: Comment on issue if no changes
|
||||
if: steps.changes.outputs.has_changes == 'false'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/issues/${{ inputs.issue_number }}/comments" \
|
||||
-d '{"body": "I investigated this issue but was unable to determine code changes needed. A human reviewer should take a look."}'
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user