10 Commits

Author SHA1 Message Date
GitCaddy
56dcda0d5e fix: remove binaries from git tracking
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Successful in 1m22s
Release / build (arm64, darwin) (push) Successful in 2m9s
Release / build (amd64, linux) (push) Successful in 2m19s
Release / build (amd64, windows) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 1m9s
Release / release (push) Successful in 21s
2026-01-12 01:36:19 +00:00
GitCaddy
e44f0c403b fix: remove accidentally committed binaries and add to gitignore
Some checks are pending
CI / build-and-test (push) Waiting to run
2026-01-12 01:35:38 +00:00
GitCaddy
fb1498bf7a fix: add -a flag to force rebuild and prevent cached binaries
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Successful in 1m14s
Release / build (amd64, windows) (push) Successful in 1m38s
Release / build (amd64, linux) (push) Successful in 2m52s
Release / build (arm64, darwin) (push) Successful in 2m50s
Release / build (arm64, linux) (push) Successful in 1m48s
Release / release (push) Successful in 47s
2026-01-12 01:28:20 +00:00
GitCaddy
fa69213d15 fix: use GitHub Actions expression syntax for VERSION
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, linux) (push) Successful in 45s
Release / build (amd64, windows) (push) Successful in 54s
Release / build (amd64, darwin) (push) Successful in 1m8s
Release / build (arm64, darwin) (push) Successful in 1m6s
Release / build (arm64, linux) (push) Successful in 42s
Release / release (push) Successful in 25s
2026-01-12 01:22:54 +00:00
GitCaddy
f92e50f35b fix: use GITHUB_REF instead of GITHUB_REF_NAME for version extraction
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Successful in 47s
Release / build (amd64, linux) (push) Successful in 55s
Release / build (arm64, darwin) (push) Successful in 1m7s
Release / build (amd64, windows) (push) Successful in 1m10s
Release / build (arm64, linux) (push) Successful in 51s
Release / release (push) Successful in 26s
2026-01-12 01:14:20 +00:00
GitCaddy
a792b47b41 fix: isolate golangci-lint cache per job to prevent parallel conflicts
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Successful in 56s
Release / build (amd64, linux) (push) Successful in 1m0s
Release / build (amd64, windows) (push) Successful in 1m9s
Release / build (arm64, linux) (push) Successful in 38s
Release / build (arm64, darwin) (push) Successful in 55s
Release / release (push) Successful in 27s
Add GOLANGCI_LINT_CACHE and XDG_CACHE_HOME environment variables
pointing to job-specific cache directory to prevent parallel job
conflicts when running golangci-lint.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 00:47:11 +00:00
GitCaddy
68ec7efde0 fix: isolate golangci-lint cache per job to prevent parallel conflicts
Add GOLANGCI_LINT_CACHE and XDG_CACHE_HOME environment variables
pointing to job-specific cache directory to prevent parallel job
conflicts when running golangci-lint.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 00:45:26 +00:00
GitCaddy
f314ffb036 feat: implement job-isolated cache directories
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Successful in 17s
Release / build (amd64, linux) (push) Successful in 21s
Release / build (amd64, windows) (push) Successful in 17s
Release / build (arm64, darwin) (push) Successful in 16s
Release / build (arm64, linux) (push) Successful in 43s
Release / release (push) Successful in 29s
- Each job now gets its own cache directory: ~/.cache/act/jobs/{taskId}/
- Cache is cleaned up automatically after job completion
- Periodic cleanup removes stale job caches older than 2 hours
- Eliminates race conditions in npm/pnpm cache operations
- No more ENOTEMPTY errors from concurrent tool installs
- Fix workflow: use linux-latest and setup-go@v4
2026-01-11 22:25:24 +00:00
GitCaddy
b303a83a77 feat(capabilities): add visionOS SDK, PowerShell versions, working directory disk space
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, darwin) (push) Waiting to run
Release / build (amd64, linux) (push) Waiting to run
Release / build (amd64, windows) (push) Waiting to run
Release / build (arm64, darwin) (push) Waiting to run
Release / build (arm64, linux) (push) Waiting to run
Release / release (push) Blocked by required conditions
- Add visionOS/xrOS SDK detection for Vision Pro development
- Add PowerShell version detection (pwsh and powershell) with actual versions
- Detect disk space on working directory filesystem (not just root)
  - Useful for runners using external/USB drives for builds
