8 Commits

Author SHA1 Message Date
GitCaddy
48a589eb79 fix: add cross-platform disk detection for Windows/macOS builds
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
- 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
GitCaddy
fef300dd5b docs: add HOWTOSTART.md guide for setting up runners
All checks were successful
CI / build-and-test (push) Successful in 17s
Comprehensive guide covering:
- Prerequisites and quick start
- Registration process
- Labels configuration
- Running as a systemd service
- Docker support
- Capabilities detection
- Troubleshooting tips

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:18:39 +00:00
GitCaddy
49a0b6f167 feat(capabilities): Add Linux distro detection and suggested labels
All checks were successful
CI / build-and-test (push) Successful in 14s
- Add DistroInfo struct to detect Linux distribution from /etc/os-release
- Add detectLinuxDistro() function to parse distro ID, version, pretty name
- Add generateSuggestedLabels() to create industry-standard labels
- Suggested labels include: linux/windows/macos, distro name, with -latest suffix

🤖 Generated with Claude Code
2026-01-11 17:25:45 +00:00
GitCaddy
e5fdaadbd2 feat: handle bandwidth test requests from server
All checks were successful
CI / build-and-test (push) Successful in 8s
- Update to actions-proto-go v0.5.7 with RequestBandwidthTest field
- Add SetBandwidthManager method to Poller
- Check FetchTaskResponse for bandwidth test request
- Include bandwidth in capabilities sent to server

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 15:29:21 +00:00
GitCaddy
ab382dc256 feat: add bandwidth testing to runner capabilities
All checks were successful
CI / build-and-test (push) Successful in 8s
- Add BandwidthManager for periodic bandwidth tests (hourly)
- Test download speed and latency against registered Gitea server
- Include bandwidth in runner capabilities JSON
- Add FormatBandwidth helper for display

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 07:38:49 +00:00
GitCaddy
2c66de4df2 fix: run make fmt for code formatting
Some checks failed
CI / build-and-test (push) Successful in 8s
Release / build (amd64, darwin) (push) Successful in 14s
Release / build (amd64, linux) (push) Successful in 13s
Release / build (amd64, windows) (push) Failing after 17s
Release / build (arm64, darwin) (push) Successful in 21s
Release / build (arm64, linux) (push) Successful in 5s
Release / release (push) Has been skipped
2026-01-11 07:11:49 +00:00
GitCaddy
9de33d4252 Send runner capabilities with FetchTask poll
Some checks failed
CI / build-and-test (push) Failing after 8s
- Add envcheck import and capability detection to poller.go
- Send capabilities JSON with every FetchTask request
- Use GitCaddy actions-proto-go v0.5.6 with CapabilitiesJson field
2026-01-11 07:03:54 +00:00
GitCaddy
ff8375c6e1 Add disk space reporting to runner capabilities
Some checks failed
CI / build-and-test (push) Failing after 4s
- Add DiskInfo struct with total, free, used bytes and usage percentage
- Detect disk space using unix.Statfs on startup
- Add periodic capabilities updates every 5 minutes
- Add disk space warnings at 85% (warning) and 95% (critical) thresholds
- Send updated capabilities to Gitea server periodically

This helps monitor runners that are running low on disk space.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 05:16:49 +00:00
9 changed files with 628 additions and 16 deletions

121
HOWTOSTART.md Normal file
View File

