Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa69213d15 | ||
|
|
f92e50f35b | ||
|
|
a792b47b41 | ||
|
|
68ec7efde0 | ||
|
|
f314ffb036 | ||
|
|
b303a83a77 | ||
|
|
66d0b1e608 | ||
|
|
48a589eb79 | ||
|
|
fef300dd5b | ||
|
|
49a0b6f167 | ||
|
|
e5fdaadbd2 | ||
|
|
ab382dc256 |
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
@@ -26,23 +26,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
env:
|
||||||
|
GOPRIVATE: git.marketally.com
|
||||||
|
VERSION: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
VERSION=${GITHUB_REF_NAME#v}
|
# Strip the v prefix from tag
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
EXT=""
|
EXT=""
|
||||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||||
EXT=".exe"
|
EXT=".exe"
|
||||||
fi
|
fi
|
||||||
|
echo "Building version: ${VERSION}"
|
||||||
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
|
CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
|
||||||
go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=${VERSION}" \
|
go build -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=${VERSION}" \
|
||||||
-o act_runner-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
|
-o act_runner-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
|
||||||
env:
|
|
||||||
GOPRIVATE: git.marketally.com
|
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -52,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
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)
|
||||||
BIN
act_runner-darwin-amd64
Executable file
BIN
act_runner-darwin-amd64
Executable file
Binary file not shown.
BIN
act_runner-darwin-arm64
Executable file
BIN
act_runner-darwin-arm64
Executable file
Binary file not shown.
BIN
act_runner-linux-amd64
Executable file
BIN
act_runner-linux-amd64
Executable file
Binary file not shown.
BIN
act_runner-windows-amd64.exe
Executable file
BIN
act_runner-windows-amd64.exe
Executable file
Binary file not shown.
BIN
act_runner_test
Executable file
BIN
act_runner_test
Executable file
Binary file not shown.
2
go.mod
2
go.mod
@@ -111,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
|
replace github.com/distribution/reference v0.6.0 => github.com/distribution/reference v0.5.0
|
||||||
|
|
||||||
// Use GitCaddy fork with capability support
|
// Use GitCaddy fork with capability support
|
||||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.6
|
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=
|
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 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.6 h1:G7T0vpx8HyCFWd0YMJ9sp8rCsWtzFrCJK4BMdOFJa1A=
|
git.marketally.com/gitcaddy/actions-proto-go v0.5.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
|
||||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.6/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
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 h1:ulcquQluJbmNASkh6ina70LvcHEa9eWYfQ+DeAZ0VEE=
|
||||||
gitea.com/gitea/act v0.261.7-0.20251202193638-5417d3ac6742/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
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=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||||
|
|||||||
@@ -37,8 +37,13 @@ const (
|
|||||||
DiskSpaceCriticalThreshold = 95.0
|
DiskSpaceCriticalThreshold = 95.0
|
||||||
// CapabilitiesUpdateInterval is how often to update capabilities (including disk space)
|
// CapabilitiesUpdateInterval is how often to update capabilities (including disk space)
|
||||||
CapabilitiesUpdateInterval = 5 * time.Minute
|
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 {
|
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
cfg, err := config.LoadDefault(*configFile)
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
@@ -152,7 +157,15 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
dockerHost = dh
|
dockerHost = dh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
capabilities := envcheck.DetectCapabilities(ctx, dockerHost)
|
|
||||||
|
// 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, cfg.Container.WorkdirParent)
|
||||||
|
// Include initial bandwidth result if available
|
||||||
|
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||||
capabilitiesJson := capabilities.ToJSON()
|
capabilitiesJson := capabilities.ToJSON()
|
||||||
log.Infof("detected capabilities: %s", capabilitiesJson)
|
log.Infof("detected capabilities: %s", capabilitiesJson)
|
||||||
|
|
||||||
@@ -173,9 +186,23 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start periodic capabilities update goroutine
|
// Start periodic capabilities update goroutine
|
||||||
go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost)
|
go periodicCapabilitiesUpdate(ctx, runner, ls.Names(), dockerHost, cfg.Container.WorkdirParent)
|
||||||
|
// Start periodic stale job cache cleanup (every hour, remove caches older than 2 hours)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
runner.CleanStaleJobCaches(2 * time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
poller := poll.New(cfg, cli, runner)
|
poller := poll.New(cfg, cli, runner)
|
||||||
|
poller.SetBandwidthManager(bandwidthManager)
|
||||||
|
|
||||||
if daemArgs.Once || reg.Ephemeral {
|
if daemArgs.Once || reg.Ephemeral {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
@@ -225,8 +252,8 @@ func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// periodicCapabilitiesUpdate periodically updates capabilities including disk space
|
// periodicCapabilitiesUpdate periodically updates capabilities including disk space and bandwidth
|
||||||
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string) {
|
func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNames []string, dockerHost string, workingDir string) {
|
||||||
ticker := time.NewTicker(CapabilitiesUpdateInterval)
|
ticker := time.NewTicker(CapabilitiesUpdateInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -234,10 +261,19 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Debug("stopping periodic capabilities update")
|
log.Debug("stopping periodic capabilities update")
|
||||||
|
if bandwidthManager != nil {
|
||||||
|
bandwidthManager.Stop()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Detect updated capabilities (disk space changes over time)
|
// Detect updated capabilities (disk space changes over time)
|
||||||
capabilities := envcheck.DetectCapabilities(ctx, dockerHost)
|
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, workingDir)
|
||||||
|
|
||||||
|
// Include latest bandwidth result
|
||||||
|
if bandwidthManager != nil {
|
||||||
|
capabilities.Bandwidth = bandwidthManager.GetLastResult()
|
||||||
|
}
|
||||||
|
|
||||||
capabilitiesJson := capabilities.ToJSON()
|
capabilitiesJson := capabilities.ToJSON()
|
||||||
|
|
||||||
// Check for disk space warnings
|
// Check for disk space warnings
|
||||||
@@ -248,9 +284,14 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Debug("failed to update capabilities")
|
log.WithError(err).Debug("failed to update capabilities")
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("capabilities updated: disk %.1f%% used, %.2f GB free",
|
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,
|
capabilities.Disk.UsedPercent,
|
||||||
float64(capabilities.Disk.Free)/(1024*1024*1024))
|
float64(capabilities.Disk.Free)/(1024*1024*1024),
|
||||||
|
bandwidthInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Poller struct {
|
type Poller struct {
|
||||||
client client.Client
|
client client.Client
|
||||||
runner *run.Runner
|
runner *run.Runner
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
|
tasksVersion atomic.Int64 // tasksVersion used to store the version of the last task fetched from the Gitea.
|
||||||
|
bandwidthManager *envcheck.BandwidthManager
|
||||||
|
|
||||||
pollingCtx context.Context
|
pollingCtx context.Context
|
||||||
shutdownPolling context.CancelFunc
|
shutdownPolling context.CancelFunc
|
||||||
@@ -58,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() {
|
func (p *Poller) Poll() {
|
||||||
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
@@ -159,7 +165,13 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Detect capabilities including current disk space
|
// Detect capabilities including current disk space
|
||||||
caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost)
|
caps := envcheck.DetectCapabilities(ctx, p.cfg.Container.DockerHost, p.cfg.Container.WorkdirParent)
|
||||||
|
|
||||||
|
// Include latest bandwidth result if available
|
||||||
|
if p.bandwidthManager != nil {
|
||||||
|
caps.Bandwidth = p.bandwidthManager.GetLastResult()
|
||||||
|
}
|
||||||
|
|
||||||
capsJson := caps.ToJSON()
|
capsJson := caps.ToJSON()
|
||||||
|
|
||||||
// Load the version value that was in the cache when the request was sent.
|
// Load the version value that was in the cache when the request was sent.
|
||||||
@@ -181,6 +193,18 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
|
|||||||
return nil, false
|
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 {
|
if resp.Msg.TasksVersion > v {
|
||||||
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -41,6 +42,48 @@ type Runner struct {
|
|||||||
runningTasks sync.Map
|
runningTasks sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getJobCacheDir returns a job-isolated cache directory
|
||||||
|
func (r *Runner) getJobCacheDir(taskID int64) string {
|
||||||
|
return filepath.Join(r.cfg.Host.WorkdirParent, "jobs", fmt.Sprintf("%d", taskID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupJobCache removes the job-specific cache directory after completion
|
||||||
|
func (r *Runner) cleanupJobCache(taskID int64) {
|
||||||
|
jobCacheDir := r.getJobCacheDir(taskID)
|
||||||
|
if err := os.RemoveAll(jobCacheDir); err != nil {
|
||||||
|
log.Warnf("failed to cleanup job cache %s: %v", jobCacheDir, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("cleaned up job cache: %s", jobCacheDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanStaleJobCaches removes job cache directories older than maxAge
|
||||||
|
func (r *Runner) CleanStaleJobCaches(maxAge time.Duration) {
|
||||||
|
jobsDir := filepath.Join(r.cfg.Host.WorkdirParent, "jobs")
|
||||||
|
entries, err := os.ReadDir(jobsDir)
|
||||||
|
if err != nil {
|
||||||
|
return // directory may not exist yet
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoff := time.Now().Add(-maxAge)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.ModTime().Before(cutoff) {
|
||||||
|
jobPath := filepath.Join(jobsDir, entry.Name())
|
||||||
|
if err := os.RemoveAll(jobPath); err != nil {
|
||||||
|
log.Warnf("failed to remove stale job cache %s: %v", jobPath, err)
|
||||||
|
} else {
|
||||||
|
log.Infof("evicted stale job cache: %s", jobPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client) *Runner {
|
||||||
ls := labels.Labels{}
|
ls := labels.Labels{}
|
||||||
for _, v := range reg.Labels {
|
for _, v := range reg.Labels {
|
||||||
@@ -95,6 +138,7 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
|||||||
}
|
}
|
||||||
r.runningTasks.Store(task.Id, struct{}{})
|
r.runningTasks.Store(task.Id, struct{}{})
|
||||||
defer r.runningTasks.Delete(task.Id)
|
defer r.runningTasks.Delete(task.Id)
|
||||||
|
defer r.cleanupJobCache(task.Id)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -197,19 +241,30 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
maxLifetime = time.Until(deadline)
|
maxLifetime = time.Until(deadline)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create job-specific environment with isolated cache directories
|
||||||
|
jobCacheDir := r.getJobCacheDir(task.Id)
|
||||||
|
jobEnvs := make(map[string]string, len(r.envs)+2)
|
||||||
|
for k, v := range r.envs {
|
||||||
|
jobEnvs[k] = v
|
||||||
|
}
|
||||||
|
// Isolate golangci-lint cache to prevent parallel job conflicts
|
||||||
|
jobEnvs["GOLANGCI_LINT_CACHE"] = filepath.Join(jobCacheDir, "golangci-lint")
|
||||||
|
// Set XDG_CACHE_HOME to isolate other tools that respect it
|
||||||
|
jobEnvs["XDG_CACHE_HOME"] = jobCacheDir
|
||||||
|
|
||||||
runnerConfig := &runner.Config{
|
runnerConfig := &runner.Config{
|
||||||
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
|
||||||
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
|
||||||
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
|
Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)),
|
||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
ActionCacheDir: filepath.FromSlash(jobCacheDir),
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
ForcePull: r.cfg.Container.ForcePull,
|
ForcePull: r.cfg.Container.ForcePull,
|
||||||
ForceRebuild: r.cfg.Container.ForceRebuild,
|
ForceRebuild: r.cfg.Container.ForceRebuild,
|
||||||
LogOutput: true,
|
LogOutput: true,
|
||||||
JSONLogger: false,
|
JSONLogger: false,
|
||||||
Env: r.envs,
|
Env: jobEnvs,
|
||||||
Secrets: task.Secrets,
|
Secrets: task.Secrets,
|
||||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: 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,37 +4,61 @@
|
|||||||
package envcheck
|
package envcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DiskInfo holds disk space information
|
// DiskInfo holds disk space information
|
||||||
type DiskInfo struct {
|
type DiskInfo struct {
|
||||||
|
Path string `json:"path,omitempty"` // Path being checked (working directory)
|
||||||
Total uint64 `json:"total_bytes"`
|
Total uint64 `json:"total_bytes"`
|
||||||
Free uint64 `json:"free_bytes"`
|
Free uint64 `json:"free_bytes"`
|
||||||
Used uint64 `json:"used_bytes"`
|
Used uint64 `json:"used_bytes"`
|
||||||
UsedPercent float64 `json:"used_percent"`
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// XcodeInfo holds Xcode and iOS development information
|
||||||
|
type XcodeInfo struct {
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Build string `json:"build,omitempty"`
|
||||||
|
SDKs []string `json:"sdks,omitempty"` // e.g., ["iOS 17.0", "macOS 14.0"]
|
||||||
|
Simulators []string `json:"simulators,omitempty"` // Available iOS simulators
|
||||||
|
}
|
||||||
|
|
||||||
// RunnerCapabilities represents the capabilities of a runner for AI consumption
|
// RunnerCapabilities represents the capabilities of a runner for AI consumption
|
||||||
type RunnerCapabilities struct {
|
type RunnerCapabilities struct {
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
Arch string `json:"arch"`
|
Arch string `json:"arch"`
|
||||||
|
Distro *DistroInfo `json:"distro,omitempty"`
|
||||||
|
Xcode *XcodeInfo `json:"xcode,omitempty"`
|
||||||
Docker bool `json:"docker"`
|
Docker bool `json:"docker"`
|
||||||
DockerCompose bool `json:"docker_compose"`
|
DockerCompose bool `json:"docker_compose"`
|
||||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||||
Shell []string `json:"shell,omitempty"`
|
Shell []string `json:"shell,omitempty"`
|
||||||
Tools map[string][]string `json:"tools,omitempty"`
|
Tools map[string][]string `json:"tools,omitempty"`
|
||||||
|
BuildTools []string `json:"build_tools,omitempty"` // Available build/installer tools
|
||||||
|
PackageManagers []string `json:"package_managers,omitempty"`
|
||||||
Features *CapabilityFeatures `json:"features,omitempty"`
|
Features *CapabilityFeatures `json:"features,omitempty"`
|
||||||
Limitations []string `json:"limitations,omitempty"`
|
Limitations []string `json:"limitations,omitempty"`
|
||||||
Disk *DiskInfo `json:"disk,omitempty"`
|
Disk *DiskInfo `json:"disk,omitempty"`
|
||||||
|
Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"`
|
||||||
|
SuggestedLabels []string `json:"suggested_labels,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CapabilityFeatures represents feature support flags
|
// CapabilityFeatures represents feature support flags
|
||||||
@@ -46,12 +70,15 @@ type CapabilityFeatures struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DetectCapabilities detects the runner's capabilities
|
// DetectCapabilities detects the runner's capabilities
|
||||||
func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilities {
|
// workingDir is the directory where builds will run (for disk space detection)
|
||||||
|
func DetectCapabilities(ctx context.Context, dockerHost string, workingDir string) *RunnerCapabilities {
|
||||||
cap := &RunnerCapabilities{
|
cap := &RunnerCapabilities{
|
||||||
OS: runtime.GOOS,
|
OS: runtime.GOOS,
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Tools: make(map[string][]string),
|
Tools: make(map[string][]string),
|
||||||
Shell: detectShells(),
|
BuildTools: []string{},
|
||||||
|
PackageManagers: []string{},
|
||||||
|
Shell: detectShells(),
|
||||||
Features: &CapabilityFeatures{
|
Features: &CapabilityFeatures{
|
||||||
ArtifactsV4: false, // Gitea doesn't support v4 artifacts
|
ArtifactsV4: false, // Gitea doesn't support v4 artifacts
|
||||||
Cache: true,
|
Cache: true,
|
||||||
@@ -64,6 +91,16 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect Linux distribution
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cap.Distro = detectLinuxDistro()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect macOS Xcode/iOS
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
cap.Xcode = detectXcode(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Detect Docker
|
// Detect Docker
|
||||||
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
|
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
|
||||||
if cap.Docker {
|
if cap.Docker {
|
||||||
@@ -74,38 +111,213 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
|
|||||||
// Detect common tools
|
// Detect common tools
|
||||||
detectTools(ctx, cap)
|
detectTools(ctx, cap)
|
||||||
|
|
||||||
// Detect disk space
|
// Detect build tools
|
||||||
cap.Disk = detectDiskSpace()
|
detectBuildTools(ctx, cap)
|
||||||
|
|
||||||
|
// Detect package managers
|
||||||
|
detectPackageManagers(ctx, cap)
|
||||||
|
|
||||||
|
// Detect disk space on the working directory's filesystem
|
||||||
|
cap.Disk = detectDiskSpace(workingDir)
|
||||||
|
|
||||||
|
// Generate suggested labels based on detected capabilities
|
||||||
|
cap.SuggestedLabels = generateSuggestedLabels(cap)
|
||||||
|
|
||||||
return cap
|
return cap
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectDiskSpace detects disk space on the root filesystem
|
// detectXcode detects Xcode and iOS development capabilities on macOS
|
||||||
func detectDiskSpace() *DiskInfo {
|
func detectXcode(ctx context.Context) *XcodeInfo {
|
||||||
var stat unix.Statfs_t
|
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// Get stats for root filesystem (or current working directory)
|
// Check for xcodebuild
|
||||||
path := "/"
|
cmd := exec.CommandContext(timeoutCtx, "xcodebuild", "-version")
|
||||||
if runtime.GOOS == "windows" {
|
output, err := cmd.Output()
|
||||||
path = "C:\\"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := unix.Statfs(path, &stat)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
total := stat.Blocks * uint64(stat.Bsize)
|
xcode := &XcodeInfo{}
|
||||||
free := stat.Bavail * uint64(stat.Bsize)
|
lines := strings.Split(string(output), "\n")
|
||||||
used := total - free
|
for _, line := range lines {
|
||||||
usedPercent := float64(used) / float64(total) * 100
|
if strings.HasPrefix(line, "Xcode ") {
|
||||||
|
xcode.Version = strings.TrimPrefix(line, "Xcode ")
|
||||||
return &DiskInfo{
|
} else if strings.HasPrefix(line, "Build version ") {
|
||||||
Total: total,
|
xcode.Build = strings.TrimPrefix(line, "Build version ")
|
||||||
Free: free,
|
}
|
||||||
Used: used,
|
|
||||||
UsedPercent: usedPercent,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get available SDKs
|
||||||
|
cmd = exec.CommandContext(timeoutCtx, "xcodebuild", "-showsdks")
|
||||||
|
output, err = cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
lines = strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// Look for SDK lines like "-sdk iphoneos17.0" or "iOS 17.0"
|
||||||
|
if strings.Contains(line, "SDK") || strings.HasPrefix(line, "-sdk") {
|
||||||
|
continue // Skip header lines
|
||||||
|
}
|
||||||
|
if strings.Contains(line, "iOS") || strings.Contains(line, "macOS") ||
|
||||||
|
strings.Contains(line, "watchOS") || strings.Contains(line, "tvOS") ||
|
||||||
|
strings.Contains(line, "visionOS") || strings.Contains(line, "xrOS") {
|
||||||
|
// Extract SDK name
|
||||||
|
if idx := strings.Index(line, "-sdk"); idx != -1 {
|
||||||
|
sdkPart := strings.TrimSpace(line[:idx])
|
||||||
|
if sdkPart != "" {
|
||||||
|
xcode.SDKs = append(xcode.SDKs, sdkPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available simulators
|
||||||
|
cmd = exec.CommandContext(timeoutCtx, "xcrun", "simctl", "list", "devices", "available", "-j")
|
||||||
|
output, err = cmd.Output()
|
||||||
|
if err == nil {
|
||||||
|
var simData struct {
|
||||||
|
Devices map[string][]struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
State string `json:"state"`
|
||||||
|
} `json:"devices"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(output, &simData) == nil {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for runtime, devices := range simData.Devices {
|
||||||
|
if strings.Contains(runtime, "iOS") {
|
||||||
|
for _, dev := range devices {
|
||||||
|
key := dev.Name
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
xcode.Simulators = append(xcode.Simulators, dev.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if xcode.Version == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return xcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xcode/iOS labels (macOS only)
|
||||||
|
if cap.Xcode != nil {
|
||||||
|
addLabel("xcode")
|
||||||
|
// Check for SDKs
|
||||||
|
for _, sdk := range cap.Xcode.SDKs {
|
||||||
|
sdkLower := strings.ToLower(sdk)
|
||||||
|
if strings.Contains(sdkLower, "ios") {
|
||||||
|
addLabel("ios")
|
||||||
|
}
|
||||||
|
if strings.Contains(sdkLower, "visionos") || strings.Contains(sdkLower, "xros") {
|
||||||
|
addLabel("visionos")
|
||||||
|
}
|
||||||
|
if strings.Contains(sdkLower, "watchos") {
|
||||||
|
addLabel("watchos")
|
||||||
|
}
|
||||||
|
if strings.Contains(sdkLower, "tvos") {
|
||||||
|
addLabel("tvos")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If simulators available, add simulator label
|
||||||
|
if len(cap.Xcode.Simulators) > 0 {
|
||||||
|
addLabel("ios-simulator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool-based labels
|
||||||
|
if _, ok := cap.Tools["dotnet"]; ok {
|
||||||
|
addLabel("dotnet")
|
||||||
|
}
|
||||||
|
if _, ok := cap.Tools["java"]; ok {
|
||||||
|
addLabel("java")
|
||||||
|
}
|
||||||
|
if _, ok := cap.Tools["node"]; ok {
|
||||||
|
addLabel("node")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tool labels
|
||||||
|
for _, tool := range cap.BuildTools {
|
||||||
|
switch tool {
|
||||||
|
case "msbuild":
|
||||||
|
addLabel("msbuild")
|
||||||
|
case "visual-studio":
|
||||||
|
addLabel("vs2022") // or detect actual version
|
||||||
|
case "inno-setup":
|
||||||
|
addLabel("inno-setup")
|
||||||
|
case "nsis":
|
||||||
|
addLabel("nsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON converts capabilities to JSON string for transmission
|
// ToJSON converts capabilities to JSON string for transmission
|
||||||
@@ -195,12 +407,19 @@ func detectDockerCompose(ctx context.Context) bool {
|
|||||||
|
|
||||||
func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
toolDetectors := map[string]func(context.Context) []string{
|
toolDetectors := map[string]func(context.Context) []string{
|
||||||
"node": detectNodeVersions,
|
"node": detectNodeVersions,
|
||||||
"go": detectGoVersions,
|
"go": detectGoVersions,
|
||||||
"python": detectPythonVersions,
|
"python": detectPythonVersions,
|
||||||
"java": detectJavaVersions,
|
"java": detectJavaVersions,
|
||||||
"dotnet": detectDotnetVersions,
|
"dotnet": detectDotnetVersions,
|
||||||
"rust": detectRustVersions,
|
"rust": detectRustVersions,
|
||||||
|
"ruby": detectRubyVersions,
|
||||||
|
"php": detectPHPVersions,
|
||||||
|
"swift": detectSwiftVersions,
|
||||||
|
"kotlin": detectKotlinVersions,
|
||||||
|
"flutter": detectFlutterVersions,
|
||||||
|
"dart": detectDartVersions,
|
||||||
|
"powershell": detectPowerShellVersions,
|
||||||
}
|
}
|
||||||
|
|
||||||
for tool, detector := range toolDetectors {
|
for tool, detector := range toolDetectors {
|
||||||
@@ -208,6 +427,242 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
|
|||||||
cap.Tools[tool] = versions
|
cap.Tools[tool] = versions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect additional tools that just need presence check
|
||||||
|
simpleTools := map[string]string{
|
||||||
|
"git": "git",
|
||||||
|
"cmake": "cmake",
|
||||||
|
"make": "make",
|
||||||
|
"ninja": "ninja",
|
||||||
|
"gradle": "gradle",
|
||||||
|
"maven": "mvn",
|
||||||
|
"npm": "npm",
|
||||||
|
"yarn": "yarn",
|
||||||
|
"pnpm": "pnpm",
|
||||||
|
"cargo": "cargo",
|
||||||
|
"pip": "pip3",
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, cmd := range simpleTools {
|
||||||
|
if v := detectSimpleToolVersion(ctx, cmd); v != "" {
|
||||||
|
cap.Tools[name] = []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
detectWindowsBuildTools(ctx, cap)
|
||||||
|
case "darwin":
|
||||||
|
detectMacOSBuildTools(ctx, cap)
|
||||||
|
case "linux":
|
||||||
|
detectLinuxBuildTools(ctx, cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectWindowsBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
|
// Check for Visual Studio via vswhere
|
||||||
|
vswherePaths := []string{
|
||||||
|
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`,
|
||||||
|
`C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe`,
|
||||||
|
}
|
||||||
|
for _, vswhere := range vswherePaths {
|
||||||
|
if _, err := os.Stat(vswhere); err == nil {
|
||||||
|
cmd := exec.CommandContext(ctx, vswhere, "-latest", "-property", "displayName")
|
||||||
|
if output, err := cmd.Output(); err == nil && len(output) > 0 {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "visual-studio")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for MSBuild
|
||||||
|
msbuildPaths := []string{
|
||||||
|
`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
`C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
`C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
`C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe`,
|
||||||
|
}
|
||||||
|
for _, msbuild := range msbuildPaths {
|
||||||
|
if _, err := os.Stat(msbuild); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "msbuild")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Inno Setup
|
||||||
|
innoSetupPaths := []string{
|
||||||
|
`C:\Program Files (x86)\Inno Setup 6\ISCC.exe`,
|
||||||
|
`C:\Program Files\Inno Setup 6\ISCC.exe`,
|
||||||
|
`C:\Program Files (x86)\Inno Setup 5\ISCC.exe`,
|
||||||
|
`C:\Program Files\Inno Setup 5\ISCC.exe`,
|
||||||
|
}
|
||||||
|
for _, iscc := range innoSetupPaths {
|
||||||
|
if _, err := os.Stat(iscc); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also check PATH
|
||||||
|
if _, err := exec.LookPath("iscc"); err == nil {
|
||||||
|
if !contains(cap.BuildTools, "inno-setup") {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "inno-setup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for NSIS
|
||||||
|
nsisPaths := []string{
|
||||||
|
`C:\Program Files (x86)\NSIS\makensis.exe`,
|
||||||
|
`C:\Program Files\NSIS\makensis.exe`,
|
||||||
|
}
|
||||||
|
for _, nsis := range nsisPaths {
|
||||||
|
if _, err := os.Stat(nsis); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("makensis"); err == nil {
|
||||||
|
if !contains(cap.BuildTools, "nsis") {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "nsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for WiX Toolset
|
||||||
|
wixPaths := []string{
|
||||||
|
`C:\Program Files (x86)\WiX Toolset v3.11\bin\candle.exe`,
|
||||||
|
`C:\Program Files (x86)\WiX Toolset v3.14\bin\candle.exe`,
|
||||||
|
}
|
||||||
|
for _, wix := range wixPaths {
|
||||||
|
if _, err := os.Stat(wix); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "wix")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for signtool (Windows SDK)
|
||||||
|
signtoolPaths, _ := filepath.Glob(`C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe`)
|
||||||
|
if len(signtoolPaths) > 0 {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "signtool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectMacOSBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
|
// Check for xcpretty
|
||||||
|
if _, err := exec.LookPath("xcpretty"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "xcpretty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for fastlane
|
||||||
|
if _, err := exec.LookPath("fastlane"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "fastlane")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for CocoaPods
|
||||||
|
if _, err := exec.LookPath("pod"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "cocoapods")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Carthage
|
||||||
|
if _, err := exec.LookPath("carthage"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "carthage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for SwiftLint
|
||||||
|
if _, err := exec.LookPath("swiftlint"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "swiftlint")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for create-dmg or similar
|
||||||
|
if _, err := exec.LookPath("create-dmg"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "create-dmg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Packages (packagesbuild)
|
||||||
|
if _, err := exec.LookPath("packagesbuild"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "packages")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pkgbuild (built-in)
|
||||||
|
if _, err := exec.LookPath("pkgbuild"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "pkgbuild")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for codesign (built-in)
|
||||||
|
if _, err := exec.LookPath("codesign"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "codesign")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for notarytool (built-in with Xcode)
|
||||||
|
if _, err := exec.LookPath("notarytool"); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, "notarytool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectLinuxBuildTools(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
|
// Check for common Linux build tools
|
||||||
|
tools := []string{
|
||||||
|
"gcc", "g++", "clang", "clang++",
|
||||||
|
"autoconf", "automake", "libtool",
|
||||||
|
"pkg-config", "meson",
|
||||||
|
"dpkg-deb", "rpmbuild", "fpm",
|
||||||
|
"appimage-builder", "linuxdeploy",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
|
if _, err := exec.LookPath(tool); err == nil {
|
||||||
|
cap.BuildTools = append(cap.BuildTools, tool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPackageManagers(ctx context.Context, cap *RunnerCapabilities) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
if _, err := exec.LookPath("choco"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "chocolatey")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("scoop"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "scoop")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("winget"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "winget")
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
if _, err := exec.LookPath("brew"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "homebrew")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("port"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "macports")
|
||||||
|
}
|
||||||
|
case "linux":
|
||||||
|
if _, err := exec.LookPath("apt"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "apt")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("yum"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "yum")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("dnf"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "dnf")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("pacman"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "pacman")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("zypper"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "zypper")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("apk"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "apk")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("snap"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "snap")
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("flatpak"); err == nil {
|
||||||
|
cap.PackageManagers = append(cap.PackageManagers, "flatpak")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNodeVersions(ctx context.Context) []string {
|
func detectNodeVersions(ctx context.Context) []string {
|
||||||
@@ -228,16 +683,8 @@ func detectPythonVersions(ctx context.Context) []string {
|
|||||||
|
|
||||||
// Also try python
|
// Also try python
|
||||||
if v := detectToolVersion(ctx, "python", "--version", "Python "); len(v) > 0 {
|
if v := detectToolVersion(ctx, "python", "--version", "Python "); len(v) > 0 {
|
||||||
// Avoid duplicates
|
|
||||||
for _, ver := range v {
|
for _, ver := range v {
|
||||||
found := false
|
if !contains(versions, ver) {
|
||||||
for _, existing := range versions {
|
|
||||||
if existing == ver {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
versions = append(versions, ver)
|
versions = append(versions, ver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,20 +700,17 @@ func detectJavaVersions(ctx context.Context) []string {
|
|||||||
return 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")
|
lines := strings.Split(string(output), "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.Contains(line, "version") {
|
if strings.Contains(line, "version") {
|
||||||
// Extract version from quotes
|
|
||||||
start := strings.Index(line, "\"")
|
start := strings.Index(line, "\"")
|
||||||
end := strings.LastIndex(line, "\"")
|
end := strings.LastIndex(line, "\"")
|
||||||
if start != -1 && end > start {
|
if start != -1 && end > start {
|
||||||
version := line[start+1 : end]
|
version := line[start+1 : end]
|
||||||
// Simplify version (e.g., "17.0.1" -> "17")
|
|
||||||
parts := strings.Split(version, ".")
|
parts := strings.Split(version, ".")
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
if parts[0] == "1" && len(parts) > 1 {
|
if parts[0] == "1" && len(parts) > 1 {
|
||||||
return []string{parts[1]} // Java 8 style: 1.8 -> 8
|
return []string{parts[1]}
|
||||||
}
|
}
|
||||||
return []string{parts[0]}
|
return []string{parts[0]}
|
||||||
}
|
}
|
||||||
@@ -291,21 +735,11 @@ func detectDotnetVersions(ctx context.Context) []string {
|
|||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Format: "8.0.100 [/path/to/sdk]"
|
|
||||||
parts := strings.Split(line, " ")
|
parts := strings.Split(line, " ")
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
version := parts[0]
|
version := parts[0]
|
||||||
// Simplify to major version
|
|
||||||
major := strings.Split(version, ".")[0]
|
major := strings.Split(version, ".")[0]
|
||||||
// Avoid duplicates
|
if !contains(versions, major) {
|
||||||
found := false
|
|
||||||
for _, v := range versions {
|
|
||||||
if v == major {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
versions = append(versions, major)
|
versions = append(versions, major)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,6 +752,102 @@ func detectRustVersions(ctx context.Context) []string {
|
|||||||
return detectToolVersion(ctx, "rustc", "--version", "rustc ")
|
return detectToolVersion(ctx, "rustc", "--version", "rustc ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func detectRubyVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "ruby", "--version", "ruby ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPHPVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "php", "--version", "PHP ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectSwiftVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "swift", "--version", "Swift version ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectKotlinVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "kotlin", "-version", "Kotlin version ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectFlutterVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "flutter", "--version", "Flutter ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectDartVersions(ctx context.Context) []string {
|
||||||
|
return detectToolVersion(ctx, "dart", "--version", "Dart SDK version: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPowerShellVersions(ctx context.Context) []string {
|
||||||
|
versions := []string{}
|
||||||
|
|
||||||
|
// Check for pwsh (PowerShell Core / PowerShell 7+)
|
||||||
|
if v := detectPwshVersion(ctx, "pwsh"); v != "" {
|
||||||
|
versions = append(versions, "pwsh:"+v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for powershell (Windows PowerShell 5.x)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if v := detectPwshVersion(ctx, "powershell"); v != "" {
|
||||||
|
versions = append(versions, "powershell:"+v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPwshVersion(ctx context.Context, cmd string) string {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Use -Command to get version
|
||||||
|
var c *exec.Cmd
|
||||||
|
if cmd == "pwsh" {
|
||||||
|
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
|
||||||
|
} else {
|
||||||
|
c = exec.CommandContext(timeoutCtx, cmd, "-Command", "$PSVersionTable.PSVersion.ToString()")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := c.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
version := strings.TrimSpace(string(output))
|
||||||
|
// Return major.minor
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
return parts[0] + "." + parts[1]
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectSimpleToolVersion(ctx context.Context, cmd string) string {
|
||||||
|
if _, err := exec.LookPath(cmd); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c := exec.CommandContext(timeoutCtx, cmd, "--version")
|
||||||
|
output, err := c.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Try without --version for tools that don't support it
|
||||||
|
return "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
line := strings.TrimSpace(strings.Split(string(output), "\n")[0])
|
||||||
|
// Extract version number if possible
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
for _, part := range parts {
|
||||||
|
// Look for something that looks like a version
|
||||||
|
if len(part) > 0 && (part[0] >= '0' && part[0] <= '9' || part[0] == 'v') {
|
||||||
|
return strings.TrimPrefix(part, "v")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "installed"
|
||||||
|
}
|
||||||
|
|
||||||
func detectToolVersion(ctx context.Context, cmd string, args string, prefix string) []string {
|
func detectToolVersion(ctx context.Context, cmd string, args string, prefix string) []string {
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -335,13 +865,10 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get just the version number
|
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
version := parts[0]
|
version := parts[0]
|
||||||
// Clean up version string
|
|
||||||
version = strings.TrimPrefix(version, "v")
|
version = strings.TrimPrefix(version, "v")
|
||||||
// Return major.minor or just major
|
|
||||||
vparts := strings.Split(version, ".")
|
vparts := strings.Split(version, ".")
|
||||||
if len(vparts) >= 2 {
|
if len(vparts) >= 2 {
|
||||||
return []string{vparts[0] + "." + vparts[1]}
|
return []string{vparts[0] + "." + vparts[1]}
|
||||||
@@ -351,3 +878,12 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contains(slice []string, item string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
43
internal/pkg/envcheck/disk_unix.go
Normal file
43
internal/pkg/envcheck/disk_unix.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//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 specified path's filesystem (Unix version)
|
||||||
|
// If path is empty, defaults to "/"
|
||||||
|
func detectDiskSpace(path string) *DiskInfo {
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var stat unix.Statfs_t
|
||||||
|
|
||||||
|
err := unix.Statfs(path, &stat)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to root if the path doesn't exist
|
||||||
|
err = unix.Statfs("/", &stat)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
total := stat.Blocks * uint64(stat.Bsize)
|
||||||
|
free := stat.Bavail * uint64(stat.Bsize)
|
||||||
|
used := total - free
|
||||||
|
usedPercent := float64(used) / float64(total) * 100
|
||||||
|
|
||||||
|
return &DiskInfo{
|
||||||
|
Path: path,
|
||||||
|
Total: total,
|
||||||
|
Free: free,
|
||||||
|
Used: used,
|
||||||
|
UsedPercent: usedPercent,
|
||||||
|
}
|
||||||
|
}
|
||||||
57
internal/pkg/envcheck/disk_windows.go
Normal file
57
internal/pkg/envcheck/disk_windows.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package envcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// detectDiskSpace detects disk space on the specified path's drive (Windows version)
|
||||||
|
// If path is empty, defaults to "C:\"
|
||||||
|
func detectDiskSpace(path string) *DiskInfo {
|
||||||
|
if path == "" {
|
||||||
|
path = "C:\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve to absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
absPath = "C:\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract drive letter (e.g., "D:\" from "D:\builds\runner")
|
||||||
|
drivePath := filepath.VolumeName(absPath) + "\\"
|
||||||
|
if drivePath == "\\" {
|
||||||
|
drivePath = "C:\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64
|
||||||
|
|
||||||
|
pathPtr := windows.StringToUTF16Ptr(drivePath)
|
||||||
|
err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to C: drive
|
||||||
|
pathPtr = windows.StringToUTF16Ptr("C:\\")
|
||||||
|
err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
drivePath = "C:\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
used := totalNumberOfBytes - totalNumberOfFreeBytes
|
||||||
|
usedPercent := float64(used) / float64(totalNumberOfBytes) * 100
|
||||||
|
|
||||||
|
return &DiskInfo{
|
||||||
|
Path: drivePath,
|
||||||
|
Total: totalNumberOfBytes,
|
||||||
|
Free: totalNumberOfFreeBytes,
|
||||||
|
Used: used,
|
||||||
|
UsedPercent: usedPercent,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user