- Add watchOS and tvOS suggested labels
- Refactor disk detection to accept path parameter

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:32:23 +00:00
GitCaddy
66d0b1e608 feat(capabilities): enhanced tool and platform detection
Some checks are pending
CI / build-and-test (push) Waiting to run
macOS:
- Xcode version and build detection
- iOS/watchOS/tvOS SDK detection
- iOS Simulator detection
- Swift, CocoaPods, Carthage, fastlane detection
- Code signing tools (codesign, notarytool)
- Package builders (pkgbuild, create-dmg)

Windows:
- Visual Studio detection via vswhere
- MSBuild detection
- Inno Setup (ISCC) detection
- NSIS (makensis) detection
- WiX Toolset detection
- Windows SDK signtool detection
- Package managers (Chocolatey, Scoop, winget)

Linux:
- GCC/Clang compiler detection
- Build tools (autoconf, automake, meson)
- Package builders (dpkg-deb, rpmbuild, fpm)
- AppImage tools detection

Cross-platform:
- Ruby, PHP, Swift, Kotlin, Flutter, Dart
- CMake, Make, Ninja, Gradle, Maven
- npm, yarn, pnpm, cargo, pip
- Git version detection

Suggested labels now include:
- xcode, ios, ios-simulator for macOS with Xcode
- inno-setup, nsis, msbuild, vs2022 for Windows
- Tool-based labels (dotnet, java, node)

🤖 Generated with Claude Code
2026-01-11 20:20:02 +00:00
9 changed files with 653 additions and 64 deletions

View File

