diff --git a/go.mod b/go.mod index 036d395..7042e77 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.11 require ( - code.gitea.io/actions-proto-go v0.4.1 + code.gitea.io/actions-proto-go v0.5.0 code.gitea.io/gitea-vet v0.2.3 connectrpc.com/connect v1.16.2 github.com/avast/retry-go/v4 v4.6.0 @@ -108,3 +108,6 @@ replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.16.2 // Remove after replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0 + +// Use GitCaddy fork with capability support +replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.0 diff --git a/go.sum b/go.sum index 5a0c5c8..49b46b4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls= -code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= @@ -8,6 +6,8 @@ cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +git.marketally.com/gitcaddy/actions-proto-go v0.5.0 h1:D2loMnqTXiaJL6TMfOOUJz4/3Vpv0AnMDSJVuiqMNrM= +git.marketally.com/gitcaddy/actions-proto-go v0.5.0/go.mod h1:li5RzZsj1sV8a0SXzXWsGNwv0dYw7Wj829AgloZqF5o= gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742 h1:ulcquQluJbmNASkh6ina70LvcHEa9eWYfQ+DeAZ0VEE= gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= @@ -51,8 +51,6 @@ github.com/docker/cli v25.0.3+incompatible h1:KLeNs7zws74oFuVhgZQ5ONGZiXUUdgsdy6 github.com/docker/cli v25.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= -github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU= github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= @@ -233,8 +231,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= @@ -248,8 +244,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -271,20 +265,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/app/cmd/daemon.go b/internal/app/cmd/daemon.go index 06810b2..9072147 100644 --- a/internal/app/cmd/daemon.go +++ b/internal/app/cmd/daemon.go @@ -136,8 +136,19 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu runner := run.NewRunner(cfg, reg, cli) + // Detect runner capabilities for AI-friendly workflow generation + dockerHost := cfg.Container.DockerHost + if dockerHost == "" { + if dh, err := getDockerSocketPath(""); err == nil { + dockerHost = dh + } + } + capabilities := envcheck.DetectCapabilities(ctx, dockerHost) + capabilitiesJson := capabilities.ToJSON() + log.Infof("detected capabilities: %s", capabilitiesJson) + // declare the labels of the runner before fetching tasks - resp, err := runner.Declare(ctx, ls.Names()) + resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJson) if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented { log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later") return err diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index 4b0f76b..295dfe6 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -249,9 +249,10 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. return execErr } -func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) { +func (r *Runner) Declare(ctx context.Context, labels []string, capabilitiesJson string) (*connect.Response[runnerv1.DeclareResponse], error) { return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{ - Version: ver.Version(), - Labels: labels, + Version: ver.Version(), + Labels: labels, + CapabilitiesJson: capabilitiesJson, })) } diff --git a/internal/pkg/envcheck/capabilities.go b/internal/pkg/envcheck/capabilities.go new file mode 100644 index 0000000..3cfd601 --- /dev/null +++ b/internal/pkg/envcheck/capabilities.go @@ -0,0 +1,312 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package envcheck + +import ( + "context" + "encoding/json" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/docker/docker/client" +) + +// RunnerCapabilities represents the capabilities of a runner for AI consumption +type RunnerCapabilities struct { + OS string `json:"os"` + Arch string `json:"arch"` + Docker bool `json:"docker"` + DockerCompose bool `json:"docker_compose"` + ContainerRuntime string `json:"container_runtime,omitempty"` + Shell []string `json:"shell,omitempty"` + Tools map[string][]string `json:"tools,omitempty"` + Features *CapabilityFeatures `json:"features,omitempty"` + Limitations []string `json:"limitations,omitempty"` +} + +// CapabilityFeatures represents feature support flags +type CapabilityFeatures struct { + ArtifactsV4 bool `json:"artifacts_v4"` + Cache bool `json:"cache"` + Services bool `json:"services"` + CompositeActions bool `json:"composite_actions"` +} + +// DetectCapabilities detects the runner's capabilities +func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilities { + cap := &RunnerCapabilities{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Tools: make(map[string][]string), + Shell: detectShells(), + Features: &CapabilityFeatures{ + ArtifactsV4: false, // Gitea doesn't support v4 artifacts + Cache: true, + Services: true, + CompositeActions: true, + }, + Limitations: []string{ + "actions/upload-artifact@v4 not supported (use v3 or direct API upload)", + "actions/download-artifact@v4 not supported (use v3)", + }, + } + + // Detect Docker + cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost) + if cap.Docker { + cap.DockerCompose = detectDockerCompose(ctx) + cap.Features.Services = true + } + + // Detect common tools + detectTools(ctx, cap) + + return cap +} + +// ToJSON converts capabilities to JSON string for transmission +func (c *RunnerCapabilities) ToJSON() string { + data, err := json.Marshal(c) + if err != nil { + return "{}" + } + return string(data) +} + +func detectShells() []string { + shells := []string{} + + switch runtime.GOOS { + case "windows": + if _, err := exec.LookPath("pwsh"); err == nil { + shells = append(shells, "pwsh") + } + if _, err := exec.LookPath("powershell"); err == nil { + shells = append(shells, "powershell") + } + shells = append(shells, "cmd") + case "darwin": + if _, err := exec.LookPath("zsh"); err == nil { + shells = append(shells, "zsh") + } + if _, err := exec.LookPath("bash"); err == nil { + shells = append(shells, "bash") + } + shells = append(shells, "sh") + default: // linux and others + if _, err := exec.LookPath("bash"); err == nil { + shells = append(shells, "bash") + } + shells = append(shells, "sh") + } + + return shells +} + +func detectDocker(ctx context.Context, dockerHost string) (bool, string) { + opts := []client.Opt{client.FromEnv} + if dockerHost != "" { + opts = append(opts, client.WithHost(dockerHost)) + } + + cli, err := client.NewClientWithOpts(opts...) + if err != nil { + return false, "" + } + defer cli.Close() + + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + _, err = cli.Ping(timeoutCtx) + if err != nil { + return false, "" + } + + // Check if it's podman or docker + info, err := cli.Info(timeoutCtx) + if err == nil { + if strings.Contains(strings.ToLower(info.Name), "podman") { + return true, "podman" + } + } + + return true, "docker" +} + +func detectDockerCompose(ctx context.Context) bool { + // Check for docker compose v2 (docker compose) + cmd := exec.CommandContext(ctx, "docker", "compose", "version") + if err := cmd.Run(); err == nil { + return true + } + + // Check for docker-compose v1 + if _, err := exec.LookPath("docker-compose"); err == nil { + return true + } + + return false +} + +func detectTools(ctx context.Context, cap *RunnerCapabilities) { + toolDetectors := map[string]func(context.Context) []string{ + "node": detectNodeVersions, + "go": detectGoVersions, + "python": detectPythonVersions, + "java": detectJavaVersions, + "dotnet": detectDotnetVersions, + "rust": detectRustVersions, + } + + for tool, detector := range toolDetectors { + if versions := detector(ctx); len(versions) > 0 { + cap.Tools[tool] = versions + } + } +} + +func detectNodeVersions(ctx context.Context) []string { + return detectToolVersion(ctx, "node", "--version", "v") +} + +func detectGoVersions(ctx context.Context) []string { + return detectToolVersion(ctx, "go", "version", "go") +} + +func detectPythonVersions(ctx context.Context) []string { + versions := []string{} + + // Try python3 first + if v := detectToolVersion(ctx, "python3", "--version", "Python "); len(v) > 0 { + versions = append(versions, v...) + } + + // Also try python + if v := detectToolVersion(ctx, "python", "--version", "Python "); len(v) > 0 { + // Avoid duplicates + for _, ver := range v { + found := false + for _, existing := range versions { + if existing == ver { + found = true + break + } + } + if !found { + versions = append(versions, ver) + } + } + } + + return versions +} + +func detectJavaVersions(ctx context.Context) []string { + cmd := exec.CommandContext(ctx, "java", "-version") + output, err := cmd.CombinedOutput() + if err != nil { + return nil + } + + // Java version output goes to stderr and looks like: openjdk version "17.0.1" or java version "1.8.0_301" + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "version") { + // Extract version from quotes + start := strings.Index(line, "\"") + end := strings.LastIndex(line, "\"") + if start != -1 && end > start { + version := line[start+1 : end] + // Simplify version (e.g., "17.0.1" -> "17") + parts := strings.Split(version, ".") + if len(parts) > 0 { + if parts[0] == "1" && len(parts) > 1 { + return []string{parts[1]} // Java 8 style: 1.8 -> 8 + } + return []string{parts[0]} + } + } + } + } + + return nil +} + +func detectDotnetVersions(ctx context.Context) []string { + cmd := exec.CommandContext(ctx, "dotnet", "--list-sdks") + output, err := cmd.Output() + if err != nil { + return nil + } + + versions := []string{} + lines := strings.Split(string(output), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Format: "8.0.100 [/path/to/sdk]" + parts := strings.Split(line, " ") + if len(parts) > 0 { + version := parts[0] + // Simplify to major version + major := strings.Split(version, ".")[0] + // Avoid duplicates + found := false + for _, v := range versions { + if v == major { + found = true + break + } + } + if !found { + versions = append(versions, major) + } + } + } + + return versions +} + +func detectRustVersions(ctx context.Context) []string { + return detectToolVersion(ctx, "rustc", "--version", "rustc ") +} + +func detectToolVersion(ctx context.Context, cmd string, args string, prefix string) []string { + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + c := exec.CommandContext(timeoutCtx, cmd, args) + output, err := c.Output() + if err != nil { + return nil + } + + line := strings.TrimSpace(string(output)) + if prefix != "" { + if idx := strings.Index(line, prefix); idx != -1 { + line = line[idx+len(prefix):] + } + } + + // Get just the version number + parts := strings.Fields(line) + if len(parts) > 0 { + version := parts[0] + // Clean up version string + version = strings.TrimPrefix(version, "v") + // Return major.minor or just major + vparts := strings.Split(version, ".") + if len(vparts) >= 2 { + return []string{vparts[0] + "." + vparts[1]} + } + return []string{vparts[0]} + } + + return nil +}