gitea/routers/api/v2/actions.go
logikonline fbd5da0acb
Some checks are pending
Build and Release / Lint and Test (push) Waiting to run
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Add AI-friendly enhancements: runner capabilities, release archive, action compatibility
- Add runner capability discovery API (v2) for AI tools to query before writing workflows
- Add release archive feature with filter toggle UI
- Add GitHub Actions compatibility layer with action aliasing
- Store runner capabilities JSON from act_runner Declare calls
- Add migrations for release archive and runner capabilities fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 04:56:11 -05:00

192 lines
5.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"encoding/json"
"time"
"code.gitea.io/gitea/modules/actions"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
)
// getSupportedActions converts the compatibility module data to API format
func getSupportedActions() map[string]api.ActionSupport {
result := make(map[string]api.ActionSupport)
for name, compat := range actions.BuiltinCompatibility {
result[name] = api.ActionSupport{
Versions: compat.Versions,
Notes: compat.Notes,
}
}
return result
}
// GetActionsCapabilities returns structured capability information for AI consumption
// @Summary Get runner capabilities for AI workflow generation
// @Description Returns detailed runner capabilities, platform info, and action compatibility
// @Tags actions
// @Accept json
// @Produce json
// @Param owner path string true "owner of the repo"
// @Param repo path string true "name of the repo"
// @Success 200 {object} api.ActionsCapabilitiesResponse
// @Router /repos/{owner}/{repo}/actions/runners/capabilities [get]
func GetActionsCapabilities(ctx *context.APIContext) {
repo := ctx.Repo.Repository
// Get runners available for this repository
runners, err := actions_model.GetRunnersOfRepo(ctx, repo.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
// Build response
response := &api.ActionsCapabilitiesResponse{
Runners: make([]*api.RunnerWithCapabilities, 0, len(runners)),
Platform: &api.PlatformInfo{
Type: "gitea",
Version: setting.AppVer,
DefaultActionsURL: setting.Actions.DefaultActionsURL.URL(),
SupportedActions: getSupportedActions(),
UnsupportedFeatures: actions.UnsupportedFeatures,
},
WorkflowHints: &api.WorkflowHints{
PreferredCheckout: "actions/checkout@v4",
ArtifactUploadAlternative: "Use Gitea API: curl -X POST $GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/{id}/assets",
SecretAccess: "Use ${{ secrets.NAME }} syntax",
},
}
// Process each runner
for _, runner := range runners {
status := "offline"
if runner.LastOnline.AsTime().Add(actions_model.RunnerOfflineTime).After(time.Now()) {
status = "online"
}
runnerResp := &api.RunnerWithCapabilities{
ID: runner.ID,
Name: runner.Name,
Status: status,
Labels: runner.AgentLabels,
}
// Parse capabilities JSON if available
if runner.CapabilitiesJSON != "" {
var cap api.RunnerCapability
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &cap); err == nil {
runnerResp.Capabilities = &cap
}
}
// If no capabilities, infer from labels
if runnerResp.Capabilities == nil {
runnerResp.Capabilities = inferCapabilitiesFromLabels(runner.AgentLabels)
}
response.Runners = append(response.Runners, runnerResp)
}
ctx.JSON(200, response)
}
// inferCapabilitiesFromLabels attempts to infer capabilities from runner labels
func inferCapabilitiesFromLabels(labels []string) *api.RunnerCapability {
cap := &api.RunnerCapability{
Limitations: []string{
"Capabilities inferred from labels - may not be accurate",
"actions/upload-artifact@v4 not supported (use v3 or direct API upload)",
},
}
for _, label := range labels {
switch label {
case "ubuntu-latest", "ubuntu-22.04", "ubuntu-20.04":
cap.OS = "linux"
cap.Shell = []string{"bash", "sh"}
case "windows-latest", "windows-2022", "windows-2019":
cap.OS = "windows"
cap.Shell = []string{"pwsh", "powershell", "cmd"}
case "macos-latest", "macos-13", "macos-12":
cap.OS = "darwin"
cap.Shell = []string{"bash", "sh", "zsh"}
case "linux":
cap.OS = "linux"
case "x64", "amd64":
cap.Arch = "amd64"
case "arm64", "aarch64":
cap.Arch = "arm64"
case "docker":
cap.Docker = true
cap.ContainerRuntime = "docker"
}
}
return cap
}
// ValidateWorkflow validates a workflow YAML and returns compatibility warnings
// @Summary Validate a workflow for compatibility
// @Description Parses workflow YAML and returns warnings about unsupported features
// @Tags actions
// @Accept json
// @Produce json
// @Param owner path string true "owner of the repo"
// @Param repo path string true "name of the repo"
// @Param body body api.WorkflowValidationRequest true "Workflow content"
// @Success 200 {object} api.WorkflowValidationResponse
// @Router /repos/{owner}/{repo}/actions/workflows/validate [post]
func ValidateWorkflow(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.WorkflowValidationRequest)
response := &api.WorkflowValidationResponse{
Valid: true,
Warnings: make([]*api.WorkflowValidationWarning, 0),
}
// Check for known incompatible actions using the compatibility module
for action, message := range actions.IncompatibleActions {
if containsAction(form.Content, action) {
response.Warnings = append(response.Warnings, &api.WorkflowValidationWarning{
Action: action,
Severity: "error",
Message: message,
Suggestion: actions.GetSuggestedAlternative(action),
})
}
}
if len(response.Warnings) > 0 {
response.Valid = false
}
ctx.JSON(200, response)
}
// containsAction checks if workflow content contains a specific action
func containsAction(content, action string) bool {
// Simple string search - could be enhanced with YAML parsing
return len(content) > 0 && len(action) > 0 &&
(contains(content, "uses: "+action) || contains(content, "uses: \""+action+"\""))
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstr(s, substr))
}
func containsSubstr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}