@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: linux-latest
strategy:
matrix:
include:
@@ -26,23 +26,26 @@ jobs:
with:
fetch-depth: 0
- uses: actions/setup-go@v5
- uses: actions/setup-go@v4
with:
go-version-file: 'go.mod'
cache: false
- name: Build
env:
GOPRIVATE: git.marketally.com
VERSION: ${{ github.ref_name }}
run: |
VERSION=${GITHUB_REF_NAME#v}
# Strip the v prefix from tag
VERSION="${VERSION#v}"
EXT=""
if [ "${{ matrix.goos }}" = "windows" ]; then
EXT=".exe"
fi
echo "Building version: ${VERSION}"
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 -a -ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=${VERSION}" \
-o act_runner-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}
env:
GOPRIVATE: git.marketally.com
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -52,7 +55,7 @@ jobs:
release:
needs: build
runs-on: ubuntu-latest
runs-on: linux-latest
steps:
- uses: actions/checkout@v4

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ coverage.txt
__debug_bin
# gorelease binary folder
dist
act_runner-*

BIN
act_runner_test Executable file
View File

Binary file not shown.

View File

@@ -163,7 +163,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
bandwidthManager.Start(ctx)
log.Infof("bandwidth manager started, testing against: %s", reg.Address)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost)
capabilities := envcheck.DetectCapabilities(ctx, dockerHost, cfg.Container.WorkdirParent)
// Include initial bandwidth result if available
capabilities.Bandwidth = bandwidthManager.GetLastResult()
capabilitiesJson := capabilities.ToJSON()
@@ -186,7 +186,20 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
// 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.SetBandwidthManager(bandwidthManager)
@@ -240,7 +253,7 @@ func checkDiskSpaceWarnings(capabilities *envcheck.RunnerCapabilities) {
}
// 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)
defer ticker.Stop()
@@ -254,7 +267,7 @@ func periodicCapabilitiesUpdate(ctx context.Context, runner *run.Runner, labelNa
return
case <-ticker.C:
// 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 {

View File

@@ -165,7 +165,7 @@ func (p *Poller) fetchTask(ctx context.Context) (*runnerv1.Task, bool) {
defer cancel()
// 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 {

View File

@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
@@ -41,6 +42,48 @@ type Runner struct {
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 {
ls := labels.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{}{})
defer r.runningTasks.Delete(task.Id)
defer r.cleanupJobCache(task.Id)
ctx, cancel := context.WithTimeout(ctx, r.cfg.Runner.Timeout)
defer cancel()
@@ -197,19 +241,30 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
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{
// On Linux, 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)),
BindWorkdir: false,
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
ActionCacheDir: filepath.FromSlash(jobCacheDir),
ReuseContainers: false,
ForcePull: r.cfg.Container.ForcePull,
ForceRebuild: r.cfg.Container.ForceRebuild,
LogOutput: true,
JSONLogger: false,
Env: r.envs,
Env: jobEnvs,
Secrets: task.Secrets,
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
AutoRemove: true,

View File

@@ -9,6 +9,7 @@ import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@@ -18,6 +19,7 @@ import (
// DiskInfo holds disk space information
type DiskInfo struct {
Path string `json:"path,omitempty"` // Path being checked (working directory)
Total uint64 `json:"total_bytes"`
Free uint64 `json:"free_bytes"`
Used uint64 `json:"used_bytes"`
@@ -31,16 +33,27 @@ type DistroInfo struct {
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
type RunnerCapabilities struct {
OS string `json:"os"`
Arch string `json:"arch"`
Distro *DistroInfo `json:"distro,omitempty"`
Xcode *XcodeInfo `json:"xcode,omitempty"`
Docker bool `json:"docker"`
DockerCompose bool `json:"docker_compose"`
ContainerRuntime string `json:"container_runtime,omitempty"`
Shell []string `json:"shell,omitempty"`
Tools map[string][]string `json:"tools,omitempty"`
BuildTools []string `json:"build_tools,omitempty"` // Available build/installer tools
PackageManagers []string `json:"package_managers,omitempty"`
Features *CapabilityFeatures `json:"features,omitempty"`
Limitations []string `json:"limitations,omitempty"`
Disk *DiskInfo `json:"disk,omitempty"`
@@ -57,12 +70,15 @@ type CapabilityFeatures struct {
}
// 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{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Tools: make(map[string][]string),
Shell: detectShells(),
OS: runtime.GOOS,
Arch: runtime.GOARCH,
Tools: make(map[string][]string),
BuildTools: []string{},
PackageManagers: []string{},
Shell: detectShells(),
Features: &CapabilityFeatures{
ArtifactsV4: false, // Gitea doesn't support v4 artifacts
Cache: true,
@@ -80,6 +96,11 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
cap.Distro = detectLinuxDistro()
}
// Detect macOS Xcode/iOS
if runtime.GOOS == "darwin" {
cap.Xcode = detectXcode(ctx)
}
// Detect Docker
cap.Docker, cap.ContainerRuntime = detectDocker(ctx, dockerHost)
if cap.Docker {
@@ -90,8 +111,14 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
// Detect common tools
detectTools(ctx, cap)
// Detect disk space
cap.Disk = detectDiskSpace()
// Detect build tools
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)
@@ -99,6 +126,86 @@ func DetectCapabilities(ctx context.Context, dockerHost string) *RunnerCapabilit
return cap
}
// detectXcode detects Xcode and iOS development capabilities on macOS
func detectXcode(ctx context.Context) *XcodeInfo {
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Check for xcodebuild
cmd := exec.CommandContext(timeoutCtx, "xcodebuild", "-version")
output, err := cmd.Output()
if err != nil {
return nil
}
xcode := &XcodeInfo{}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "Xcode ") {
xcode.Version = strings.TrimPrefix(line, "Xcode ")
} else if strings.HasPrefix(line, "Build version ") {
xcode.Build = strings.TrimPrefix(line, "Build version ")
}
}
// 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")
@@ -160,10 +267,59 @@ func generateSuggestedLabels(cap *RunnerCapabilities) []string {
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
func (c *RunnerCapabilities) ToJSON() string {
data, err := json.Marshal(c)
@@ -251,12 +407,19 @@ func detectDockerCompose(ctx context.Context) bool {
func detectTools(ctx context.Context, cap *RunnerCapabilities) {
toolDetectors := map[string]func(context.Context) []string{
"node": detectNodeVersions,
"go": detectGoVersions,
"python": detectPythonVersions,
"java": detectJavaVersions,
"dotnet": detectDotnetVersions,
"rust": detectRustVersions,
"node": detectNodeVersions,
"go": detectGoVersions,
"python": detectPythonVersions,
"java": detectJavaVersions,
"dotnet": detectDotnetVersions,
"rust": detectRustVersions,
"ruby": detectRubyVersions,
"php": detectPHPVersions,
"swift": detectSwiftVersions,
"kotlin": detectKotlinVersions,
"flutter": detectFlutterVersions,
"dart": detectDartVersions,
"powershell": detectPowerShellVersions,
}
for tool, detector := range toolDetectors {
@@ -264,6 +427,242 @@ func detectTools(ctx context.Context, cap *RunnerCapabilities) {
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 {
@@ -284,16 +683,8 @@ func detectPythonVersions(ctx context.Context) []string {
// Also try python
if v := detectToolVersion(ctx, "python", "--version", "Python "); len(v) > 0 {
// Avoid duplicates
for _, ver := range v {
found := false
for _, existing := range versions {
if existing == ver {
found = true
break
}
}
if !found {
if !contains(versions, ver) {
versions = append(versions, ver)
}
}
@@ -309,20 +700,17 @@ func detectJavaVersions(ctx context.Context) []string {
return nil
}
// Java version output goes to stderr and looks like: openjdk version "17.0.1" or java version "1.8.0_301"
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.Contains(line, "version") {
// Extract version from quotes
start := strings.Index(line, "\"")
end := strings.LastIndex(line, "\"")
if start != -1 && end > start {
version := line[start+1 : end]
// Simplify version (e.g., "17.0.1" -> "17")
parts := strings.Split(version, ".")
if len(parts) > 0 {
if parts[0] == "1" && len(parts) > 1 {
return []string{parts[1]} // Java 8 style: 1.8 -> 8
return []string{parts[1]}
}
return []string{parts[0]}
}
@@ -347,21 +735,11 @@ func detectDotnetVersions(ctx context.Context) []string {
if line == "" {
continue
}
// Format: "8.0.100 [/path/to/sdk]"
parts := strings.Split(line, " ")
if len(parts) > 0 {
version := parts[0]
// Simplify to major version
major := strings.Split(version, ".")[0]
// Avoid duplicates
found := false
for _, v := range versions {
if v == major {
found = true
break
}
}
if !found {
if !contains(versions, major) {
versions = append(versions, major)
}
}
@@ -374,6 +752,102 @@ func detectRustVersions(ctx context.Context) []string {
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 {
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
@@ -391,13 +865,10 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
}
}
// Get just the version number
parts := strings.Fields(line)
if len(parts) > 0 {
version := parts[0]
// Clean up version string
version = strings.TrimPrefix(version, "v")
// Return major.minor or just major
vparts := strings.Split(version, ".")
if len(vparts) >= 2 {
return []string{vparts[0] + "." + vparts[1]}
@@ -407,3 +878,12 @@ func detectToolVersion(ctx context.Context, cmd string, args string, prefix stri
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}

View File

@@ -9,13 +9,23 @@ import (
"golang.org/x/sys/unix"
)
// detectDiskSpace detects disk space on the root filesystem (Unix version)
func detectDiskSpace() *DiskInfo {
// 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("/", &stat)
err := unix.Statfs(path, &stat)
if err != nil {
return 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)
@@ -24,6 +34,7 @@ func detectDiskSpace() *DiskInfo {
usedPercent := float64(used) / float64(total) * 100
return &DiskInfo{
Path: path,
Total: total,
Free: free,
Used: used,

View File

@@ -6,23 +6,49 @@
package envcheck
import (
"path/filepath"
"golang.org/x/sys/windows"
)
// detectDiskSpace detects disk space on the C: drive (Windows version)
func detectDiskSpace() *DiskInfo {
// 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
path := windows.StringToUTF16Ptr("C:\\")
err := windows.GetDiskFreeSpaceEx(path, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
pathPtr := windows.StringToUTF16Ptr(drivePath)
err = windows.GetDiskFreeSpaceEx(pathPtr, &freeBytesAvailable, &totalNumberOfBytes, &totalNumberOfFreeBytes)
if err != nil {
return 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,