Compare commits
8 Commits
v0.3.3-gitcaddy
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48a589eb79 | ||
|
|
fef300dd5b | ||
|
|
49a0b6f167 | ||
|
|
e5fdaadbd2 | ||
|
|
ab382dc256 | ||
|
|
2c66de4df2 | ||
|
|
9de33d4252 | ||
|
|
ff8375c6e1 |
121
HOWTOSTART.md
Normal file
121
HOWTOSTART.md
Normal 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
5
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
209
internal/pkg/envcheck/bandwidth.go
Normal file
209
internal/pkg/envcheck/bandwidth.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
32
internal/pkg/envcheck/disk_unix.go
Normal file
32
internal/pkg/envcheck/disk_unix.go
Normal 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,
|
||||
}
|
||||
}
|
||||
31
internal/pkg/envcheck/disk_windows.go
Normal file
31
internal/pkg/envcheck/disk_windows.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user