diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index dd0537f2..1dec77df 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "maps" + "os" "path/filepath" "strings" "sync" @@ -196,11 +197,18 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. maxLifetime = time.Until(deadline) } + workdirParent := strings.TrimLeft(r.cfg.Container.WorkdirParent, "/") + if r.cfg.Container.BindWorkdir { + // Append the task ID to isolate concurrent jobs from the same repo. + workdirParent = fmt.Sprintf("%s/%d", workdirParent, task.Id) + } + workdir := filepath.FromSlash(fmt.Sprintf("/%s/%s", workdirParent, preset.Repository)) + runnerConfig := &runner.Config{ // On Linux, Workdir will be like "///" // On Windows, Workdir will be like "\\\" - Workdir: filepath.FromSlash(fmt.Sprintf("/%s/%s", strings.TrimLeft(r.cfg.Container.WorkdirParent, "/"), preset.Repository)), - BindWorkdir: false, + Workdir: workdir, + BindWorkdir: r.cfg.Container.BindWorkdir, ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent), ReuseContainers: false, @@ -245,6 +253,15 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. execErr := executor(ctx) reporter.SetOutputs(job.Outputs) + + if r.cfg.Container.BindWorkdir { + // Remove the entire task-specific directory (e.g. /workspace/). + taskDir := filepath.FromSlash("/" + workdirParent) + if err := os.RemoveAll(taskDir); err != nil { + log.Warnf("failed to clean up workspace %s: %v", taskDir, err) + } + } + return execErr } diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 4d21ef58..7f9c41a8 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -103,6 +103,12 @@ container: require_docker: false # Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner docker_timeout: 0s + # Bind the workspace to the host filesystem instead of using Docker volumes. + # This is required for Docker-in-Docker (DinD) setups when jobs use docker compose + # with bind mounts (e.g., ".:/app"), as volume-based workspaces are not accessible + # from the DinD daemon's filesystem. When enabled, ensure the workspace parent + # directory is also mounted into the runner container and listed in valid_volumes. + bind_workdir: false host: # The parent directory of a job's working directory. diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 0fe48d94..764369fb 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -57,6 +57,7 @@ type Container struct { ForceRebuild bool `yaml:"force_rebuild"` // Rebuild docker image(s) even if already present RequireDocker bool `yaml:"require_docker"` // Always require a reachable docker daemon, even if not required by act_runner DockerTimeout time.Duration `yaml:"docker_timeout"` // Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner + BindWorkdir bool `yaml:"bind_workdir"` // BindWorkdir binds the workspace to the host filesystem instead of using Docker volumes. Required for DinD when jobs use docker compose with bind mounts. } // Host represents the configuration for the host.