mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 17:04:22 +02:00
Compare commits
2 Commits
68547886a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6c6ffef9 | ||
|
|
70387cca44 |
@@ -35,6 +35,7 @@ jobs:
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
- name: goreleaser
|
||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
|
||||
with:
|
||||
|
||||
@@ -27,6 +27,7 @@ jobs:
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
check-latest: true
|
||||
- name: Import GPG key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||
|
||||
@@ -21,6 +21,7 @@ jobs:
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
check-latest: true
|
||||
- name: prepare anonymous docker config
|
||||
run: mkdir -p "$DOCKER_CONFIG" && echo '{}' > "$DOCKER_CONFIG/config.json"
|
||||
# Pre-pull act/runner's two largest base images so a slow pull can't dominate `make test`;
|
||||
|
||||
5
Makefile
5
Makefile
@@ -141,7 +141,12 @@ security-check:
|
||||
|
||||
.PHONY: tidy
|
||||
tidy: ## run go mod tidy
|
||||
$(eval GO_TOOLCHAIN := $(shell grep -Eo '^toolchain\s+go[0-9.]+' go.mod | cut -d' ' -f2))
|
||||
$(GO) mod tidy
|
||||
@# workaround https://github.com/golang/go/issues/75331: restore toolchain if tidy dropped it
|
||||
@if [ -n "$(GO_TOOLCHAIN)" ] && ! grep -qE '^toolchain\s' go.mod; then \
|
||||
$(GO) mod edit -toolchain=$(GO_TOOLCHAIN); \
|
||||
fi
|
||||
|
||||
.PHONY: tidy-check
|
||||
tidy-check: tidy
|
||||
|
||||
44
README.md
44
README.md
@@ -129,48 +129,38 @@ Same idea as `dind`, but built on `docker:dind-rootless` so the bundled daemon a
|
||||
|
||||
### Configuration
|
||||
|
||||
The runner is configured with a YAML file. Generate a starting point (this matches what ships in the tree):
|
||||
The runner reads a YAML file. Without one, every option keeps its default.
|
||||
|
||||
```bash
|
||||
./gitea-runner config generate > config.yaml
|
||||
./gitea-runner config init # write config.yaml, with no option set
|
||||
./gitea-runner config generate | less # read what the options do
|
||||
./gitea-runner -c config.yaml daemon # -c also works on register and cache-server
|
||||
```
|
||||
|
||||
> The top-level `generate-config` command still does the same thing, but is deprecated in favour of `config generate`.
|
||||
|
||||
Pass it with `-c` / `--config` on any command that loads configuration (`register`, `daemon`, `cache-server`):
|
||||
|
||||
```bash
|
||||
./gitea-runner -c config.yaml register
|
||||
./gitea-runner -c config.yaml daemon
|
||||
./gitea-runner -c config.yaml cache-server
|
||||
```
|
||||
|
||||
Every option is described in [config.example.yaml](internal/pkg/config/config.example.yaml) (the same content `config generate` prints).
|
||||
`config generate` prints [config.example.yaml](internal/pkg/config/config.example.yaml). Every value in it is commented out, so copy the lines you want to change into your own file and uncomment them.
|
||||
|
||||
#### Editing a config file
|
||||
|
||||
`config` changes an existing file in place, keeping its comments and key order, which is handy in provisioning scripts:
|
||||
`config` edits a file in place, which is handy in provisioning scripts:
|
||||
|
||||
```bash
|
||||
./gitea-runner -c config.yaml config set runner.capacity 4
|
||||
./gitea-runner -c config.yaml config set runner.timeout 90m # written as 1h30m0s
|
||||
./gitea-runner -c config.yaml config set runner.envs.MY_VAR value
|
||||
./gitea-runner -c config.yaml config add runner.labels 'ubuntu:docker://node:22'
|
||||
./gitea-runner -c config.yaml config remove runner.labels 'ubuntu:docker://node:22'
|
||||
./gitea-runner -c config.yaml config get runner.labels
|
||||
./gitea-runner config set runner.capacity 4
|
||||
./gitea-runner config set runner.timeout 90m # written as 1h30m0s
|
||||
./gitea-runner config set runner.envs.MY_VAR value
|
||||
./gitea-runner config add runner.labels 'ubuntu:docker://node:22'
|
||||
./gitea-runner config remove runner.labels 'ubuntu:docker://node:22'
|
||||
./gitea-runner config get runner.labels
|
||||
```
|
||||
|
||||
`-c` is optional for these subcommands: without it they use `config.yaml` (or `config.yml`) from the working directory, falling back to the directory of the `gitea-runner` binary, and print which file they picked to stderr.
|
||||
A key is its dotted YAML path. An unknown key, a value of the wrong type, or `add`/`remove` on anything but a list is refused before the file is touched. `set` replaces a whole list when you give it several values.
|
||||
|
||||
Keys are the dotted YAML path and are validated against the known options, so a typo is rejected instead of being written. `add` and `remove` only work on list options such as `runner.labels` and `container.valid_volumes`, and fail if the value is already present or missing. `set` replaces the whole list when given several values.
|
||||
An edit keeps the comments and the key order of the file. Indentation becomes two spaces, and a blank line between two values is dropped.
|
||||
|
||||
The file is re-encoded on every edit, so indentation is normalised to two spaces and blank lines inside a section are dropped.
|
||||
`config get`, `set`, `add` and `remove` use `config.yaml` (or `config.yml`) from the working directory, then from the directory of the binary, and print their choice to stderr. `config init` writes `config.yaml` in the working directory, and refuses to overwrite an existing config without `--force`. Pass `-c` for another path.
|
||||
|
||||
#### Without a config file
|
||||
#### Environment variables
|
||||
|
||||
If you omit `-c`, built-in defaults apply (same as an empty YAML document).
|
||||
|
||||
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.
|
||||
Earlier releases let a few environment variables (`GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE`) override parts of the config. They are gone, use the YAML file for all settings. The Docker images still read their own variables, such as `RUNNER_STATE_FILE`, see [scripts/run.sh](scripts/run.sh) and the container documentation below.
|
||||
|
||||
### Labels
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ the runner as a background service on a systemd host.
|
||||
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
|
||||
3. Write a config, hand it to the service user, and register as that user so the
|
||||
`.runner` file ends up in the working directory:
|
||||
|
||||
```bash
|
||||
sudo -u gitea-runner gitea-runner config generate > /etc/gitea-runner/config.yaml
|
||||
sudo mkdir -p /etc/gitea-runner
|
||||
sudo gitea-runner config init --config /etc/gitea-runner/config.yaml
|
||||
sudo chown gitea-runner /etc/gitea-runner/config.yaml
|
||||
cd /var/lib/gitea-runner
|
||||
sudo -u gitea-runner gitea-runner register --config /etc/gitea-runner/config.yaml
|
||||
```
|
||||
|
||||
@@ -49,10 +49,10 @@ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
|
||||
gitea-runner register
|
||||
```
|
||||
|
||||
- Generate a `gitea-runner` configuration file in the data directory. Edit the file to adjust for the system.
|
||||
- Write a `gitea-runner` configuration file in the data directory. Edit the file to adjust for the system, `gitea-runner config generate` documents every option.
|
||||
|
||||
```bash
|
||||
gitea-runner config generate >/home/rootless/gitea-runner/config
|
||||
gitea-runner config init --config /home/rootless/gitea-runner/config
|
||||
```
|
||||
|
||||
- Create a new user-level`systemd` unit file as `/home/rootless/.config/systemd/user/gitea-runner.service` with the following contents:
|
||||
|
||||
2
go.mod
2
go.mod
@@ -2,6 +2,8 @@ module gitea.com/gitea/runner
|
||||
|
||||
go 1.26.0
|
||||
|
||||
toolchain go1.26.5
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.20.0
|
||||
dario.cat/mergo v1.0.2
|
||||
|
||||
@@ -26,6 +26,7 @@ func loadConfigCmd(configFile *string) *cobra.Command {
|
||||
}
|
||||
|
||||
configCmd.AddCommand(loadGenerateConfigCmd("generate"))
|
||||
configCmd.AddCommand(loadInitConfigCmd(configFile))
|
||||
|
||||
configCmd.AddCommand(&cobra.Command{
|
||||
Use: "get <key>",
|
||||
@@ -73,10 +74,38 @@ func loadConfigCmd(configFile *string) *cobra.Command {
|
||||
return configCmd
|
||||
}
|
||||
|
||||
func loadInitConfigCmd(configFile *string) *cobra.Command {
|
||||
var force bool
|
||||
initCmd := &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Write a minimal config file",
|
||||
Long: "Write a minimal config file, leaving every option at its default.\nWithout --config it writes config.yaml in the working directory.",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
file, taken := *configFile, []string{*configFile}
|
||||
if file == "" {
|
||||
file, taken = defaultConfigFileNames[0], defaultConfigFileNames // any of them would shadow the new file
|
||||
}
|
||||
for _, name := range taken {
|
||||
if _, err := os.Stat(name); err == nil && !force {
|
||||
return fmt.Errorf("config file %q already exists, pass --force to overwrite it", name)
|
||||
}
|
||||
}
|
||||
if err := config.WriteFile(file, []byte(config.Minimal)); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "wrote config file %q\n", file)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
initCmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite an existing config file")
|
||||
return initCmd
|
||||
}
|
||||
|
||||
func loadGenerateConfigCmd(use string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: use,
|
||||
Short: "Generate an example config file",
|
||||
Short: "Print the example config, which documents every option",
|
||||
Args: cobra.MaximumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s", config.Example)
|
||||
|
||||
@@ -32,6 +32,30 @@ func TestConfigCmdGeneratePrintsTheExample(t *testing.T) {
|
||||
assert.Equal(t, string(config.Example), out)
|
||||
}
|
||||
|
||||
func TestConfigCmdInitWritesTheMinimalConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "config.yaml")
|
||||
|
||||
out, _, err := runConfigCmd(t, file, "init")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, out, file)
|
||||
content, err := os.ReadFile(file)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, config.Minimal, string(content))
|
||||
|
||||
_, _, err = runConfigCmd(t, file, "init")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "--force")
|
||||
|
||||
_, _, err = runConfigCmd(t, file, "init", "--force")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Chdir(t.TempDir())
|
||||
_, _, err = runConfigCmd(t, "", "init")
|
||||
require.NoError(t, err)
|
||||
assert.FileExists(t, defaultConfigFileNames[0])
|
||||
}
|
||||
|
||||
// The subcommands only wire arguments through, so one pass over all of them is enough.
|
||||
func TestConfigCmdEditsTheFile(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "config.yaml")
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Example configuration file, it's safe to copy this as the default config file without any modification.
|
||||
|
||||
# You don't have to copy this file to your instance,
|
||||
# just run `./gitea-runner config generate > config.yaml` to generate a config file.
|
||||
# Every option with its default value, all commented out. Read this file, do not copy it.
|
||||
# `./gitea-runner config init` writes a config file to copy the lines you change into.
|
||||
|
||||
# Logging for the runner process itself (messages printed to stderr).
|
||||
# This does not control how workflow step output is streamed to the Gitea UI;
|
||||
@@ -9,92 +7,92 @@
|
||||
log:
|
||||
# logrus severity: trace, debug, info, warn, error, fatal, panic.
|
||||
# trace and debug turn on caller/file:line in log lines. Default if omitted: info.
|
||||
level: info
|
||||
#level: info
|
||||
|
||||
runner:
|
||||
# Where to store the registration result.
|
||||
file: .runner
|
||||
#file: .runner
|
||||
# Execute how many tasks concurrently at the same time.
|
||||
# With `container.network` empty, each concurrent docker job takes a subnet from the
|
||||
# daemon's address pool, so a high capacity can exhaust it. See `default-address-pools`
|
||||
# in the docker daemon config.
|
||||
capacity: 1
|
||||
#capacity: 1
|
||||
# Extra environment variables to run jobs.
|
||||
envs:
|
||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
#envs:
|
||||
# A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
# A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
# Extra environment variables to run jobs from a file.
|
||||
# It will be ignored if it's empty or the file doesn't exist.
|
||||
env_file: .env
|
||||
#env_file: .env
|
||||
# The timeout for a job to be finished.
|
||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||
# So the job could be stopped by the Gitea instance if its timeout is shorter than this.
|
||||
timeout: 3h
|
||||
#timeout: 3h
|
||||
# The timeout for the runner to wait for running jobs to finish when shutting down.
|
||||
# Any running jobs that haven't finished after this timeout will be cancelled.
|
||||
shutdown_timeout: 0s
|
||||
#shutdown_timeout: 0s
|
||||
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||
insecure: false
|
||||
#insecure: false
|
||||
# The timeout for fetching the job from the Gitea instance.
|
||||
fetch_timeout: 5s
|
||||
#fetch_timeout: 5s
|
||||
# The interval for fetching the job from the Gitea instance.
|
||||
fetch_interval: 2s
|
||||
#fetch_interval: 2s
|
||||
# The maximum interval for fetching the job from the Gitea instance.
|
||||
# The runner uses exponential backoff when idle, increasing the interval up to this maximum.
|
||||
# Set to 0 or same as fetch_interval to disable backoff.
|
||||
fetch_interval_max: 5s
|
||||
#fetch_interval_max: 5s
|
||||
# While idle, remove stale bind-workdir task directories and orphaned host-mode
|
||||
# scratch directories (left behind when a host cleanup delete stalls) older than
|
||||
# this duration. Setting either workdir_cleanup_age or idle_cleanup_interval to 0
|
||||
# (or any non-positive value) disables stale-directory cleanup entirely, along with
|
||||
# the docker network cleanup below.
|
||||
workdir_cleanup_age: 24h
|
||||
#workdir_cleanup_age: 24h
|
||||
# Cadence for the idle cleanup pass. Besides the directories above, on runners that use
|
||||
# docker it removes the per-job networks of jobs this runner did not live to tear down,
|
||||
# which would otherwise hold a subnet of the daemon address pool until the host is rebuilt.
|
||||
idle_cleanup_interval: 10m
|
||||
#idle_cleanup_interval: 10m
|
||||
# The base interval for periodic log flush to the Gitea instance.
|
||||
# Logs may be sent earlier if the buffer reaches log_report_batch_size
|
||||
# or if log_report_max_latency expires after the first buffered row.
|
||||
log_report_interval: 5s
|
||||
#log_report_interval: 5s
|
||||
# The maximum time a log row can wait before being sent.
|
||||
# This ensures even a single log line appears on the frontend within this duration.
|
||||
# Must be less than log_report_interval to have any effect.
|
||||
log_report_max_latency: 3s
|
||||
#log_report_max_latency: 3s
|
||||
# Flush logs immediately when the buffer reaches this many rows.
|
||||
# This ensures bursty output (e.g., npm install) is delivered promptly.
|
||||
log_report_batch_size: 100
|
||||
#log_report_batch_size: 100
|
||||
# The interval for reporting task state (step status, timing) to the Gitea instance.
|
||||
# State is also reported immediately on step transitions (start/stop).
|
||||
state_report_interval: 5s
|
||||
#state_report_interval: 5s
|
||||
# Per-attempt deadline for flushing the final logs and task state when a job
|
||||
# finishes, on a detached context so a server cancel can't block the acknowledgement.
|
||||
report_close_timeout: 10s
|
||||
#report_close_timeout: 10s
|
||||
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
|
||||
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
|
||||
# and github_mirror is not empty. In this case,
|
||||
# it replaces https://github.com with the value here, which is useful for some special network environments.
|
||||
github_mirror: ''
|
||||
#github_mirror: ''
|
||||
# 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
|
||||
#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
|
||||
#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 .
|
||||
# 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.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
#labels:
|
||||
# - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
# - "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||
# - "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
# Allocate a pseudo-TTY for each step's process. Applies to both host and docker backends.
|
||||
# Default false matches GitHub 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.
|
||||
allocate_pty: false
|
||||
#allocate_pty: false
|
||||
# Optional executable on the host, run once after each task's built-in cleanup
|
||||
# (post-steps, container teardown, bind-workdir removal). Additive only.
|
||||
#
|
||||
@@ -107,24 +105,24 @@ runner:
|
||||
# Windows: use .exe, .bat, or .cmd. PowerShell (.ps1) is not supported yet as
|
||||
# the configured path; wrap PowerShell commands in a .cmd file instead.
|
||||
# Full guide: docs/post-task-script.md
|
||||
post_task_script: ''
|
||||
#post_task_script: ''
|
||||
# Hard limit on post_task_script runtime. Default if omitted: 5m.
|
||||
post_task_script_timeout: 5m
|
||||
#post_task_script_timeout: 5m
|
||||
# Scripts run inside the job environment before the job's first step and after its last
|
||||
# one, the equivalent of GitHub's ACTIONS_RUNNER_HOOK_JOB_STARTED and
|
||||
# ACTIONS_RUNNER_HOOK_JOB_COMPLETED, which are read when these are unset. The paths are
|
||||
# resolved inside the job environment. Either one failing fails the job.
|
||||
# Full guide: docs/job-hooks.md
|
||||
hooks:
|
||||
job_started: ''
|
||||
job_completed: ''
|
||||
#hooks:
|
||||
# job_started: ''
|
||||
# job_completed: ''
|
||||
|
||||
cache:
|
||||
# Enable the built-in cache server (used by actions/cache and similar actions).
|
||||
enabled: true
|
||||
#enabled: true
|
||||
# Directory where cache blobs are stored on disk. Default: $HOME/.cache/actcache
|
||||
# Ignored when external_server is set.
|
||||
dir: ""
|
||||
#dir: ""
|
||||
# Outbound IP or hostname that job containers use to reach this runner's cache server.
|
||||
# Leave empty to detect automatically. 0.0.0.0 is not valid here.
|
||||
# If the runner itself runs in Docker, automatic detection can choose an
|
||||
@@ -133,33 +131,33 @@ cache:
|
||||
# to a hostname/IP reachable from job containers, and set port to a fixed
|
||||
# published port or put the job containers on a shared Docker network.
|
||||
# Ignored when external_server is set.
|
||||
host: ""
|
||||
#host: ""
|
||||
# Port for the built-in cache server. 0 picks a random free port.
|
||||
# Ignored when external_server is set.
|
||||
port: 0
|
||||
#port: 0
|
||||
# URL of a shared `gitea-runner cache-server` to use instead of starting a local one.
|
||||
# Set on every runner that should share a cache pool. A trailing slash is optional.
|
||||
# Example: "http://cache-host:8088/"
|
||||
# Requires external_secret (below) to match the value on the cache-server.
|
||||
external_server: ""
|
||||
#external_server: ""
|
||||
# Shared secret between this runner and the external cache-server.
|
||||
# Required when external_server is set. Must be identical on every runner and the cache-server.
|
||||
# Generate with: openssl rand -hex 32
|
||||
external_secret: ""
|
||||
#external_secret: ""
|
||||
# Path to a file containing the shared secret, as an alternative to external_secret.
|
||||
# Use this to keep the secret out of this file.
|
||||
# Surrounding whitespace is trimmed, so a trailing newline in the file is fine.
|
||||
# Setting both external_secret and external_secret_file is an error.
|
||||
external_secret_file: ""
|
||||
#external_secret_file: ""
|
||||
# When true, reuse a cached action instead of fetching from the remote on every job.
|
||||
# A moved tag (e.g. a re-tagged "v6") or an updated branch stays at the cached commit
|
||||
# until its cache entry expires or is manually removed.
|
||||
offline_mode: false
|
||||
#offline_mode: false
|
||||
# Serve the actions cache service v2 API, used by actions/cache@v4.2 and later. Those actions
|
||||
# refuse any host they do not take for GitHub, so reaching it means editing that check out of
|
||||
# the action's own bundle, keeping the untouched copy beside it. The same edit lets the stock
|
||||
# upload-artifact and download-artifact work here. A bundle that does not match is left alone.
|
||||
v2: true
|
||||
#v2: true
|
||||
|
||||
container:
|
||||
# Specifies the network to which the container will connect.
|
||||
@@ -168,31 +166,31 @@ container:
|
||||
# For dockerized runners using the built-in cache server, a custom shared
|
||||
# network can be required so job containers can reach cache.host/cache.port.
|
||||
# Deprecated: `network_mode` is still accepted for old configs; use `network` instead.
|
||||
network: ""
|
||||
#network: ""
|
||||
# network_create_options only apply when `network` is left empty and the runner
|
||||
# auto-creates a per-job network that does not already exist. They have no effect
|
||||
# when a custom `network` name is set, because that network is used as-is and never
|
||||
# created by the runner. Omit the entire block to use Docker's defaults. An auto-created
|
||||
# network is labelled com.gitea.runner.uuid=<this runner's uuid>, which is how the idle
|
||||
# cleanup tells its own leftovers apart from those of other runners on the same daemon.
|
||||
network_create_options:
|
||||
enable_ipv4: true # Omit to use Docker's default (IPv4 enabled). Set false to disable IPv4.
|
||||
enable_ipv6: false # Omit to use Docker's default (IPv6 disabled). Enabling it requires dockerd started with --ipv6.
|
||||
#network_create_options:
|
||||
# enable_ipv4: true # Omit to use Docker's default (IPv4 enabled). Set false to disable IPv4.
|
||||
# enable_ipv6: false # Omit to use Docker's default (IPv6 disabled). Enabling it requires dockerd started with --ipv6.
|
||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||
privileged: false
|
||||
#privileged: false
|
||||
# Any other options to be used when the container is started, for example:
|
||||
# options: --add-host=my.gitea.url:host-gateway
|
||||
# A volume declared here replaces the one the runner mounts on the same container path, so the
|
||||
# tool cache can be kept on the host. Its source must also be allowed by valid_volumes below:
|
||||
# options: --volume /host/toolcache:/opt/hostedtoolcache
|
||||
options:
|
||||
#options:
|
||||
# The parent directory of a job's working directory.
|
||||
# NOTE: There is no need to add the first '/' of the path as runner will add it automatically.
|
||||
# If the path starts with '/', the '/' will be trimmed.
|
||||
# For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
|
||||
# If it's empty, /workspace will be used.
|
||||
# Purely numeric subdirectories under this path are reserved for task workspaces and may be removed by idle cleanup.
|
||||
workdir_parent:
|
||||
#workdir_parent:
|
||||
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
|
||||
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
|
||||
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
|
||||
@@ -202,62 +200,62 @@ container:
|
||||
# If you want to allow any volume, please use the following configuration:
|
||||
# valid_volumes:
|
||||
# - '**'
|
||||
valid_volumes: []
|
||||
#valid_volumes: []
|
||||
# Overrides the docker client host with the specified one.
|
||||
# If it's empty, runner will find an available docker host automatically.
|
||||
# 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: ""
|
||||
#docker_host: ""
|
||||
# Pull docker image(s) even if already present.
|
||||
# Defaults to false when the key is omitted.
|
||||
# Two exceptions: an image pinned by digest (image@sha256:...) cannot change, so it is never
|
||||
# re-pulled, and a pull that fails while a copy is already on the host does not fail the job,
|
||||
# which runs on that copy with a warning in its log.
|
||||
force_pull: false
|
||||
#force_pull: false
|
||||
# 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
|
||||
require_docker: false
|
||||
#require_docker: false
|
||||
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or runner
|
||||
docker_timeout: 0s
|
||||
#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
|
||||
#bind_workdir: false
|
||||
# How long a job waits for a service container that declares a healthcheck to become
|
||||
# healthy. A negative value (e.g. -1s) starts the steps without waiting.
|
||||
service_ready_timeout: 5m
|
||||
#service_ready_timeout: 5m
|
||||
|
||||
host:
|
||||
# The parent directory of a job's working directory.
|
||||
# If it's empty, $HOME/.cache/act/ will be used.
|
||||
workdir_parent:
|
||||
#workdir_parent:
|
||||
|
||||
# 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.
|
||||
health_check:
|
||||
enabled: false
|
||||
#enabled: false
|
||||
# Minimum free space required on the filesystem holding runner workspaces.
|
||||
# Defaults to 1024 MiB when omitted or set to zero.
|
||||
min_free_disk_space_mb: 1024
|
||||
#min_free_disk_space_mb: 1024
|
||||
# Optional additional executable. A non-zero exit, timeout, or startup failure
|
||||
# marks the runner unavailable.
|
||||
script: ''
|
||||
#script: ''
|
||||
# How long a script result is cached and its maximum execution time.
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
#interval: 30s
|
||||
#timeout: 10s
|
||||
|
||||
metrics:
|
||||
# Enable the Prometheus metrics endpoint.
|
||||
# When enabled, metrics are served at /metrics, liveness at /healthz, and
|
||||
# task-admission readiness at /readyz.
|
||||
enabled: false
|
||||
#enabled: false
|
||||
# The address for the metrics HTTP server to listen on.
|
||||
# Defaults to localhost only. Set to ":9101" to allow external access,
|
||||
# but ensure the port is firewall-protected as there is no authentication.
|
||||
addr: "127.0.0.1:9101"
|
||||
#addr: "127.0.0.1:9101"
|
||||
# Consecutive polling failures may last this long before /readyz returns 503.
|
||||
readiness_grace: 30s
|
||||
#readiness_grace: 30s
|
||||
|
||||
@@ -24,6 +24,12 @@ import (
|
||||
// (so a programmatically built config still gets a sane bound).
|
||||
const DefaultPostTaskScriptTimeout = 5 * time.Minute
|
||||
|
||||
// Minimal is the smallest config file that runs the runner: options it does not
|
||||
// name keep their default, and it names none.
|
||||
const Minimal = `# Minimal config file. Every option it does not set keeps its default.
|
||||
# "gitea-runner config generate" prints all options, "config set <key> <value>" sets one here.
|
||||
`
|
||||
|
||||
// Log represents the configuration for logging.
|
||||
type Log struct {
|
||||
Level string `yaml:"level"` // Level indicates the logging level.
|
||||
|
||||
@@ -348,12 +348,23 @@ cache:
|
||||
assert.Contains(t, err.Error(), "contains no secret")
|
||||
}
|
||||
|
||||
// The shipped example must parse, and every key in it must be one the config knows.
|
||||
func TestLoadDefault_ExampleConfigParses(t *testing.T) {
|
||||
// The shipped configs must parse, hold no key the config does not know, and leave
|
||||
// every option at its default, as all of their values are commented out.
|
||||
func TestLoadDefault_ShippedConfigsChangeNothing(t *testing.T) {
|
||||
hook := test.NewGlobal()
|
||||
defer hook.Reset()
|
||||
|
||||
_, err := LoadDefault("config.example.yaml")
|
||||
defaults, err := LoadDefault("")
|
||||
require.NoError(t, err)
|
||||
|
||||
dir := t.TempDir()
|
||||
for name, content := range map[string][]byte{"config.example.yaml": Example, "minimal.yaml": []byte(Minimal)} {
|
||||
file := filepath.Join(dir, name)
|
||||
require.NoError(t, os.WriteFile(file, content, 0o600))
|
||||
|
||||
cfg, err := LoadDefault(file)
|
||||
require.NoError(t, err, name)
|
||||
assert.Equal(t, defaults, cfg, name)
|
||||
}
|
||||
assert.Empty(t, hook.AllEntries())
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ type editSession struct {
|
||||
root *yaml.Node
|
||||
field *fieldInfo
|
||||
segments []string
|
||||
preamble []byte // text of a file that holds no YAML node, which the encoder cannot give back
|
||||
}
|
||||
|
||||
// loadForEdit validates the path and parses the file, so every caller fails before anything is written.
|
||||
@@ -176,7 +177,7 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("config file %q does not exist, create one with `config generate`", file)
|
||||
return nil, fmt.Errorf("config file %q does not exist, create one with `config init`", file)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +186,9 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
if err := yaml.Unmarshal(content, &root); err != nil {
|
||||
return nil, fmt.Errorf("parse config file %q: %w", file, err)
|
||||
}
|
||||
var preamble []byte
|
||||
if root.Kind == 0 || len(root.Content) == 0 {
|
||||
preamble = bytes.TrimSpace(content) // all the file has is comments
|
||||
root = yaml.Node{
|
||||
Kind: yaml.DocumentNode,
|
||||
Content: []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}},
|
||||
@@ -195,7 +198,7 @@ func loadForEdit(file, path string) (*editSession, error) {
|
||||
return nil, fmt.Errorf("config file %q is not a YAML mapping", file)
|
||||
}
|
||||
|
||||
return &editSession{file: file, path: path, original: content, root: &root, field: field, segments: segments}, nil
|
||||
return &editSession{file: file, path: path, original: content, root: &root, field: field, segments: segments, preamble: preamble}, nil
|
||||
}
|
||||
|
||||
func loadSequenceEdit(file, path string, values []string) (*editSession, error) {
|
||||
@@ -319,16 +322,28 @@ func encodeYAML(node *yaml.Node) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// restoreBlankLines re-inserts the blank lines between top-level sections that the encoder drops.
|
||||
func restoreBlankLines(original, generated []byte) []byte {
|
||||
// restoreLayout re-applies the spacing the encoder drops: the blank lines between
|
||||
// top-level sections, and the indentation of comments, which the encoder emits at the
|
||||
// indentation of the node it attached them to rather than the one they were written at.
|
||||
func restoreLayout(original, generated []byte) []byte {
|
||||
type comment struct {
|
||||
line string // as written, indentation included
|
||||
trimmed string
|
||||
blankBefore bool
|
||||
}
|
||||
|
||||
var comments []comment
|
||||
spaced := map[string]bool{}
|
||||
blank := false
|
||||
for line := range strings.Lines(string(original)) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.TrimSpace(line) == "":
|
||||
case trimmed == "":
|
||||
blank = true
|
||||
case strings.HasPrefix(line, "#"): // the block belongs to the key below it
|
||||
case strings.HasPrefix(trimmed, "#"):
|
||||
comments = append(comments, comment{line: line, trimmed: trimmed, blankBefore: blank})
|
||||
blank = false
|
||||
default:
|
||||
if key, ok := topLevelKey(line); ok && blank {
|
||||
spaced[key] = true
|
||||
@@ -338,16 +353,26 @@ func restoreBlankLines(original, generated []byte) []byte {
|
||||
}
|
||||
|
||||
var out []string
|
||||
appendBlank := func() {
|
||||
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
|
||||
out = append(out, "")
|
||||
}
|
||||
}
|
||||
for line := range strings.Lines(string(generated)) {
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
if key, ok := topLevelKey(line); ok && spaced[key] {
|
||||
start := len(out)
|
||||
for start > 0 && strings.HasPrefix(out[start-1], "#") {
|
||||
start--
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
if i := slices.IndexFunc(comments, func(c comment) bool { return c.trimmed == trimmed }); i >= 0 {
|
||||
if comments[i].blankBefore {
|
||||
appendBlank()
|
||||
} else if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) == "" {
|
||||
out = out[:len(out)-1] // the encoder separates a comment block it moved
|
||||
}
|
||||
if start > 0 && strings.TrimSpace(out[start-1]) != "" {
|
||||
out = slices.Insert(out, start, "")
|
||||
line = comments[i].line
|
||||
comments = comments[i+1:] // the encoder keeps their order, so earlier ones cannot match again
|
||||
}
|
||||
} else if key, ok := topLevelKey(line); ok && spaced[key] {
|
||||
appendBlank()
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
@@ -377,12 +402,21 @@ func (s *editSession) write() error {
|
||||
return fmt.Errorf("the edit would produce a config the runner cannot load: %w", err)
|
||||
}
|
||||
|
||||
content := restoreBlankLines(s.original, generated)
|
||||
if len(s.preamble) > 0 { // before restoreLayout, so that it spaces the preamble too
|
||||
generated = slices.Concat(s.preamble, []byte("\n"), generated)
|
||||
}
|
||||
|
||||
content := restoreLayout(s.original, generated)
|
||||
if bytes.Contains(s.original, []byte("\r\n")) { // the encoder only ever emits LF
|
||||
content = bytes.ReplaceAll(content, []byte("\n"), []byte("\r\n"))
|
||||
}
|
||||
|
||||
file := s.file
|
||||
return WriteFile(s.file, content)
|
||||
}
|
||||
|
||||
// WriteFile replaces the config file in one step, keeping the mode and owner of the
|
||||
// file it replaces, so a half-written config never reaches a runner reading it.
|
||||
func WriteFile(file string, content []byte) error {
|
||||
if resolved, err := filepath.EvalSymlinks(file); err == nil {
|
||||
file = resolved // keeps a config linked in from elsewhere intact
|
||||
}
|
||||
|
||||
@@ -252,16 +252,39 @@ func TestEditValuesFileHandling(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// The example config is the file users edit, so it has to stay written the way
|
||||
// the encoder emits it, down to the single space before a trailing comment.
|
||||
func TestEditValuesKeepsExampleConfigIntact(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(file, Example, 0o600))
|
||||
// An edit has to give the file back unchanged around it, down to the indentation of
|
||||
// a commented-out option, as that documentation is what the user reads and edits.
|
||||
func TestEditValuesPreservesFileText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content []byte
|
||||
edit func(file string) error
|
||||
added string // the only text the edit may add
|
||||
}{
|
||||
{
|
||||
name: "example config",
|
||||
content: Example,
|
||||
edit: func(file string) error { return AddValue(file, "runner.labels", "ubuntu:docker://node:22") },
|
||||
added: " labels:\n - ubuntu:docker://node:22\n",
|
||||
},
|
||||
{
|
||||
name: "minimal config",
|
||||
content: []byte(Minimal),
|
||||
edit: func(file string) error { return SetValue(file, "runner.capacity", "4") },
|
||||
added: "runner:\n capacity: 4\n",
|
||||
},
|
||||
}
|
||||
|
||||
require.NoError(t, AddValue(file, "runner.labels", "ubuntu:docker://node:22"))
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(file, tc.content, 0o600))
|
||||
|
||||
require.NoError(t, tc.edit(file))
|
||||
|
||||
content, err := os.ReadFile(file)
|
||||
require.NoError(t, err)
|
||||
withoutAdded := strings.Replace(string(content), " - ubuntu:docker://node:22\n", "", 1)
|
||||
assert.Equal(t, string(Example), withoutAdded, "only the appended label may differ")
|
||||
assert.Equal(t, string(tc.content), strings.Replace(string(content), tc.added, "", 1))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user