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
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
check-latest: true
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
|
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ jobs:
|
|||||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
|
check-latest: true
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ jobs:
|
|||||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||||
with:
|
with:
|
||||||
go-version-file: 'go.mod'
|
go-version-file: 'go.mod'
|
||||||
|
check-latest: true
|
||||||
- name: prepare anonymous docker config
|
- name: prepare anonymous docker config
|
||||||
run: mkdir -p "$DOCKER_CONFIG" && echo '{}' > "$DOCKER_CONFIG/config.json"
|
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`;
|
# 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
|
.PHONY: tidy
|
||||||
tidy: ## run go mod 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
|
$(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
|
.PHONY: tidy-check
|
||||||
tidy-check: tidy
|
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
|
### 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
|
```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`.
|
`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.
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
#### Editing a config file
|
#### 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
|
```bash
|
||||||
./gitea-runner -c config.yaml config set runner.capacity 4
|
./gitea-runner config set runner.capacity 4
|
||||||
./gitea-runner -c config.yaml config set runner.timeout 90m # written as 1h30m0s
|
./gitea-runner config set runner.timeout 90m # written as 1h30m0s
|
||||||
./gitea-runner -c config.yaml config set runner.envs.MY_VAR value
|
./gitea-runner config set runner.envs.MY_VAR value
|
||||||
./gitea-runner -c config.yaml config add runner.labels 'ubuntu:docker://node:22'
|
./gitea-runner config add runner.labels 'ubuntu:docker://node:22'
|
||||||
./gitea-runner -c config.yaml config remove runner.labels 'ubuntu:docker://node:22'
|
./gitea-runner config remove runner.labels 'ubuntu:docker://node:22'
|
||||||
./gitea-runner -c config.yaml config get runner.labels
|
./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 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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Labels
|
### 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
|
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:
|
`.runner` file ends up in the working directory:
|
||||||
|
|
||||||
```bash
|
```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
|
cd /var/lib/gitea-runner
|
||||||
sudo -u gitea-runner gitea-runner register --config /etc/gitea-runner/config.yaml
|
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
|
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
|
```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:
|
- 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
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.20.0
|
connectrpc.com/connect v1.20.0
|
||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func loadConfigCmd(configFile *string) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configCmd.AddCommand(loadGenerateConfigCmd("generate"))
|
configCmd.AddCommand(loadGenerateConfigCmd("generate"))
|
||||||
|
configCmd.AddCommand(loadInitConfigCmd(configFile))
|
||||||
|
|
||||||
configCmd.AddCommand(&cobra.Command{
|
configCmd.AddCommand(&cobra.Command{
|
||||||
Use: "get <key>",
|
Use: "get <key>",
|
||||||
@@ -73,10 +74,38 @@ func loadConfigCmd(configFile *string) *cobra.Command {
|
|||||||
return configCmd
|
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 {
|
func loadGenerateConfigCmd(use string) *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: use,
|
Use: use,
|
||||||
Short: "Generate an example config file",
|
Short: "Print the example config, which documents every option",
|
||||||
Args: cobra.MaximumNArgs(0),
|
Args: cobra.MaximumNArgs(0),
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), "%s", config.Example)
|
fmt.Fprintf(cmd.OutOrStdout(), "%s", config.Example)
|
||||||
|
|||||||
@@ -32,6 +32,30 @@ func TestConfigCmdGeneratePrintsTheExample(t *testing.T) {
|
|||||||
assert.Equal(t, string(config.Example), out)
|
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.
|
// The subcommands only wire arguments through, so one pass over all of them is enough.
|
||||||
func TestConfigCmdEditsTheFile(t *testing.T) {
|
func TestConfigCmdEditsTheFile(t *testing.T) {
|
||||||
file := filepath.Join(t.TempDir(), "config.yaml")
|
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.
|
# 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.
|
||||||
# You don't have to copy this file to your instance,
|
|
||||||
# just run `./gitea-runner config generate > config.yaml` to generate a config file.
|
|
||||||
|
|
||||||
# Logging for the runner process itself (messages printed to stderr).
|
# Logging for the runner process itself (messages printed to stderr).
|
||||||
# This does not control how workflow step output is streamed to the Gitea UI;
|
# This does not control how workflow step output is streamed to the Gitea UI;
|
||||||
@@ -9,92 +7,92 @@
|
|||||||
log:
|
log:
|
||||||
# logrus severity: trace, debug, info, warn, error, fatal, panic.
|
# logrus severity: trace, debug, info, warn, error, fatal, panic.
|
||||||
# trace and debug turn on caller/file:line in log lines. Default if omitted: info.
|
# trace and debug turn on caller/file:line in log lines. Default if omitted: info.
|
||||||
level: info
|
#level: info
|
||||||
|
|
||||||
runner:
|
runner:
|
||||||
# Where to store the registration result.
|
# Where to store the registration result.
|
||||||
file: .runner
|
#file: .runner
|
||||||
# Execute how many tasks concurrently at the same time.
|
# Execute how many tasks concurrently at the same time.
|
||||||
# With `container.network` empty, each concurrent docker job takes a subnet from the
|
# 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`
|
# daemon's address pool, so a high capacity can exhaust it. See `default-address-pools`
|
||||||
# in the docker daemon config.
|
# in the docker daemon config.
|
||||||
capacity: 1
|
#capacity: 1
|
||||||
# Extra environment variables to run jobs.
|
# Extra environment variables to run jobs.
|
||||||
envs:
|
#envs:
|
||||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
# A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
# A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||||
# Extra environment variables to run jobs from a file.
|
# Extra environment variables to run jobs from a file.
|
||||||
# It will be ignored if it's empty or the file doesn't exist.
|
# 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.
|
# The timeout for a job to be finished.
|
||||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
# 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.
|
# 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.
|
# 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.
|
# 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.
|
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||||
insecure: false
|
#insecure: false
|
||||||
# The timeout for fetching the job from the Gitea instance.
|
# 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.
|
# 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 maximum interval for fetching the job from the Gitea instance.
|
||||||
# The runner uses exponential backoff when idle, increasing the interval up to this maximum.
|
# 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.
|
# 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
|
# While idle, remove stale bind-workdir task directories and orphaned host-mode
|
||||||
# scratch directories (left behind when a host cleanup delete stalls) older than
|
# 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
|
# 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
|
# (or any non-positive value) disables stale-directory cleanup entirely, along with
|
||||||
# the docker network cleanup below.
|
# 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
|
# 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,
|
# 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.
|
# 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.
|
# The base interval for periodic log flush to the Gitea instance.
|
||||||
# Logs may be sent earlier if the buffer reaches log_report_batch_size
|
# 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.
|
# 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.
|
# 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.
|
# 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.
|
# 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.
|
# Flush logs immediately when the buffer reaches this many rows.
|
||||||
# This ensures bursty output (e.g., npm install) is delivered promptly.
|
# 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.
|
# The interval for reporting task state (step status, timing) to the Gitea instance.
|
||||||
# State is also reported immediately on step transitions (start/stop).
|
# 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
|
# 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.
|
# 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.
|
# 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,
|
# 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,
|
# 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.
|
# 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.
|
# 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.
|
# 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 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.
|
# 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 .
|
||||||
# If it's empty when registering, it will ask for inputting labels.
|
# 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.
|
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
||||||
labels:
|
#labels:
|
||||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
# - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
# - "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.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.
|
# 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
|
# 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
|
# terminal; tools like `docker build` emit redrawing progress frames into the captured log
|
||||||
# when a TTY is present.
|
# 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
|
# Optional executable on the host, run once after each task's built-in cleanup
|
||||||
# (post-steps, container teardown, bind-workdir removal). Additive only.
|
# (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
|
# Windows: use .exe, .bat, or .cmd. PowerShell (.ps1) is not supported yet as
|
||||||
# the configured path; wrap PowerShell commands in a .cmd file instead.
|
# the configured path; wrap PowerShell commands in a .cmd file instead.
|
||||||
# Full guide: docs/post-task-script.md
|
# Full guide: docs/post-task-script.md
|
||||||
post_task_script: ''
|
#post_task_script: ''
|
||||||
# Hard limit on post_task_script runtime. Default if omitted: 5m.
|
# 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
|
# 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
|
# 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
|
# 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.
|
# resolved inside the job environment. Either one failing fails the job.
|
||||||
# Full guide: docs/job-hooks.md
|
# Full guide: docs/job-hooks.md
|
||||||
hooks:
|
#hooks:
|
||||||
job_started: ''
|
# job_started: ''
|
||||||
job_completed: ''
|
# job_completed: ''
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
# Enable the built-in cache server (used by actions/cache and similar actions).
|
# 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
|
# Directory where cache blobs are stored on disk. Default: $HOME/.cache/actcache
|
||||||
# Ignored when external_server is set.
|
# Ignored when external_server is set.
|
||||||
dir: ""
|
#dir: ""
|
||||||
# Outbound IP or hostname that job containers use to reach this runner's cache server.
|
# 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.
|
# 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
|
# 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
|
# 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.
|
# published port or put the job containers on a shared Docker network.
|
||||||
# Ignored when external_server is set.
|
# Ignored when external_server is set.
|
||||||
host: ""
|
#host: ""
|
||||||
# Port for the built-in cache server. 0 picks a random free port.
|
# Port for the built-in cache server. 0 picks a random free port.
|
||||||
# Ignored when external_server is set.
|
# 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.
|
# 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.
|
# Set on every runner that should share a cache pool. A trailing slash is optional.
|
||||||
# Example: "http://cache-host:8088/"
|
# Example: "http://cache-host:8088/"
|
||||||
# Requires external_secret (below) to match the value on the cache-server.
|
# 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.
|
# 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.
|
# Required when external_server is set. Must be identical on every runner and the cache-server.
|
||||||
# Generate with: openssl rand -hex 32
|
# Generate with: openssl rand -hex 32
|
||||||
external_secret: ""
|
#external_secret: ""
|
||||||
# Path to a file containing the shared secret, as an alternative to 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.
|
# Use this to keep the secret out of this file.
|
||||||
# Surrounding whitespace is trimmed, so a trailing newline in the file is fine.
|
# Surrounding whitespace is trimmed, so a trailing newline in the file is fine.
|
||||||
# Setting both external_secret and external_secret_file is an error.
|
# 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.
|
# 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
|
# 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.
|
# 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
|
# 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
|
# 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
|
# 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.
|
# upload-artifact and download-artifact work here. A bundle that does not match is left alone.
|
||||||
v2: true
|
#v2: true
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# Specifies the network to which the container will connect.
|
# 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
|
# 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.
|
# 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.
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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.
|
# cleanup tells its own leftovers apart from those of other runners on the same daemon.
|
||||||
network_create_options:
|
#network_create_options:
|
||||||
enable_ipv4: true # Omit to use Docker's default (IPv4 enabled). Set false to disable IPv4.
|
# 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.
|
# 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).
|
# 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:
|
# Any other options to be used when the container is started, for example:
|
||||||
# options: --add-host=my.gitea.url:host-gateway
|
# 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
|
# 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:
|
# 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: --volume /host/toolcache:/opt/hostedtoolcache
|
||||||
options:
|
#options:
|
||||||
# The parent directory of a job's working directory.
|
# 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.
|
# 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.
|
# 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
|
# 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.
|
# 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.
|
# 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
|
# 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.
|
# 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:
|
# 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:
|
# If you want to allow any volume, please use the following configuration:
|
||||||
# valid_volumes:
|
# valid_volumes:
|
||||||
# - '**'
|
# - '**'
|
||||||
valid_volumes: []
|
#valid_volumes: []
|
||||||
# Overrides the docker client host with the specified one.
|
# Overrides the docker client host with the specified one.
|
||||||
# If it's empty, runner will find an available docker host automatically.
|
# 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 "-", 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.
|
||||||
# Defaults to false when the key is omitted.
|
# Defaults to false when the key is omitted.
|
||||||
# Two exceptions: an image pinned by digest (image@sha256:...) cannot change, so it is never
|
# 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,
|
# 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.
|
# 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
|
# 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
|
||||||
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
|
# 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.
|
# 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
|
# 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
|
# 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
|
# 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.
|
# 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
|
# 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.
|
# healthy. A negative value (e.g. -1s) starts the steps without waiting.
|
||||||
service_ready_timeout: 5m
|
#service_ready_timeout: 5m
|
||||||
|
|
||||||
host:
|
host:
|
||||||
# The parent directory of a job's working directory.
|
# The parent directory of a job's working directory.
|
||||||
# If it's empty, $HOME/.cache/act/ will be used.
|
# 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
|
# Optional local task-admission checks. Disabled by default. When enabled, low
|
||||||
# disk space or a failing script pauses new task fetching; existing jobs continue.
|
# 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.
|
# No health checks run while any job is active; the last result is reused until idle.
|
||||||
health_check:
|
health_check:
|
||||||
enabled: false
|
#enabled: false
|
||||||
# Minimum free space required on the filesystem holding runner workspaces.
|
# Minimum free space required on the filesystem holding runner workspaces.
|
||||||
# Defaults to 1024 MiB when omitted or set to zero.
|
# 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
|
# Optional additional executable. A non-zero exit, timeout, or startup failure
|
||||||
# marks the runner unavailable.
|
# marks the runner unavailable.
|
||||||
script: ''
|
#script: ''
|
||||||
# How long a script result is cached and its maximum execution time.
|
# How long a script result is cached and its maximum execution time.
|
||||||
interval: 30s
|
#interval: 30s
|
||||||
timeout: 10s
|
#timeout: 10s
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
# Enable the Prometheus metrics endpoint.
|
# Enable the Prometheus metrics endpoint.
|
||||||
# When enabled, metrics are served at /metrics, liveness at /healthz, and
|
# When enabled, metrics are served at /metrics, liveness at /healthz, and
|
||||||
# task-admission readiness at /readyz.
|
# task-admission readiness at /readyz.
|
||||||
enabled: false
|
#enabled: false
|
||||||
# The address for the metrics HTTP server to listen on.
|
# The address for the metrics HTTP server to listen on.
|
||||||
# Defaults to localhost only. Set to ":9101" to allow external access,
|
# Defaults to localhost only. Set to ":9101" to allow external access,
|
||||||
# but ensure the port is firewall-protected as there is no authentication.
|
# 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.
|
# 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).
|
// (so a programmatically built config still gets a sane bound).
|
||||||
const DefaultPostTaskScriptTimeout = 5 * time.Minute
|
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.
|
// Log represents the configuration for logging.
|
||||||
type Log struct {
|
type Log struct {
|
||||||
Level string `yaml:"level"` // Level indicates the logging level.
|
Level string `yaml:"level"` // Level indicates the logging level.
|
||||||
|
|||||||
@@ -348,12 +348,23 @@ cache:
|
|||||||
assert.Contains(t, err.Error(), "contains no secret")
|
assert.Contains(t, err.Error(), "contains no secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shipped example must parse, and every key in it must be one the config knows.
|
// The shipped configs must parse, hold no key the config does not know, and leave
|
||||||
func TestLoadDefault_ExampleConfigParses(t *testing.T) {
|
// every option at its default, as all of their values are commented out.
|
||||||
|
func TestLoadDefault_ShippedConfigsChangeNothing(t *testing.T) {
|
||||||
hook := test.NewGlobal()
|
hook := test.NewGlobal()
|
||||||
defer hook.Reset()
|
defer hook.Reset()
|
||||||
|
|
||||||
_, err := LoadDefault("config.example.yaml")
|
defaults, err := LoadDefault("")
|
||||||
require.NoError(t, err)
|
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())
|
assert.Empty(t, hook.AllEntries())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ type editSession struct {
|
|||||||
root *yaml.Node
|
root *yaml.Node
|
||||||
field *fieldInfo
|
field *fieldInfo
|
||||||
segments []string
|
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.
|
// 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)
|
content, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -185,7 +186,9 @@ func loadForEdit(file, path string) (*editSession, error) {
|
|||||||
if err := yaml.Unmarshal(content, &root); err != nil {
|
if err := yaml.Unmarshal(content, &root); err != nil {
|
||||||
return nil, fmt.Errorf("parse config file %q: %w", file, err)
|
return nil, fmt.Errorf("parse config file %q: %w", file, err)
|
||||||
}
|
}
|
||||||
|
var preamble []byte
|
||||||
if root.Kind == 0 || len(root.Content) == 0 {
|
if root.Kind == 0 || len(root.Content) == 0 {
|
||||||
|
preamble = bytes.TrimSpace(content) // all the file has is comments
|
||||||
root = yaml.Node{
|
root = yaml.Node{
|
||||||
Kind: yaml.DocumentNode,
|
Kind: yaml.DocumentNode,
|
||||||
Content: []*yaml.Node{{Kind: yaml.MappingNode, Tag: "!!map"}},
|
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 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) {
|
func loadSequenceEdit(file, path string, values []string) (*editSession, error) {
|
||||||
@@ -319,16 +322,28 @@ func encodeYAML(node *yaml.Node) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreBlankLines re-inserts the blank lines between top-level sections that the encoder drops.
|
// restoreLayout re-applies the spacing the encoder drops: the blank lines between
|
||||||
func restoreBlankLines(original, generated []byte) []byte {
|
// 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{}
|
spaced := map[string]bool{}
|
||||||
blank := false
|
blank := false
|
||||||
for line := range strings.Lines(string(original)) {
|
for line := range strings.Lines(string(original)) {
|
||||||
line = strings.TrimRight(line, "\r\n")
|
line = strings.TrimRight(line, "\r\n")
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
switch {
|
switch {
|
||||||
case strings.TrimSpace(line) == "":
|
case trimmed == "":
|
||||||
blank = true
|
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:
|
default:
|
||||||
if key, ok := topLevelKey(line); ok && blank {
|
if key, ok := topLevelKey(line); ok && blank {
|
||||||
spaced[key] = true
|
spaced[key] = true
|
||||||
@@ -338,16 +353,26 @@ func restoreBlankLines(original, generated []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out []string
|
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)) {
|
for line := range strings.Lines(string(generated)) {
|
||||||
line = strings.TrimRight(line, "\r\n")
|
line = strings.TrimRight(line, "\r\n")
|
||||||
if key, ok := topLevelKey(line); ok && spaced[key] {
|
trimmed := strings.TrimSpace(line)
|
||||||
start := len(out)
|
if strings.HasPrefix(trimmed, "#") {
|
||||||
for start > 0 && strings.HasPrefix(out[start-1], "#") {
|
if i := slices.IndexFunc(comments, func(c comment) bool { return c.trimmed == trimmed }); i >= 0 {
|
||||||
start--
|
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]) != "" {
|
line = comments[i].line
|
||||||
out = slices.Insert(out, start, "")
|
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)
|
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)
|
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
|
if bytes.Contains(s.original, []byte("\r\n")) { // the encoder only ever emits LF
|
||||||
content = bytes.ReplaceAll(content, []byte("\n"), []byte("\r\n"))
|
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 {
|
if resolved, err := filepath.EvalSymlinks(file); err == nil {
|
||||||
file = resolved // keeps a config linked in from elsewhere intact
|
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
|
// An edit has to give the file back unchanged around it, down to the indentation of
|
||||||
// the encoder emits it, down to the single space before a trailing comment.
|
// a commented-out option, as that documentation is what the user reads and edits.
|
||||||
func TestEditValuesKeepsExampleConfigIntact(t *testing.T) {
|
func TestEditValuesPreservesFileText(t *testing.T) {
|
||||||
file := filepath.Join(t.TempDir(), "config.yaml")
|
tests := []struct {
|
||||||
require.NoError(t, os.WriteFile(file, Example, 0o600))
|
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)
|
content, err := os.ReadFile(file)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
withoutAdded := strings.Replace(string(content), " - ubuntu:docker://node:22\n", "", 1)
|
assert.Equal(t, string(tc.content), strings.Replace(string(content), tc.added, "", 1))
|
||||||
assert.Equal(t, string(Example), withoutAdded, "only the appended label may differ")
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user