mirror of
https://gitea.com/gitea/act_runner
synced 2026-09-21 19:37:07 +02:00
enhance: add macOS VM executor backend
Replace the Tart-based prototype with a runner-oriented macOS VM executor backed by runner-executor-macos and Apple Virtualization. Assisted-by: Codet:MODEL_VERSION
This commit is contained in:
@@ -92,7 +92,8 @@ runner:
|
||||
# at the price of the stock artifact actions refusing and the cache client keeping to v1.
|
||||
#patch_actions: true
|
||||
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
|
||||
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
# Like: "macos-arm64:host", "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest",
|
||||
# or "macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest"
|
||||
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
|
||||
# If it's empty when registering, it will ask for inputting labels.
|
||||
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
||||
@@ -271,6 +272,19 @@ host:
|
||||
# If it's empty, $HOME/.cache/act/ will be used.
|
||||
#workdir_parent:
|
||||
|
||||
macos_vm:
|
||||
# Path to the runner-executor-macos binary. Empty uses PATH lookup.
|
||||
#executor_path: runner-executor-macos
|
||||
# The guest directory under which the job workspace is created. The VM must run
|
||||
# as a user that can write there.
|
||||
#workdir_parent: /Users/admin/runner
|
||||
# Number of virtual CPUs. Omit or set to 0 to keep the image default.
|
||||
#cpu: 0
|
||||
# VM memory in megabytes. Omit or set to 0 to keep the image default.
|
||||
#memory: 0
|
||||
# How long to wait for the VM to become ready after it starts.
|
||||
#boot_timeout: 5m
|
||||
|
||||
# Optional local task-admission checks. Disabled by default. When enabled, low
|
||||
# disk space or a failing script pauses new task fetching; existing jobs continue.
|
||||
# No health checks run while any job is active; the last result is reused until idle.
|
||||
|
||||
@@ -179,6 +179,15 @@ type Host struct {
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent specifies the parent directory for the host's working directory.
|
||||
}
|
||||
|
||||
// MacOSVM represents the configuration for the macOS VM executor.
|
||||
type MacOSVM struct {
|
||||
ExecutorPath string `yaml:"executor_path"` // ExecutorPath is the runner-executor-macos binary to invoke.
|
||||
WorkdirParent string `yaml:"workdir_parent"` // WorkdirParent is the guest directory under which the job workspace is created.
|
||||
CPU int `yaml:"cpu"` // CPU is the number of virtual CPUs. Zero keeps the image default.
|
||||
Memory int `yaml:"memory"` // Memory is the VM memory in megabytes. Zero keeps the image default.
|
||||
BootTimeout time.Duration `yaml:"boot_timeout"` // BootTimeout bounds how long to wait for the VM to become ready.
|
||||
}
|
||||
|
||||
// Metrics represents the configuration for the Prometheus metrics endpoint.
|
||||
type Metrics struct {
|
||||
Enabled bool `yaml:"enabled"` // Enabled indicates whether the metrics endpoint is exposed.
|
||||
@@ -203,6 +212,7 @@ type Config struct {
|
||||
Cache Cache `yaml:"cache"` // Cache represents the configuration for caching.
|
||||
Container Container `yaml:"container"` // Container represents the configuration for the container.
|
||||
Host Host `yaml:"host"` // Host represents the configuration for the host.
|
||||
MacOSVM MacOSVM `yaml:"macos_vm"` // MacOSVM represents the configuration for the macOS VM executor.
|
||||
Metrics Metrics `yaml:"metrics"` // Metrics represents the configuration for the Prometheus metrics endpoint.
|
||||
HealthCheck HealthCheck `yaml:"health_check"` // HealthCheck controls opt-in local task-admission checks.
|
||||
}
|
||||
@@ -300,6 +310,15 @@ func LoadDefault(file string) (*Config, error) {
|
||||
}
|
||||
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
||||
}
|
||||
if cfg.MacOSVM.ExecutorPath == "" {
|
||||
cfg.MacOSVM.ExecutorPath = "runner-executor-macos"
|
||||
}
|
||||
if cfg.MacOSVM.WorkdirParent == "" {
|
||||
cfg.MacOSVM.WorkdirParent = "/Users/admin/runner"
|
||||
}
|
||||
if cfg.MacOSVM.BootTimeout <= 0 {
|
||||
cfg.MacOSVM.BootTimeout = 5 * time.Minute
|
||||
}
|
||||
if cfg.Runner.FetchTimeout <= 0 {
|
||||
cfg.Runner.FetchTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
@@ -67,6 +67,31 @@ func TestLoadDefault_DefaultsWorkdirCleanupAge(t *testing.T) {
|
||||
assert.Equal(t, DefaultImage, cfg.Runner.DefaultImage)
|
||||
}
|
||||
|
||||
func TestLoadDefault_MacOSVMDefaults(t *testing.T) {
|
||||
cfg, err := LoadDefault("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "runner-executor-macos", cfg.MacOSVM.ExecutorPath)
|
||||
assert.Equal(t, "/Users/admin/runner", cfg.MacOSVM.WorkdirParent)
|
||||
assert.Equal(t, 5*time.Minute, cfg.MacOSVM.BootTimeout)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
macos_vm:
|
||||
executor_path: /usr/local/bin/runner-executor-macos
|
||||
workdir_parent: /Users/runner
|
||||
cpu: 4
|
||||
memory: 8192
|
||||
boot_timeout: 3m
|
||||
`), 0o600))
|
||||
cfg, err = LoadDefault(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/usr/local/bin/runner-executor-macos", cfg.MacOSVM.ExecutorPath)
|
||||
assert.Equal(t, "/Users/runner", cfg.MacOSVM.WorkdirParent)
|
||||
assert.Equal(t, 4, cfg.MacOSVM.CPU)
|
||||
assert.Equal(t, 8192, cfg.MacOSVM.Memory)
|
||||
assert.Equal(t, 3*time.Minute, cfg.MacOSVM.BootTimeout)
|
||||
}
|
||||
|
||||
func TestLoadDefault_HealthChecksAreOptIn(t *testing.T) {
|
||||
cfg, err := LoadDefault("")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -9,11 +9,15 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SchemeHost = "host"
|
||||
SchemeDocker = "docker"
|
||||
SchemeHost = "host"
|
||||
SchemeDocker = "docker"
|
||||
SchemeMacOSVM = "macos-vm"
|
||||
|
||||
// SelfHostedPlatform is the platform marker act treats as "run on the host".
|
||||
SelfHostedPlatform = "-self-hosted"
|
||||
|
||||
// MacOSVMSchemePrefix marks a platform image that act should run in a macOS VM.
|
||||
MacOSVMSchemePrefix = "macos-vm://"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
@@ -42,7 +46,7 @@ func Parse(str string) (*Label, error) {
|
||||
if len(splits) >= 3 {
|
||||
label.Arg = splits[2]
|
||||
}
|
||||
if label.Schema != SchemeHost && label.Schema != SchemeDocker {
|
||||
if label.Schema != SchemeHost && label.Schema != SchemeDocker && label.Schema != SchemeMacOSVM {
|
||||
// Not a schema we know: the colon belongs to the label name itself.
|
||||
return &Label{
|
||||
Name: str,
|
||||
@@ -64,6 +68,15 @@ func (l Labels) RequireDocker() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (l Labels) RequireMacOSVM() bool {
|
||||
for _, label := range l {
|
||||
if label.Schema == SchemeMacOSVM {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PickPlatform returns the platform of the first runs-on entry this runner has a label for, or "".
|
||||
func (l Labels) PickPlatform(runsOn []string) string {
|
||||
platforms := make(map[string]string, len(l))
|
||||
@@ -72,10 +85,12 @@ func (l Labels) PickPlatform(runsOn []string) string {
|
||||
case SchemeDocker:
|
||||
// "//" will be ignored
|
||||
platforms[label.Name] = strings.TrimPrefix(label.Arg, "//")
|
||||
case SchemeMacOSVM:
|
||||
platforms[label.Name] = MacOSVMSchemePrefix + strings.TrimPrefix(label.Arg, "//")
|
||||
case SchemeHost:
|
||||
platforms[label.Name] = SelfHostedPlatform
|
||||
default:
|
||||
// unreachable: Parse only produces host or docker schemas
|
||||
// unreachable: Parse only produces host, docker, or macOS VM schemas
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,15 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest",
|
||||
want: &Label{
|
||||
Name: "macos-latest",
|
||||
Schema: "macos-vm",
|
||||
Arg: "//ghcr.io/cirruslabs/macos-sonoma-base:latest",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: "ubuntu:host",
|
||||
want: &Label{
|
||||
@@ -103,6 +112,7 @@ func TestRequireDocker(t *testing.T) {
|
||||
{"empty", nil, false},
|
||||
{"only host", []string{"ubuntu:host", "self-hosted"}, false},
|
||||
{"has docker", []string{"ubuntu:host", "ubuntu:docker://node:18"}, true},
|
||||
{"macos-vm does not require docker", []string{"macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -111,10 +121,29 @@ func TestRequireDocker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireMacOSVM(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
strs []string
|
||||
want bool
|
||||
}{
|
||||
{"empty", nil, false},
|
||||
{"only host", []string{"ubuntu:host", "self-hosted"}, false},
|
||||
{"only docker", []string{"ubuntu:docker://node:18"}, false},
|
||||
{"has macos-vm", []string{"macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.want, mustParse(t, tt.strs...).RequireMacOSVM())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickPlatform(t *testing.T) {
|
||||
ls := mustParse(t,
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
"macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest",
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
@@ -124,6 +153,7 @@ func TestPickPlatform(t *testing.T) {
|
||||
}{
|
||||
{"docker strips leading slashes", []string{"ubuntu"}, "node:18"},
|
||||
{"host maps to self-hosted marker", []string{"self-hosted"}, SelfHostedPlatform},
|
||||
{"macos-vm prefixes platform marker", []string{"macos-latest"}, MacOSVMSchemePrefix + "ghcr.io/cirruslabs/macos-sonoma-base:latest"},
|
||||
{"first match wins", []string{"self-hosted", "ubuntu"}, SelfHostedPlatform},
|
||||
{"unknown label picks nothing", []string{"windows"}, ""},
|
||||
{"no runsOn picks nothing", nil, ""},
|
||||
@@ -145,12 +175,14 @@ func TestToStrings(t *testing.T) {
|
||||
ls := mustParse(t,
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
"macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest",
|
||||
"bare",
|
||||
"pool:e57e18d4",
|
||||
)
|
||||
require.Equal(t, []string{
|
||||
"ubuntu:docker://node:18",
|
||||
"self-hosted:host",
|
||||
"macos-latest:macos-vm://ghcr.io/cirruslabs/macos-sonoma-base:latest",
|
||||
"bare:host",
|
||||
"pool:e57e18d4",
|
||||
}, ls.ToStrings())
|
||||
|
||||
Reference in New Issue
Block a user