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 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>
192 lines
5.8 KiB
Go
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
|
|
}
|