2
0
Files
gitcaddy-runner/internal/pkg/envcheck/capabilities.go
GitCaddy 48a589eb79
Some checks failed
CI / build-and-test (push) Failing after 2s
Release / build (amd64, darwin) (push) Successful in 6s
Release / build (amd64, linux) (push) Successful in 5s
Release / build (amd64, windows) (push) Successful in 6s
Release / build (arm64, darwin) (push) Successful in 5s
Release / build (arm64, linux) (push) Successful in 5s
Release / release (push) Successful in 11s
fix: add cross-platform disk detection for Windows/macOS builds
- Split detectDiskSpace() into platform-specific files with build tags
- disk_unix.go: Uses unix.Statfs for Linux and macOS
- disk_windows.go: Uses windows.GetDiskFreeSpaceEx for Windows
- Fixes Windows cross-compilation build errors

🤖 Generated with Claude Code
2026-01-11 19:29:27 +00:00

410 lines
10 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package envcheck
import (
"bufio"
"context"
"encoding/json"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/docker/docker/client"
)
// DiskInfo holds disk space information
type DiskInfo struct {
Total uint64 `json:"total_bytes"`
Free uint64 `json:"free_bytes"`
Used uint64 `json:"used_bytes"`
UsedPercent float64 `json:"used_percent"`
}
// DistroInfo holds Linux distribution information
type DistroInfo struct {
ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora"
VersionID string `json:"version_id,omitempty"` // e.g., "24.04", "12"
PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS"
}
// RunnerCapabilities represents the capabilities of a runner for AI consumption
type RunnerCapabilities struct {
OS string `json:"os"`
Arch string `json:"arch"`
Distro *DistroInfo `json:"distro,omitempty"`
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"`
Disk *DiskInfo `json:"disk,omitempty"`
Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"`
SuggestedLabels []string `json:"suggested_labels,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 Linux distribution
if runtime.GOOS == "linux" {
cap.Distro = detectLinuxDistro()
}
// 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)
// Detect disk space
cap.Disk = detectDiskSpace()
// Generate suggested labels based on detected capabilities
cap.SuggestedLabels = generateSuggestedLabels(cap)
return cap
}
// detectLinuxDistro reads /etc/os-release to get distribution info
func detectLinuxDistro() *DistroInfo {
file, err := os.Open("/etc/os-release")
if err != nil {
return nil
}
defer file.Close()
distro := &DistroInfo{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "ID=") {
distro.ID = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
} else if strings.HasPrefix(line, "VERSION_ID=") {
distro.VersionID = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"")
} else if strings.HasPrefix(line, "PRETTY_NAME=") {
distro.PrettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
}
}
if distro.ID == "" {
return nil
}
return distro
}
// generateSuggestedLabels creates industry-standard labels based on capabilities
func generateSuggestedLabels(cap *RunnerCapabilities) []string {
labels := []string{}
seen := make(map[string]bool)
addLabel := func(label string) {
if label != "" && !seen[label] {
seen[label] = true
labels = append(labels, label)
}
}
// OS labels
switch cap.OS {
case "linux":
addLabel("linux")
addLabel("linux-latest")
case "windows":
addLabel("windows")
addLabel("windows-latest")
case "darwin":
addLabel("macos")
addLabel("macos-latest")
}
// Distro labels (Linux only)
if cap.Distro != nil && cap.Distro.ID != "" {
distro := strings.ToLower(cap.Distro.ID)
addLabel(distro)
addLabel(distro + "-latest")
}
return labels
}
// 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
}