@@ -0,0 +1,121 @@
# How to Start a GitCaddy Runner
This guide explains how to set up and start a GitCaddy Actions runner (act_runner) to execute your CI/CD workflows.
## Prerequisites
- A Linux, macOS, or Windows machine
- Network access to your GitCaddy/Gitea instance
- (Optional) Docker installed for container-based workflows
## Quick Start
### 1. Download the Runner
Download the latest release from the [releases page](https://git.marketally.com/gitcaddy/act_runner/releases) or build from source:
```bash
git clone https://git.marketally.com/gitcaddy/act_runner.git
cd act_runner
make build
```
### 2. Register the Runner
Get a registration token from your GitCaddy instance:
- **Global runners**: Admin Area → Actions → Runners → Create Runner
- **Organization runners**: Organization Settings → Actions → Runners
- **Repository runners**: Repository Settings → Actions → Runners
Then register:
```bash
./act_runner register --no-interactive \
--instance https://your-gitea-instance.com \
--token YOUR_REGISTRATION_TOKEN \
--name my-runner \
--labels linux,ubuntu-latest
```
### 3. Start the Runner
```bash
./act_runner daemon
```
## Configuration Options
### Runner Labels
Labels determine which jobs the runner can execute. Configure labels during registration or edit them in the admin UI.
Common labels:
- `linux`, `linux-latest` - Linux runners
- `windows`, `windows-latest` - Windows runners
- `macos`, `macos-latest` - macOS runners
- `ubuntu`, `ubuntu-latest` - Ubuntu-specific
- `self-hosted` - Self-hosted runners
### Running as a Service
#### Linux (systemd)
```bash
sudo cat > /etc/systemd/system/act_runner.service << 'SERVICE'
[Unit]
Description=GitCaddy Actions Runner
After=network.target
[Service]
Type=simple
User=runner
WorkingDirectory=/opt/act_runner
ExecStart=/opt/act_runner/act_runner daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
SERVICE
sudo systemctl enable act_runner
sudo systemctl start act_runner
```
### Docker Support
For workflows that use container actions, ensure Docker is installed and the runner user has access:
```bash
sudo usermod -aG docker $USER
```
## Capabilities Detection
The runner automatically detects and reports:
- Operating system and architecture
- Available shells (bash, sh, powershell)
- Installed tools (node, python, go, etc.)
- Docker availability
- Disk space and network bandwidth
These capabilities help admins understand what each runner can handle.
## Troubleshooting
### Runner not connecting
1. Check network connectivity to your GitCaddy instance
2. Verify the registration token is valid
3. Check firewall rules allow outbound HTTPS
### Jobs not running
1. Verify runner labels match the job's `runs-on` requirement
2. Check runner is online in the admin panel
3. Review runner logs: `journalctl -u act_runner -f`
## More Information
- [act_runner Repository](https://git.marketally.com/gitcaddy/act_runner)
- [GitCaddy Documentation](https://git.marketally.com/gitcaddy/gitea)

5
go.mod
View File

@@ -23,6 +23,8 @@ require (
gotest.tools/v3 v3.5.1
)
require golang.org/x/sys v0.37.0
require (
cyphar.com/go-pathrs v0.2.1 // indirect
dario.cat/mergo v1.0.0 // indirect
@@ -96,7 +98,6 @@ require (
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.23.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@@ -110,4 +111,4 @@ replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.16.2
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.2
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.7

4
go.sum
View File

@@ -6,8 +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.2 h1:+0tDJeyduhxpYrBNScN8w5din/0zJ/KtAh/Eo6mE9QE=
git.marketally.com/gitcaddy/actions-proto-go v0.5.2/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
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=

View File

@@ -30,6 +30,20 @@ import (
"gitea.com/gitea/act_runner/internal/pkg/ver"
)
const (
// DiskSpaceWarningThreshold is the percentage at which to warn about low disk space
DiskSpaceWarningThreshold = 85.0
// DiskSpaceCriticalThreshold is the percentage at which to log critical warnings
DiskSpaceCriticalThreshold = 95.0
// CapabilitiesUpdateInterval is how often to update capabilities (including disk space)
CapabilitiesUpdateInterval = 5 * time.Minute
// BandwidthTestInterval is how often to run bandwidth tests (hourly)
BandwidthTestInterval = 1 * time.Hour
)
// Global bandwidth manager - accessible for triggering manual tests
var bandwidthManager *envcheck.BandwidthManager
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
cfg, err := config.LoadDefault(*configFile)
@@ -143,10 +157,21 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
dockerHost = dh
}
}
// Initialize bandwidth manager with the Gitea server address
bandwidthManager = envcheck.NewBandwidthManager(reg.Address, BandwidthTestInterval)
bandwidthManager.Start(ctx)
log.Infof("bandwidth manager started, testing against: %s", reg.Address)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost)
// Include initial bandwidth result if available
capabilities.Bandwidth = bandwidthManager.GetLastResult()
capabilitiesJson := capabilities.ToJSON()
log.Infof("detected capabilities: %s", capabilitiesJson)
// Check disk space and warn if low
checkDiskSpaceWarnings(capabilities)
// declare the labels of the runner before fetching tasks
resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJson)
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
@@ -160,7 +185,11 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
}
// Start periodic capabilities update goroutine
go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost)
poller := poll.New(cfg, cli, runner)
poller.SetBandwidthManager(bandwidthManager)
if daemArgs.Once || reg.Ephemeral {
done := make(chan struct{})
@@ -194,6 +223,67 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
}
// checkDiskSpaceWarnings logs warnings if disk space is low
func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
if capabilities.Disk == nil {
return
}
usedPercent := capabilities.Disk.UsedPercent
freeGB := float64(capabilities.Disk.Free) / (1024 * 1024 * 1024)
if usedPercent >= DiskSpaceCriticalThreshold {
log.Errorf("CRITICAL: Disk space critically low! %.1f%% used, only %.2f GB free. Runner may fail to execute jobs!", usedPercent, freeGB)
} else if usedPercent >= DiskSpaceWarningThreshold {
log.Warnf("WARNING: Disk space running low. %.1f%% used, %.2f GB free. Consider cleaning up disk space.", usedPercent, freeGB)
}
}
// periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string) {
ticker := time.NewTicker(CapabilitiesUpdateInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Debug("stopping periodic capabilities update")
if bandwidthManager != nil {
bandwidthManager.Stop()
}
return
case <-ticker.C:
// Detect updated capabilities (disk space changes over time)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost)
// Include latest bandwidth result
if bandwidthManager != nil {
capabilities.Bandwidth = bandwidthManager.GetLastResult()
}
capabilitiesJson := capabilities.ToJSON()
// Check for disk space warnings
checkDiskSpaceWarnings(capabilities)
// Send updated capabilities to server
_, err := runner.Declare(ctx, labelNames, capabilitiesJson)
if err != nil {
log.WithError(err).Debug("failed to update capabilities")
} else {
bandwidthInfo := ""
if capabilities.Bandwidth != nil {
bandwidthInfo = fmt.Sprintf(", bandwidth: %.1f Mbps", capabilities.Bandwidth.DownloadMbps)
}
log.Debugf("capabilities updated: disk %.1f%% used, %.2f GB free%s",
capabilities.Disk.UsedPercent,
float64(capabilities.Disk.Free)/(1024*1024*1024),
bandwidthInfo)
}
}
}
}
type daemonArgs struct {
Once bool
}

