mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-07-22 02:37:45 +00:00
fix: Minor fixes (#1075)
A batch of small, self-contained fixes and docs/example additions. Fixes #625 - align the example config's `force_pull` with the actual default (`false`) Fixes #804 - return an error instead of discarding `os.UserHomeDir()` when defaulting `cache.dir`/`host.workdir_parent` Fixes #571 - send a `gitea-runner/<version>` User-Agent on API requests Fixes #650 - detect an `Unauthenticated` fetch response and exit the daemon with an error instead of retrying forever Fixes #766 - add `exec --eventpath` to supply a JSON event payload file Fixes #256 - add a `bug-report` subcommand that prints version/Go/OS-arch/CPU info Fixes #617 - add `runner.set_act_env` (default `true`) to optionally omit the `ACT=true` env var Fixes #635 - record a failure result (and guard a nil reusable-workflow caller) when the job `if`-expression fails to evaluate Fixes #1005 - remove README docs for config env-var overrides that were already removed from the code Fixes #448 - clarify in `exec --job` help that `--workflows` may be needed to disambiguate Fixes #209 - note that `host`-labelled runners still need Docker for `docker://` actions and service containers Fixes #757 - add a systemd service example with automatic restart Fixes #474 - add a Kubernetes StatefulSet example that persists the `.runner` registration across reschedules Fixes #776 - build the basic (non-dind) docker image for `linux/riscv64` Fixes #628 - build the basic (non-dind) docker image for `linux/s390x`Reviewed-on: https://gitea.com/gitea/runner/pulls/1075 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
@@ -43,12 +43,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant:
|
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
|
- target: basic
|
||||||
tag_suffix: ""
|
tag_suffix: ""
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x
|
||||||
- target: dind
|
- target: dind
|
||||||
tag_suffix: "-dind"
|
tag_suffix: "-dind"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
- target: dind-rootless
|
- target: dind-rootless
|
||||||
tag_suffix: "-dind-rootless"
|
tag_suffix: "-dind-rootless"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -82,9 +88,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
target: ${{ matrix.variant.target }}
|
target: ${{ matrix.variant.target }}
|
||||||
platforms: |
|
platforms: ${{ matrix.variant.platforms }}
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||||
|
|||||||
@@ -42,12 +42,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant:
|
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
|
- target: basic
|
||||||
tag_suffix: ""
|
tag_suffix: ""
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x
|
||||||
- target: dind
|
- target: dind
|
||||||
tag_suffix: "-dind"
|
tag_suffix: "-dind"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
- target: dind-rootless
|
- target: dind-rootless
|
||||||
tag_suffix: "-dind-rootless"
|
tag_suffix: "-dind-rootless"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
env:
|
env:
|
||||||
@@ -91,9 +97,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
target: ${{ matrix.variant.target }}
|
target: ${{ matrix.variant.target }}
|
||||||
platforms: |
|
platforms: ${{ matrix.variant.platforms }}
|
||||||
linux/amd64
|
|
||||||
linux/arm64
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -143,23 +143,16 @@ Every option is described in [config.example.yaml](internal/pkg/config/config.ex
|
|||||||
|
|
||||||
#### Without a config file
|
#### 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 |
|
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.
|
||||||
| --- | --- |
|
|
||||||
| `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.
|
|
||||||
|
|
||||||
#### Registration vs config labels
|
#### 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.
|
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`)
|
#### 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.
|
Each runner starts its own cache server automatically. Cache entries are local to that runner — runners do not share a cache by default.
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ func (rc *RunContext) GetEnv() map[string]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !rc.Config.DisableActEnv {
|
||||||
rc.Env["ACT"] = "true"
|
rc.Env["ACT"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
if !rc.Config.NoSkipCheckout {
|
if !rc.Config.NoSkipCheckout {
|
||||||
rc.Env["ACT_SKIP_CHECKOUT"] = "true"
|
rc.Env["ACT_SKIP_CHECKOUT"] = "true"
|
||||||
@@ -790,7 +792,13 @@ func (rc *RunContext) Executor() (common.Executor, error) {
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
res, err := rc.isEnabled(ctx)
|
res, err := rc.isEnabled(ctx)
|
||||||
if err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ type Config struct {
|
|||||||
ArtifactServerAddr string // the address the artifact server binds to
|
ArtifactServerAddr string // the address the artifact server binds to
|
||||||
ArtifactServerPort string // the port the artifact server binds to
|
ArtifactServerPort string // the port the artifact server binds to
|
||||||
NoSkipCheckout bool // do not skip actions/checkout
|
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
|
RemoteName string // remote name in local git repo config
|
||||||
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
||||||
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ Files in this directory:
|
|||||||
|
|
||||||
- [`rootless-docker.yaml`](rootless-docker.yaml)
|
- [`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.
|
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.
|
||||||
|
|||||||
79
examples/kubernetes/statefulset-dind.yaml
Normal file
79
examples/kubernetes/statefulset-dind.yaml
Normal file
@@ -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
|
||||||
34
examples/systemd/README.md
Normal file
34
examples/systemd/README.md
Normal file
@@ -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.
|
||||||
30
examples/systemd/gitea-runner.service
Normal file
30
examples/systemd/gitea-runner.service
Normal file
@@ -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
|
||||||
31
internal/app/cmd/bug_report.go
Normal file
31
internal/app/cmd/bug_report.go
Normal file
@@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,9 @@ func Execute(ctx context.Context) {
|
|||||||
// ./gitea-runner exec
|
// ./gitea-runner exec
|
||||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||||
|
|
||||||
|
// ./gitea-runner bug-report
|
||||||
|
rootCmd.AddCommand(loadBugReportCmd())
|
||||||
|
|
||||||
// ./gitea-runner config
|
// ./gitea-runner config
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: "generate-config",
|
Use: "generate-config",
|
||||||
|
|||||||
@@ -176,7 +176,12 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
} else {
|
} else {
|
||||||
go poller.Poll()
|
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)
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type executeArgs struct {
|
|||||||
runList bool
|
runList bool
|
||||||
job string
|
job string
|
||||||
event string
|
event string
|
||||||
|
eventpath string
|
||||||
workdir string
|
workdir string
|
||||||
workflowsPath string
|
workflowsPath string
|
||||||
noWorkflowRecurse bool
|
noWorkflowRecurse bool
|
||||||
@@ -441,6 +442,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
ArtifactServerPort: execArgs.artifactServerPort,
|
ArtifactServerPort: execArgs.artifactServerPort,
|
||||||
ArtifactServerAddr: execArgs.artifactServerAddr,
|
ArtifactServerAddr: execArgs.artifactServerAddr,
|
||||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||||
|
EventPath: execArgs.resolve(execArgs.eventpath),
|
||||||
// PresetGitHubContext: preset,
|
// PresetGitHubContext: preset,
|
||||||
// EventJSON: string(eventJSON),
|
// EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
|
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().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.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.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
||||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
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")
|
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ type Poller struct {
|
|||||||
shutdownJobs context.CancelFunc
|
shutdownJobs context.CancelFunc
|
||||||
|
|
||||||
done chan struct{}
|
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
|
// 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() {
|
func (p *Poller) runIdleMaintenance() {
|
||||||
if idleRunner, ok := p.runner.(IdleRunner); ok {
|
if idleRunner, ok := p.runner.(IdleRunner); ok {
|
||||||
idleRunner.OnIdle(p.jobsCtx)
|
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())
|
metrics.PollFetchDuration.Observe(time.Since(start).Seconds())
|
||||||
|
|
||||||
if err != nil {
|
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")
|
log.WithError(err).Error("failed to fetch task")
|
||||||
s.consecutiveErrors++
|
s.consecutiveErrors++
|
||||||
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
||||||
|
|||||||
@@ -78,6 +78,35 @@ func TestPoller_FetchErrorIncrementsErrorsOnly(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), s.consecutiveEmpty)
|
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
|
// TestPoller_CalculateInterval verifies the exponential backoff math is
|
||||||
// correctly driven by the workerState counters.
|
// correctly driven by the workerState counters.
|
||||||
func TestPoller_CalculateInterval(t *testing.T) {
|
func TestPoller_CalculateInterval(t *testing.T) {
|
||||||
|
|||||||
@@ -445,6 +445,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
NoSkipCheckout: true,
|
NoSkipCheckout: true,
|
||||||
|
DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv,
|
||||||
PresetGitHubContext: preset,
|
PresetGitHubContext: preset,
|
||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/internal/pkg/ver"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
|
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
|
||||||
"gitea.dev/actions-proto-go/runner/v1/runnerv1connect"
|
"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 {
|
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
|
req.Header().Set("User-Agent", "gitea-runner/"+ver.Version())
|
||||||
if uuid != "" {
|
if uuid != "" {
|
||||||
req.Header().Set(UUIDHeader, uuid)
|
req.Header().Set(UUIDHeader, uuid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
# 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.
|
# Set to false to clone the full history.
|
||||||
action_shallow_clone: true
|
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.
|
# 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" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||||
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
|
# 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 "-", 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.
|
# 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: ""
|
docker_host: ""
|
||||||
# Pull docker image(s) even if already present
|
# Pull docker image(s) even if already present.
|
||||||
force_pull: true
|
# Defaults to false when the key is omitted.
|
||||||
|
force_pull: false
|
||||||
# Rebuild docker image(s) even if already present
|
# Rebuild docker image(s) even if already present
|
||||||
force_rebuild: false
|
force_rebuild: false
|
||||||
# Always require a reachable docker daemon, even if not required by runner
|
# Always require a reachable docker daemon, even if not required by runner
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type Runner struct {
|
|||||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
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
|
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.
|
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.
|
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.
|
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.
|
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
|
b := true
|
||||||
cfg.Runner.ActionShallowClone = &b
|
cfg.Runner.ActionShallowClone = &b
|
||||||
}
|
}
|
||||||
|
if cfg.Runner.SetActEnv == nil {
|
||||||
|
b := true
|
||||||
|
cfg.Runner.SetActEnv = &b
|
||||||
|
}
|
||||||
if cfg.Cache.Enabled == nil {
|
if cfg.Cache.Enabled == nil {
|
||||||
b := true
|
b := true
|
||||||
cfg.Cache.Enabled = &b
|
cfg.Cache.Enabled = &b
|
||||||
}
|
}
|
||||||
if *cfg.Cache.Enabled {
|
if *cfg.Cache.Enabled {
|
||||||
if cfg.Cache.Dir == "" {
|
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")
|
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||||
}
|
}
|
||||||
if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" {
|
if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" {
|
||||||
@@ -173,7 +181,10 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
cfg.Container.WorkdirParent = "workspace"
|
cfg.Container.WorkdirParent = "workspace"
|
||||||
}
|
}
|
||||||
if cfg.Host.WorkdirParent == "" {
|
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")
|
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
||||||
}
|
}
|
||||||
if cfg.Runner.FetchTimeout <= 0 {
|
if cfg.Runner.FetchTimeout <= 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user