diff --git a/.gitea/workflows/release-nightly.yml b/.gitea/workflows/release-nightly.yml index 280cc359..708e1621 100644 --- a/.gitea/workflows/release-nightly.yml +++ b/.gitea/workflows/release-nightly.yml @@ -43,12 +43,18 @@ jobs: strategy: matrix: variant: + # The basic image is built from source and can target any arch the + # toolchain supports. The dind variants are limited to the arches the + # docker:dind base image publishes. - target: basic tag_suffix: "" + platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x - target: dind tag_suffix: "-dind" + platforms: linux/amd64,linux/arm64 - target: dind-rootless tag_suffix: "-dind-rootless" + platforms: linux/amd64,linux/arm64 steps: - name: Checkout @@ -82,9 +88,7 @@ jobs: context: . file: ./Dockerfile target: ${{ matrix.variant.target }} - platforms: | - linux/amd64 - linux/arm64 + platforms: ${{ matrix.variant.platforms }} push: true tags: | ${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }} diff --git a/.gitea/workflows/release-tag.yml b/.gitea/workflows/release-tag.yml index 0691dde1..e9d76e5c 100644 --- a/.gitea/workflows/release-tag.yml +++ b/.gitea/workflows/release-tag.yml @@ -42,12 +42,18 @@ jobs: strategy: matrix: variant: + # The basic image is built from source and can target any arch the + # toolchain supports. The dind variants are limited to the arches the + # docker:dind base image publishes. - target: basic tag_suffix: "" + platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x - target: dind tag_suffix: "-dind" + platforms: linux/amd64,linux/arm64 - target: dind-rootless tag_suffix: "-dind-rootless" + platforms: linux/amd64,linux/arm64 container: image: catthehacker/ubuntu:act-latest env: @@ -91,9 +97,7 @@ jobs: context: . file: ./Dockerfile target: ${{ matrix.variant.target }} - platforms: | - linux/amd64 - linux/arm64 + platforms: ${{ matrix.variant.platforms }} push: true tags: ${{ steps.docker_meta.outputs.tags }} build-args: | diff --git a/README.md b/README.md index ad03a664..ffc53149 100644 --- a/README.md +++ b/README.md @@ -143,23 +143,16 @@ Every option is described in [config.example.yaml](internal/pkg/config/config.ex #### Without a config file -If you omit `-c`, built-in defaults apply (same as an empty YAML document). A small set of **deprecated** environment variables can still override parts of that default config, but **only when no `-c` path was given**; they are ignored if you use a config file: +If you omit `-c`, built-in defaults apply (same as an empty YAML document). -| Variable | Effect | -| --- | --- | -| `GITEA_DEBUG` | If true, sets log level to `debug` | -| `GITEA_TRACE` | If true, sets log level to `trace` | -| `GITEA_RUNNER_CAPACITY` | Concurrent jobs (integer) | -| `GITEA_RUNNER_FILE` | Registration state file path (default `.runner`) | -| `GITEA_RUNNER_ENVIRON` | Extra job env vars as comma-separated `KEY:VALUE` pairs | -| `GITEA_RUNNER_ENV_FILE` | Path to an env file merged into job env (same idea as `runner.env_file` in YAML) | - -Prefer a YAML file for all settings. +Earlier releases let a small set of environment variables (`GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE`) override parts of the default config. Those overrides have been removed — use a YAML config file for all settings instead. For the Docker images, the entrypoint still understands a separate set of variables (such as `RUNNER_STATE_FILE`); see [scripts/run.sh](scripts/run.sh) and the container documentation below. #### Registration vs config labels If `runner.labels` is set in the YAML file, those labels are used during `register` and the `--labels` CLI flag is ignored. +> **Note:** A runner that only exposes `host` labels still needs access to a Docker daemon (e.g. a mounted `/var/run/docker.sock`) whenever a job uses a `docker://` action or a service container. `host` labels only change where the job's own steps run; container-based steps and actions are still executed with Docker. + #### Caching (`actions/cache`) Each runner starts its own cache server automatically. Cache entries are local to that runner — runners do not share a cache by default. diff --git a/act/runner/run_context.go b/act/runner/run_context.go index 5fdfeb41..1f36da0c 100644 --- a/act/runner/run_context.go +++ b/act/runner/run_context.go @@ -138,7 +138,9 @@ func (rc *RunContext) GetEnv() map[string]string { } } } - rc.Env["ACT"] = "true" + if !rc.Config.DisableActEnv { + rc.Env["ACT"] = "true" + } if !rc.Config.NoSkipCheckout { rc.Env["ACT_SKIP_CHECKOUT"] = "true" @@ -790,7 +792,13 @@ func (rc *RunContext) Executor() (common.Executor, error) { return func(ctx context.Context) error { res, err := rc.isEnabled(ctx) if err != nil { - rc.caller.setReusedWorkflowJobResult(rc.JobName, "failure") // For Gitea + // Record the failure so a job whose if-expression fails to evaluate + // gets a result (and therefore a stop time) instead of being left + // unfinished. rc.caller is only set for reusable workflows. + rc.result("failure") + if rc.caller != nil { // For Gitea + rc.caller.setReusedWorkflowJobResult(rc.JobName, "failure") + } return err } if res { diff --git a/act/runner/runner.go b/act/runner/runner.go index b8a12cde..51191a98 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -65,6 +65,7 @@ type Config struct { ArtifactServerAddr string // the address the artifact server binds to ArtifactServerPort string // the port the artifact server binds to NoSkipCheckout bool // do not skip actions/checkout + DisableActEnv bool // do not inject the ACT=true environment variable into jobs RemoteName string // remote name in local git repo config ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub. diff --git a/examples/kubernetes/README.md b/examples/kubernetes/README.md index c755c885..403bea52 100644 --- a/examples/kubernetes/README.md +++ b/examples/kubernetes/README.md @@ -13,3 +13,6 @@ Files in this directory: - [`rootless-docker.yaml`](rootless-docker.yaml) How to create a rootless Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted. + +- [`statefulset-dind.yaml`](statefulset-dind.yaml) + StatefulSet variant of the dind example. Each replica gets a stable identity and its own persistent volume via `volumeClaimTemplates`, so the runner keeps its `.runner` registration across restarts and reschedules instead of trying to register again. diff --git a/examples/kubernetes/statefulset-dind.yaml b/examples/kubernetes/statefulset-dind.yaml new file mode 100644 index 00000000..783f6b97 --- /dev/null +++ b/examples/kubernetes/statefulset-dind.yaml @@ -0,0 +1,79 @@ +# StatefulSet variant of the dind example. +# +# Unlike the Deployment, a StatefulSet gives each replica a stable identity and, +# via volumeClaimTemplates, its own persistent volume. That means every runner +# pod keeps its own `.runner` registration file across restarts and reschedules, +# so it re-attaches to the server instead of trying to register again. +apiVersion: v1 +data: + # The registration token can be obtained from the web UI, API or command-line. + # You can also set a pre-defined global runner registration token for the Gitea instance via + # `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable. + token: << base64 encoded registration token >> +kind: Secret +metadata: + name: runner-secret +type: Opaque +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + labels: + app: runner + name: runner +spec: + serviceName: runner + replicas: 1 + selector: + matchLabels: + app: runner + template: + metadata: + labels: + app: runner + spec: + restartPolicy: Always + volumes: + - name: docker-socket + emptyDir: {} + initContainers: + - name: docker + image: docker:28.2.2-dind + securityContext: + privileged: true + volumeMounts: + - name: docker-socket + mountPath: /var/run + startupProbe: + exec: + command: ["/usr/bin/test", "-S", "/var/run/docker.sock"] + livenessProbe: + exec: + command: ["/usr/bin/test", "-S", "/var/run/docker.sock"] + restartPolicy: Always + containers: + - name: runner + image: gitea/runner:nightly + env: + - name: GITEA_INSTANCE_URL + value: http://gitea-http.gitea.svc.cluster.local:3000 + - name: GITEA_RUNNER_REGISTRATION_TOKEN + valueFrom: + secretKeyRef: + name: runner-secret + key: token + volumeMounts: + - name: runner-data + mountPath: /data + - name: docker-socket + mountPath: /var/run + volumeClaimTemplates: + - metadata: + name: runner-data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: standard diff --git a/examples/systemd/README.md b/examples/systemd/README.md new file mode 100644 index 00000000..e7244f6e --- /dev/null +++ b/examples/systemd/README.md @@ -0,0 +1,34 @@ +# Running the runner as a systemd service + +[`gitea-runner.service`](./gitea-runner.service) is an example unit for running +the runner as a background service on a systemd host. + +## Setup + +1. Install the `gitea-runner` binary (e.g. to `/usr/local/bin/gitea-runner`). +2. Create a dedicated user and working directory: + + ```bash + sudo useradd --system --home-dir /var/lib/gitea-runner --create-home gitea-runner + ``` + +3. Generate a config and register the runner (as the service user), so the + `.runner` file ends up in the working directory: + + ```bash + sudo -u gitea-runner gitea-runner generate-config > /etc/gitea-runner/config.yaml + cd /var/lib/gitea-runner + sudo -u gitea-runner gitea-runner register --config /etc/gitea-runner/config.yaml + ``` + +4. Install and enable the unit: + + ```bash + sudo cp gitea-runner.service /etc/systemd/system/gitea-runner.service + sudo systemctl daemon-reload + sudo systemctl enable --now gitea-runner + ``` + +Adjust the binary path, config path, working directory and user to match your +installation. If jobs use the host's Docker daemon, uncomment the +`docker.service` dependencies in the unit. diff --git a/examples/systemd/gitea-runner.service b/examples/systemd/gitea-runner.service new file mode 100644 index 00000000..17c37694 --- /dev/null +++ b/examples/systemd/gitea-runner.service @@ -0,0 +1,30 @@ +[Unit] +Description=Gitea Actions runner +Documentation=https://gitea.com/gitea/runner +After=network-online.target +Wants=network-online.target +# Uncomment when jobs use the local Docker daemon: +# After=docker.service +# Requires=docker.service + +[Service] +Type=simple +# Adjust the binary path, config path and working directory to your setup. +# The working directory is where the .runner registration file is read from +# unless runner.file is set to an absolute path in the config. +ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml +WorkingDirectory=/var/lib/gitea-runner +User=gitea-runner +Group=gitea-runner + +# Restart automatically so the runner survives transient failures, e.g. the +# Gitea instance being temporarily unreachable at startup. +Restart=on-failure +RestartSec=5s + +# Allow running jobs to finish before the runner is stopped. Keep this in sync +# with runner.shutdown_timeout in the config. +TimeoutStopSec=3h + +[Install] +WantedBy=multi-user.target diff --git a/internal/app/cmd/bug_report.go b/internal/app/cmd/bug_report.go new file mode 100644 index 00000000..666e7a9f --- /dev/null +++ b/internal/app/cmd/bug_report.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "fmt" + "runtime" + + "gitea.com/gitea/runner/internal/pkg/ver" + + "github.com/spf13/cobra" +) + +// loadBugReportCmd prints environment details that are useful when opening a +// bug report, so users can paste them straight into an issue. +func loadBugReportCmd() *cobra.Command { + return &cobra.Command{ + Use: "bug-report", + Short: "Print information useful when filing a bug report", + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + w := cmd.OutOrStdout() + fmt.Fprintf(w, "Runner version: %s\n", ver.Version()) + fmt.Fprintf(w, "Go version: %s\n", runtime.Version()) + fmt.Fprintf(w, "OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH) + fmt.Fprintf(w, "NumCPU: %d\n", runtime.NumCPU()) + return nil + }, + } +} diff --git a/internal/app/cmd/cmd.go b/internal/app/cmd/cmd.go index 95074001..d20d518c 100644 --- a/internal/app/cmd/cmd.go +++ b/internal/app/cmd/cmd.go @@ -56,6 +56,9 @@ func Execute(ctx context.Context) { // ./gitea-runner exec rootCmd.AddCommand(loadExecCmd(ctx)) + // ./gitea-runner bug-report + rootCmd.AddCommand(loadBugReportCmd()) + // ./gitea-runner config rootCmd.AddCommand(&cobra.Command{ Use: "generate-config", diff --git a/internal/app/cmd/daemon.go b/internal/app/cmd/daemon.go index f533b017..486fc18f 100644 --- a/internal/app/cmd/daemon.go +++ b/internal/app/cmd/daemon.go @@ -176,7 +176,12 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu } else { go poller.Poll() - <-ctx.Done() + // Stop either on an external cancellation or when the poller shuts + // itself down (e.g. after the runner has been unregistered). + select { + case <-ctx.Done(): + case <-poller.Done(): + } } log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout) @@ -189,6 +194,10 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name) } + if poller.Unregistered() { + return errors.New("runner is no longer registered with the server; please register it again") + } + return nil } } diff --git a/internal/app/cmd/exec.go b/internal/app/cmd/exec.go index 053930bd..ccbfae71 100644 --- a/internal/app/cmd/exec.go +++ b/internal/app/cmd/exec.go @@ -34,6 +34,7 @@ type executeArgs struct { runList bool job string event string + eventpath string workdir string workflowsPath string noWorkflowRecurse bool @@ -441,6 +442,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command ArtifactServerPort: execArgs.artifactServerPort, ArtifactServerAddr: execArgs.artifactServerAddr, NoSkipCheckout: execArgs.noSkipCheckout, + EventPath: execArgs.resolve(execArgs.eventpath), // PresetGitHubContext: preset, // EventJSON: string(eventJSON), ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName, @@ -496,8 +498,9 @@ func loadExecCmd(ctx context.Context) *cobra.Command { } execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows") - execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID") + execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID; when several workflow files define that job, also pass --workflows/-W to select the file") execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name") + execCmd.Flags().StringVarP(&execArg.eventpath, "eventpath", "e", "", "path to a JSON event payload file exposed as the event that triggered the workflow") execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)") execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory") execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag") diff --git a/internal/app/poll/poller.go b/internal/app/poll/poller.go index d5399831..37057afd 100644 --- a/internal/app/poll/poller.go +++ b/internal/app/poll/poller.go @@ -45,6 +45,10 @@ type Poller struct { shutdownJobs context.CancelFunc done chan struct{} + + // unregistered is set when the server rejects the runner with an + // Unauthenticated response, meaning the runner is no longer registered. + unregistered atomic.Bool } // workerState holds the single poller's backoff state. Consecutive empty or @@ -137,6 +141,19 @@ func (p *Poller) PollOnce() { } } +// Done returns a channel that is closed once polling has fully stopped, +// allowing callers to react when the poller shuts itself down (e.g. after the +// runner has been unregistered) rather than only on an external cancellation. +func (p *Poller) Done() <-chan struct{} { + return p.done +} + +// Unregistered reports whether polling stopped because the server rejected the +// runner as unregistered (an Unauthenticated response). +func (p *Poller) Unregistered() bool { + return p.unregistered.Load() +} + func (p *Poller) runIdleMaintenance() { if idleRunner, ok := p.runner.(IdleRunner); ok { idleRunner.OnIdle(p.jobsCtx) @@ -264,6 +281,15 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task, metrics.PollFetchDuration.Observe(time.Since(start).Seconds()) if err != nil { + // An Unauthenticated response means the server no longer knows this + // runner (e.g. it was deleted). Retrying forever is pointless, so stop + // polling and let the daemon exit with an error instead of spinning. + if connect.CodeOf(err) == connect.CodeUnauthenticated { + log.WithError(err).Error("server rejected the runner as unregistered, stopping poller") + p.unregistered.Store(true) + p.shutdownPolling() + return nil, false + } log.WithError(err).Error("failed to fetch task") s.consecutiveErrors++ metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc() diff --git a/internal/app/poll/poller_test.go b/internal/app/poll/poller_test.go index 78fe2cc5..92ca8fad 100644 --- a/internal/app/poll/poller_test.go +++ b/internal/app/poll/poller_test.go @@ -78,6 +78,35 @@ func TestPoller_FetchErrorIncrementsErrorsOnly(t *testing.T) { assert.Equal(t, int64(0), s.consecutiveEmpty) } +// TestPoller_FetchUnauthenticatedStopsPolling verifies that an Unauthenticated +// response marks the runner as unregistered and cancels the polling context so +// the daemon can exit instead of retrying forever. +func TestPoller_FetchUnauthenticatedStopsPolling(t *testing.T) { + client := mocks.NewClient(t) + client.On("FetchTask", mock.Anything, mock.Anything).Return( + func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnauthenticated, errors.New("unregistered runner")) + }, + ) + + cfg, err := config.LoadDefault("") + require.NoError(t, err) + p := New(cfg, client, nil) + + s := &workerState{} + _, ok := p.fetchTask(context.Background(), s) + require.False(t, ok) + + assert.True(t, p.Unregistered(), "runner should be marked unregistered") + assert.Equal(t, int64(0), s.consecutiveErrors, "unauthenticated must not drive error backoff") + + select { + case <-p.pollingCtx.Done(): + default: + t.Fatal("expected polling context to be cancelled after an Unauthenticated response") + } +} + // TestPoller_CalculateInterval verifies the exponential backoff math is // correctly driven by the workerState counters. func TestPoller_CalculateInterval(t *testing.T) { diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index a751c2de..d041aff0 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -445,6 +445,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"), AutoRemove: true, NoSkipCheckout: true, + DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv, PresetGitHubContext: preset, EventJSON: string(eventJSON), ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id), diff --git a/internal/pkg/client/http.go b/internal/pkg/client/http.go index b2fa2bb0..6a837568 100644 --- a/internal/pkg/client/http.go +++ b/internal/pkg/client/http.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "gitea.com/gitea/runner/internal/pkg/ver" + "connectrpc.com/connect" "gitea.dev/actions-proto-go/ping/v1/pingv1connect" "gitea.dev/actions-proto-go/runner/v1/runnerv1connect" @@ -36,6 +38,7 @@ func New(endpoint string, insecure bool, uuid, token string, opts ...connect.Cli opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + req.Header().Set("User-Agent", "gitea-runner/"+ver.Version()) if uuid != "" { req.Header().Set(UUIDHeader, uuid) } diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 72d6b9c7..95412e3b 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -72,6 +72,9 @@ runner: # When true (the default), fetch only the requested ref of an action repository (e.g. actions/checkout@v4) at depth 1 instead of cloning every branch's full history. # Set to false to clone the full history. action_shallow_clone: true + # When true (the default), inject the ACT=true environment variable into jobs. + # Set to false so workflows gated on `if: ${{ !env.ACT }}` behave like they do on GitHub. + set_act_env: 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" # Find more images provided by Gitea at https://gitea.com/gitea/runner-images . @@ -175,8 +178,9 @@ container: # If it's "-", runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers. # If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work. docker_host: "" - # Pull docker image(s) even if already present - force_pull: true + # Pull docker image(s) even if already present. + # Defaults to false when the key is omitted. + force_pull: false # Rebuild docker image(s) even if already present force_rebuild: false # Always require a reachable docker daemon, even if not required by runner diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 240def9d..423ee9ed 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -49,6 +49,7 @@ type Runner struct { Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github ActionShallowClone *bool `yaml:"action_shallow_clone"` // ActionShallowClone fetches only the requested ref of an action repository at depth 1 instead of cloning every branch's full history. It is a pointer to distinguish between false and not set; if not set, it defaults to true. + SetActEnv *bool `yaml:"set_act_env"` // SetActEnv controls whether the ACT=true environment variable is injected into jobs. It is a pointer to distinguish between false and not set; if not set, it defaults to true. Set it to false so workflows gated on `if: ${{ !env.ACT }}` behave like on GitHub. AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends. PostTaskScript string `yaml:"post_task_script"` // PostTaskScript is the path to an executable script run on the host after each task's cleanup completes. Empty disables the hook. On Windows use .exe/.bat/.cmd; PowerShell (.ps1) is not supported yet as the configured path. PostTaskScriptTimeout time.Duration `yaml:"post_task_script_timeout"` // PostTaskScriptTimeout caps how long the post-task script may run. Default is 5m when post_task_script is set. @@ -156,13 +157,20 @@ func LoadDefault(file string) (*Config, error) { b := true cfg.Runner.ActionShallowClone = &b } + if cfg.Runner.SetActEnv == nil { + b := true + cfg.Runner.SetActEnv = &b + } if cfg.Cache.Enabled == nil { b := true cfg.Cache.Enabled = &b } if *cfg.Cache.Enabled { if cfg.Cache.Dir == "" { - home, _ := os.UserHomeDir() + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("cache.dir is unset and the user home directory could not be determined: %w", err) + } cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache") } if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" { @@ -173,7 +181,10 @@ func LoadDefault(file string) (*Config, error) { cfg.Container.WorkdirParent = "workspace" } if cfg.Host.WorkdirParent == "" { - home, _ := os.UserHomeDir() + home, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("host.workdir_parent is unset and the user home directory could not be determined: %w", err) + } cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act") } if cfg.Runner.FetchTimeout <= 0 {