View File

@@ -18,13 +18,15 @@ import (
"gitea.com/gitea/act_runner/internal/app/run"
"gitea.com/gitea/act_runner/internal/pkg/client"
"gitea.com/gitea/act_runner/internal/pkg/config"
"gitea.com/gitea/act_runner/internal/pkg/envcheck"
)
type Poller struct {
client client.Client
runner *run.Runner
cfg *config.Config
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
client client.Client
runner *run.Runner
cfg *config.Config
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
bandwidthManager *envcheck.BandwidthManager
pollingCtx context.Context
shutdownPolling context.CancelFunc
@@ -57,6 +59,11 @@ func New(cfg *config.Config, client client.Client, runner *run.Runner) *Poller {
}
}
// SetBandwidthManager sets the bandwidth manager for on-demand testing
func (p *Poller) SetBandwidthManager(bm *envcheck.BandwidthManager) {
p.bandwidthManager = bm
}
func (p *Poller) Poll() {
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
wg := &sync.WaitGroup{}
@@ -157,11 +164,23 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
reqCtx, cancel := context.WithTimeout(ctx, p.cfg.Runner.FetchTimeout)
defer cancel()
// Detect capabilities including current disk space
caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost)
// Include latest bandwidth result if available
if p.bandwidthManager != nil {
caps.Bandwidth = p.bandwidthManager.GetLastResult()
}
capsJson := caps.ToJSON()
// Load the version value that was in the cache when the request was sent.
v := p.tasksVersion.Load()
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: v,
}))
fetchReq := &runnerv1.FetchTaskRequest{
TasksVersion: v,
CapabilitiesJson: capsJson,
}
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(fetchReq))
if errors.Is(err, context.DeadlineExceeded) {
err = nil
}
@@ -174,6 +193,18 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
return nil, false
}
// Check if server requested a bandwidth test
if resp.Msg.RequestBandwidthTest && p.bandwidthManager != nil {
log.Info("Server requested bandwidth test, running now...")
go func() {
result := p.bandwidthManager.RunTest(ctx)
if result != nil {
log.Infof("Bandwidth test completed: %.1f Mbps download, %.0f ms latency",
result.DownloadMbps, result.Latency)
}
}()
}
if resp.Msg.TasksVersion > v {
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
}
@@ -182,7 +213,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
return nil, false
}
// got a task, set `tasksVersion` to zero to focre query db in next request.
// got a task, set tasksVersion to zero to force query db in next request.
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
return resp.Msg.Task, true

View File

@@ -0,0 +1,209 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package envcheck
import (
"context"
"fmt"
"io"
"net/http"
"sync"
"time"
)
// BandwidthInfo holds network bandwidth test results
type BandwidthInfo struct {
DownloadMbps float64 `json:"download_mbps"`
UploadMbps float64 `json:"upload_mbps,omitempty"`
Latency float64 `json:"latency_ms,omitempty"`
TestedAt time.Time `json:"tested_at"`
}
// BandwidthManager handles periodic bandwidth testing
type BandwidthManager struct {
serverURL string
lastResult *BandwidthInfo
mu sync.RWMutex
testInterval time.Duration
stopChan chan struct{}
}
// NewBandwidthManager creates a new bandwidth manager
func NewBandwidthManager(serverURL string, testInterval time.Duration) *BandwidthManager {
return &BandwidthManager{
serverURL: serverURL,
testInterval: testInterval,
stopChan: make(chan struct{}),
}
}
// Start begins periodic bandwidth testing
func (bm *BandwidthManager) Start(ctx context.Context) {
// Run initial test
bm.RunTest(ctx)
// Start periodic testing
go func() {
ticker := time.NewTicker(bm.testInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
bm.RunTest(ctx)
case <-bm.stopChan:
return
case <-ctx.Done():
return
}
}
}()
}
// Stop stops the periodic testing
func (bm *BandwidthManager) Stop() {
close(bm.stopChan)
}
// RunTest runs a bandwidth test and stores the result
func (bm *BandwidthManager) RunTest(ctx context.Context) *BandwidthInfo {
result := TestBandwidth(ctx, bm.serverURL)
bm.mu.Lock()
bm.lastResult = result
bm.mu.Unlock()
return result
}
// GetLastResult returns the most recent bandwidth test result
func (bm *BandwidthManager) GetLastResult() *BandwidthInfo {
bm.mu.RLock()
defer bm.mu.RUnlock()
return bm.lastResult
}
// TestBandwidth tests network bandwidth to the Gitea server
func TestBandwidth(ctx context.Context, serverURL string) *BandwidthInfo {
if serverURL == "" {
return nil
}
info := &BandwidthInfo{
TestedAt: time.Now(),
}
// Test latency first
info.Latency = testLatency(ctx, serverURL)
// Test download speed
info.DownloadMbps = testDownloadSpeed(ctx, serverURL)
return info
}
func testLatency(ctx context.Context, serverURL string) float64 {
client := &http.Client{
Timeout: 10 * time.Second,
}
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, "HEAD", serverURL, nil)
if err != nil {
return 0
}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
return 0
}
resp.Body.Close()
latency := time.Since(start).Seconds() * 1000 // Convert to ms
return float64(int(latency*100)) / 100 // Round to 2 decimals
}
func testDownloadSpeed(ctx context.Context, serverURL string) float64 {
// Try multiple endpoints to accumulate ~1MB of data
endpoints := []string{
"/assets/css/index.css",
"/assets/js/index.js",
"/assets/img/logo.svg",
"/assets/img/logo.png",
"/",
}
client := &http.Client{
Timeout: 30 * time.Second,
}
var totalBytes int64
var totalDuration time.Duration
targetBytes := int64(1024 * 1024) // 1MB target
maxAttempts := 10 // Limit iterations
for attempt := 0; attempt < maxAttempts && totalBytes < targetBytes; attempt++ {
for _, endpoint := range endpoints {
if totalBytes >= targetBytes {
break
}
url := serverURL + endpoint
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
req, err := http.NewRequestWithContext(reqCtx, "GET", url, nil)
if err != nil {
cancel()
continue
}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
cancel()
continue
}
n, _ := io.Copy(io.Discard, resp.Body)
resp.Body.Close()
cancel()
duration := time.Since(start)
if n > 0 {
totalBytes += n
totalDuration += duration
}
}
}
if totalBytes == 0 || totalDuration == 0 {
return 0
}
// Calculate speed in Mbps
seconds := totalDuration.Seconds()
if seconds == 0 {
return 0
}
bytesPerSecond := float64(totalBytes) / seconds
mbps := (bytesPerSecond * 8) / (1024 * 1024)
return float64(int(mbps*100)) / 100
}
// FormatBandwidth formats bandwidth for display
func FormatBandwidth(mbps float64) string {
if mbps == 0 {
return "Unknown"
}
if mbps >= 1000 {
return fmt.Sprintf("%.1f Gbps", mbps/1000)
}
return fmt.Sprintf("%.1f Mbps", mbps)
}

View File

@@ -4,8 +4,10 @@
package envcheck
import (
"bufio"
"context"
"encoding/json"
"os"
"os/exec"
"runtime"
"strings"
@@ -14,10 +16,26 @@ import (
"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"`
@@ -25,6 +43,9 @@ type RunnerCapabilities struct {
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
@@ -38,10 +59,10 @@ type CapabilityFeatures struct {
// 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(),
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,
@@ -54,6 +75,11 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
},
}
// Detect Linux distribution
if runtime.GOOS == "linux" {
cap.Distro = detectLinuxDistro()
}
// Detect Docker
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
if cap.Docker {
@@ -64,9 +90,80 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
// 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)

View File

@@ -0,0 +1,32 @@
//go:build unix
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package envcheck
import (
"golang.org/x/sys/unix"
)
// detectDiskSpace detects disk space on the root filesystem (Unix version)
func detectDiskSpace() *DiskInfo {
var stat unix.Statfs_t
err := unix.Statfs("/", &stat)
if err != nil {
return nil
}
total := stat.Blocks * uint64(stat.Bsize)
free := stat.Bavail * uint64(stat.Bsize)
used := total - free
usedPercent := float64(used) / float64(total) * 100
return &DiskInfo{
Total: total,
Free: free,
Used: used,
UsedPercent: usedPercent,
}
}

View File

@@ -0,0 +1,31 @@
//go:build windows
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package envcheck
import (
"golang.org/x/sys/windows"
)
// detectDiskSpace detects disk space on the C: drive (Windows version)
func detectDiskSpace() *DiskInfo {
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64
path := windows.StringToUTF16Ptr("C:\\")
err := windows.GetDiskFreeSpaceEx(path, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
if err != nil {
return nil
}
used := totalNumberOfBytes - totalNumberOfFreeBytes
usedPercent := float64(used) / float64(totalNumberOfBytes) * 100
return &DiskInfo{
Total: totalNumberOfBytes,
Free: totalNumberOfFreeBytes,
Used: used,
UsedPercent: usedPercent,
}
}