mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 08:54:21 +02:00
Compare commits
2 Commits
d6882b3df5
...
fix/563-pu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4034634340 | ||
|
|
4691a92ea8 |
@@ -43,18 +43,12 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant:
|
variant:
|
||||||
# The basic image is built from source and can target any arch the
|
|
||||||
# toolchain supports. The dind variants are limited to the arches the
|
|
||||||
# docker:dind base image publishes.
|
|
||||||
- target: basic
|
- target: basic
|
||||||
tag_suffix: ""
|
tag_suffix: ""
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x
|
|
||||||
- target: dind
|
- target: dind
|
||||||
tag_suffix: "-dind"
|
tag_suffix: "-dind"
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- target: dind-rootless
|
- target: dind-rootless
|
||||||
tag_suffix: "-dind-rootless"
|
tag_suffix: "-dind-rootless"
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -88,7 +82,9 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
target: ${{ matrix.variant.target }}
|
target: ${{ matrix.variant.target }}
|
||||||
platforms: ${{ matrix.variant.platforms }}
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||||
|
|||||||
@@ -42,18 +42,12 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
variant:
|
variant:
|
||||||
# The basic image is built from source and can target any arch the
|
|
||||||
# toolchain supports. The dind variants are limited to the arches the
|
|
||||||
# docker:dind base image publishes.
|
|
||||||
- target: basic
|
- target: basic
|
||||||
tag_suffix: ""
|
tag_suffix: ""
|
||||||
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/s390x
|
|
||||||
- target: dind
|
- target: dind
|
||||||
tag_suffix: "-dind"
|
tag_suffix: "-dind"
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
- target: dind-rootless
|
- target: dind-rootless
|
||||||
tag_suffix: "-dind-rootless"
|
tag_suffix: "-dind-rootless"
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
env:
|
env:
|
||||||
@@ -97,7 +91,9 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
target: ${{ matrix.variant.target }}
|
target: ${{ matrix.variant.target }}
|
||||||
platforms: ${{ matrix.variant.platforms }}
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -85,8 +85,6 @@ docker run -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRA
|
|||||||
|
|
||||||
Mount a volume on `/data` if you want the registration file and optional config to survive container recreation (see [scripts/run.sh](scripts/run.sh)).
|
Mount a volume on `/data` if you want the registration file and optional config to survive container recreation (see [scripts/run.sh](scripts/run.sh)).
|
||||||
|
|
||||||
> **`/data` does not hold the image cache.** It is the runner's working directory and contains only the `.runner` registration file and, optionally, your config file. Images pulled for jobs live in the *Docker daemon's* data root, which for the `dind` flavours is inside the container (`/var/lib/docker`, or `/home/rootless/.local/share/docker` for `dind-rootless`). To keep the image cache across restarts, give that path its own volume as well — otherwise every new container re-pulls the job images. With the `basic` flavour the images live on whichever daemon you point the runner at, so there is nothing extra to persist.
|
|
||||||
|
|
||||||
### Image flavours
|
### Image flavours
|
||||||
|
|
||||||
The image is published in three flavours, all built from the single multi-stage [Dockerfile](Dockerfile) in this repository. They differ only in how a Docker daemon is made available to the jobs the runner executes; the `gitea-runner` binary inside them is identical.
|
The image is published in three flavours, all built from the single multi-stage [Dockerfile](Dockerfile) in this repository. They differ only in how a Docker daemon is made available to the jobs the runner executes; the `gitea-runner` binary inside them is identical.
|
||||||
@@ -123,8 +121,6 @@ Two processes have to run side by side here (the Docker daemon and the runner),
|
|||||||
|
|
||||||
Same idea as `dind`, but built on `docker:dind-rootless` so the bundled daemon and the runner run as an unprivileged user (`rootless`, UID 1000) rather than `root`. `DOCKER_HOST` is preset to `unix:///run/user/1000/docker.sock` so the runner talks to the rootless daemon. This reduces the blast radius compared to the privileged `dind` flavour, but rootless Docker carries the usual rootless limitations (networking, cgroups, storage drivers, and some operations that need additional host configuration such as `/etc/subuid` / `/etc/subgid` mappings and unprivileged user-namespace support).
|
Same idea as `dind`, but built on `docker:dind-rootless` so the bundled daemon and the runner run as an unprivileged user (`rootless`, UID 1000) rather than `root`. `DOCKER_HOST` is preset to `unix:///run/user/1000/docker.sock` so the runner talks to the rootless daemon. This reduces the blast radius compared to the privileged `dind` flavour, but rootless Docker carries the usual rootless limitations (networking, cgroups, storage drivers, and some operations that need additional host configuration such as `/etc/subuid` / `/etc/subgid` mappings and unprivileged user-namespace support).
|
||||||
|
|
||||||
> **The UID is fixed at 1000.** It comes from the `rootless` user baked into the upstream `docker:dind-rootless` base image, and the bundled daemon always listens on `/run/user/1000/docker.sock` inside the container, so running this flavour as a different user (`--user 1001`) does not work. If you need the runner to talk to a *host* rootless daemon that runs under some other UID, use the `basic` flavour instead and bind-mount that daemon's socket (see [examples/vm/rootless-docker.md](examples/vm/rootless-docker.md)); pointing `DOCKER_HOST` at a host socket from inside `dind-rootless` will not work. Changing the UID otherwise means rebuilding the image from a base with a different `rootless` user.
|
|
||||||
|
|
||||||
> **Note on Podman:** these images target the Docker daemon. The bundled `dind`/`dind-rootless` daemons are `dockerd`, not Podman, and the `basic` flavour expects a Docker-compatible socket. Running them under rootless Podman is not a supported configuration, though pointing the `basic` flavour at a Podman socket that emulates the Docker API may work for some workloads.
|
> **Note on Podman:** these images target the Docker daemon. The bundled `dind`/`dind-rootless` daemons are `dockerd`, not Podman, and the `basic` flavour expects a Docker-compatible socket. Running them under rootless Podman is not a supported configuration, though pointing the `basic` flavour at a Podman socket that emulates the Docker API may work for some workloads.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
@@ -147,60 +143,23 @@ Every option is described in [config.example.yaml](internal/pkg/config/config.ex
|
|||||||
|
|
||||||
#### Without a config file
|
#### Without a config file
|
||||||
|
|
||||||
If you omit `-c`, built-in defaults apply (same as an empty YAML document).
|
If you omit `-c`, built-in defaults apply (same as an empty YAML document). A small set of **deprecated** environment variables can still override parts of that default config, but **only when no `-c` path was given**; they are ignored if you use a config file:
|
||||||
|
|
||||||
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.
|
| Variable | Effect |
|
||||||
|
|
||||||
### Labels
|
|
||||||
|
|
||||||
Labels decide **which jobs a runner accepts** and **how it runs them**. A job's `runs-on` is matched against the runner's label names; the first match wins and selects the execution environment for that job.
|
|
||||||
|
|
||||||
A label is written as:
|
|
||||||
|
|
||||||
```text
|
|
||||||
<name>[:<schema>[:<args>]]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Part | Meaning |
|
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `name` | The name a workflow refers to in `runs-on`, e.g. `ubuntu-latest`. |
|
| `GITEA_DEBUG` | If true, sets log level to `debug` |
|
||||||
| `schema` | Either `docker` or `host`. Defaults to `host` when omitted. |
|
| `GITEA_TRACE` | If true, sets log level to `trace` |
|
||||||
| `args` | Only used by the `docker` schema: the image to run the job in. |
|
| `GITEA_RUNNER_CAPACITY` | Concurrent jobs (integer) |
|
||||||
|
| `GITEA_RUNNER_FILE` | Registration state file path (default `.runner`) |
|
||||||
|
| `GITEA_RUNNER_ENVIRON` | Extra job env vars as comma-separated `KEY:VALUE` pairs |
|
||||||
|
| `GITEA_RUNNER_ENV_FILE` | Path to an env file merged into job env (same idea as `runner.env_file` in YAML) |
|
||||||
|
|
||||||
Two schemas are supported:
|
Prefer a YAML file for all settings.
|
||||||
|
|
||||||
- **`docker://<image>`** — the job runs inside a container created from `<image>`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`host`** — the job's steps run directly on the machine the runner is on, using the tools installed there:
|
|
||||||
|
|
||||||
```text
|
|
||||||
macos:host
|
|
||||||
```
|
|
||||||
|
|
||||||
So with the labels
|
|
||||||
|
|
||||||
```text
|
|
||||||
ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,macos:host
|
|
||||||
```
|
|
||||||
|
|
||||||
a workflow with `runs-on: ubuntu-latest` is executed in the `runner-images:ubuntu-latest` container, and one with `runs-on: macos` is executed directly on the host.
|
|
||||||
|
|
||||||
Names may themselves contain a colon (for example `pool:e57e18d4-10d4-406f-93bf-60f127221bdd`); only `host` and `docker` are treated as schemas.
|
|
||||||
|
|
||||||
If a job's `runs-on` matches none of the runner's labels, the job still runs, in the default `docker.gitea.com/runner-images:ubuntu-latest` image. Images maintained for this purpose are listed at [gitea/runner-images](https://gitea.com/gitea/runner-images).
|
|
||||||
|
|
||||||
Labels are chosen at registration time (`--labels`, or the interactive prompt) and can be changed afterwards by editing `runner.labels` in the config file, or in the Gitea UI under the runner's settings.
|
|
||||||
|
|
||||||
#### Registration vs config labels
|
#### Registration vs config labels
|
||||||
|
|
||||||
If `runner.labels` is set in the YAML file, those labels are used during `register` and the `--labels` CLI flag is ignored.
|
If `runner.labels` is set in the YAML file, those labels are used during `register` and the `--labels` CLI flag is ignored.
|
||||||
|
|
||||||
> **Note:** A runner that only exposes `host` labels still needs access to a Docker daemon (e.g. a mounted `/var/run/docker.sock`) whenever a job uses a `docker://` action or a service container. `host` labels only change where the job's own steps run; container-based steps and actions are still executed with Docker.
|
|
||||||
|
|
||||||
#### Caching (`actions/cache`)
|
#### Caching (`actions/cache`)
|
||||||
|
|
||||||
Each runner starts its own cache server automatically. Cache entries are local to that runner — runners do not share a cache by default.
|
Each runner starts its own cache server automatically. Cache entries are local to that runner — runners do not share a cache by default.
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
|||||||
|
|
||||||
_ = logDockerResponse(logger, reader, err != nil)
|
_ = logDockerResponse(logger, reader, err != nil)
|
||||||
}
|
}
|
||||||
if err != nil {
|
return err
|
||||||
return fmt.Errorf("failed to pull image '%s' (%s): %w", imageRef, input.Platform, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,8 +229,7 @@ func (ee expressionEvaluator) evaluate(ctx context.Context, in string, defaultSt
|
|||||||
logger.Debugf("evaluating expression '%s'", in)
|
logger.Debugf("evaluating expression '%s'", in)
|
||||||
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck)
|
||||||
|
|
||||||
// evaluated is an any: %t renders everything but a bool as "%!t(string=...)"
|
printable := regexp.MustCompile(`::add-mask::.*`).ReplaceAllString(fmt.Sprintf("%t", evaluated), "::add-mask::***)")
|
||||||
printable := regexp.MustCompile(`::add-mask::.*`).ReplaceAllString(fmt.Sprintf("%v", evaluated), "::add-mask::***)")
|
|
||||||
logger.Debugf("expression '%s' evaluated to '%s'", in, printable)
|
logger.Debugf("expression '%s' evaluated to '%s'", in, printable)
|
||||||
|
|
||||||
return evaluated, err
|
return evaluated, err
|
||||||
@@ -498,7 +497,11 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
|
|||||||
if value == nil {
|
if value == nil {
|
||||||
value = v.Default
|
value = v.Default
|
||||||
}
|
}
|
||||||
inputs[k] = coerceInputValue(value, v.Type)
|
if v.Type == "boolean" {
|
||||||
|
inputs[k] = value == "true"
|
||||||
|
} else {
|
||||||
|
inputs[k] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,26 +514,17 @@ func getEvaluatorInputs(ctx context.Context, rc *RunContext, step step, ghc *mod
|
|||||||
if value == nil {
|
if value == nil {
|
||||||
value = v.Default
|
value = v.Default
|
||||||
}
|
}
|
||||||
inputs[k] = coerceInputValue(value, v.Type)
|
if v.Type == "boolean" {
|
||||||
|
inputs[k] = value == "true"
|
||||||
|
} else {
|
||||||
|
inputs[k] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return inputs
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
// coerceInputValue converts an input value to the type declared by the workflow.
|
|
||||||
// The event payload carries natively typed JSON values on newer Gitea versions,
|
|
||||||
// while defaults and older servers provide strings.
|
|
||||||
func coerceInputValue(value any, inputType string) any {
|
|
||||||
if inputType != "boolean" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
if b, ok := value.(bool); ok {
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
return value == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupWorkflowInputs(ctx context.Context, inputs *map[string]any, rc *RunContext) {
|
func setupWorkflowInputs(ctx context.Context, inputs *map[string]any, rc *RunContext) {
|
||||||
if rc.caller != nil {
|
if rc.caller != nil {
|
||||||
config := rc.Run.Workflow.WorkflowCallConfig()
|
config := rc.Run.Workflow.WorkflowCallConfig()
|
||||||
@@ -554,7 +548,7 @@ func setupWorkflowInputs(ctx context.Context, inputs *map[string]any, rc *RunCon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(*inputs)[name] = coerceInputValue(value, input.Type)
|
(*inputs)[name] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,82 +321,3 @@ func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetEvaluatorInputsBoolean(t *testing.T) {
|
|
||||||
workflows := map[string]string{
|
|
||||||
"workflow_call": `
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
flag:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
default: gitea
|
|
||||||
`,
|
|
||||||
"workflow_dispatch": `
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
flag:
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
default: gitea
|
|
||||||
`,
|
|
||||||
}
|
|
||||||
|
|
||||||
tables := []struct {
|
|
||||||
name string
|
|
||||||
event map[string]any
|
|
||||||
flag any
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// Gitea >= 1.27 resolves the inputs server-side and sends native JSON types
|
|
||||||
name: "native bool true",
|
|
||||||
event: map[string]any{"inputs": map[string]any{"flag": true}},
|
|
||||||
flag: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "native bool false",
|
|
||||||
event: map[string]any{"inputs": map[string]any{"flag": false}},
|
|
||||||
flag: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string true",
|
|
||||||
event: map[string]any{"inputs": map[string]any{"flag": "true"}},
|
|
||||||
flag: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "string false",
|
|
||||||
event: map[string]any{"inputs": map[string]any{"flag": "false"}},
|
|
||||||
flag: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "default is used when the event carries no inputs",
|
|
||||||
event: map[string]any{},
|
|
||||||
flag: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for eventName, workflow := range workflows {
|
|
||||||
for _, table := range tables {
|
|
||||||
t.Run(eventName+"/"+table.name, func(t *testing.T) {
|
|
||||||
wf, err := model.ReadWorkflow(strings.NewReader(workflow))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
rc := &RunContext{
|
|
||||||
Config: &Config{Workdir: "."},
|
|
||||||
Run: &model.Run{JobID: "job1", Workflow: wf},
|
|
||||||
}
|
|
||||||
ghc := &model.GithubContext{EventName: eventName, Event: table.event}
|
|
||||||
|
|
||||||
inputs := getEvaluatorInputs(context.Background(), rc, nil, ghc)
|
|
||||||
assert.Equal(t, table.flag, inputs["flag"])
|
|
||||||
assert.Equal(t, "gitea", inputs["name"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -138,9 +138,7 @@ func (rc *RunContext) GetEnv() map[string]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !rc.Config.DisableActEnv {
|
rc.Env["ACT"] = "true"
|
||||||
rc.Env["ACT"] = "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !rc.Config.NoSkipCheckout {
|
if !rc.Config.NoSkipCheckout {
|
||||||
rc.Env["ACT_SKIP_CHECKOUT"] = "true"
|
rc.Env["ACT_SKIP_CHECKOUT"] = "true"
|
||||||
@@ -339,9 +337,6 @@ func printStartJobContainerGroup(ctx context.Context, image, name, network strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newContainer is a variable so tests can substitute a container that needs no Docker daemon.
|
|
||||||
var newContainer = container.NewContainer
|
|
||||||
|
|
||||||
func (rc *RunContext) startJobContainer() common.Executor {
|
func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
@@ -384,13 +379,6 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
|
|
||||||
// add service containers
|
// add service containers
|
||||||
for serviceID, spec := range rc.Run.Job().Services {
|
for serviceID, spec := range rc.Run.Job().Services {
|
||||||
// GitHub compatibility: skip services whose image evaluates to an
|
|
||||||
// empty string, enabling conditional services via expressions
|
|
||||||
serviceImage := rc.ExprEval.Interpolate(ctx, spec.Image)
|
|
||||||
if serviceImage == "" {
|
|
||||||
logger.Infof("The service '%s' will not be started because the container definition has an empty image.", serviceID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// interpolate env
|
// interpolate env
|
||||||
interpolatedEnvs := make(map[string]string, len(spec.Env))
|
interpolatedEnvs := make(map[string]string, len(spec.Env))
|
||||||
for k, v := range spec.Env {
|
for k, v := range spec.Env {
|
||||||
@@ -405,9 +393,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
for _, v := range spec.Cmd {
|
for _, v := range spec.Cmd {
|
||||||
interpolatedCmd = append(interpolatedCmd, rc.ExprEval.Interpolate(ctx, v))
|
interpolatedCmd = append(interpolatedCmd, rc.ExprEval.Interpolate(ctx, v))
|
||||||
}
|
}
|
||||||
// keep these local: reusing username/password would overwrite the
|
username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials)
|
||||||
// credentials the job container is pulled with further down
|
|
||||||
serviceUsername, servicePassword, err := rc.handleServiceCredentials(ctx, spec.Credentials)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
|
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
|
||||||
}
|
}
|
||||||
@@ -428,12 +414,12 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
||||||
c := newContainer(&container.NewContainerInput{
|
c := container.NewContainer(&container.NewContainerInput{
|
||||||
Name: serviceContainerName,
|
Name: serviceContainerName,
|
||||||
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
||||||
Image: serviceImage,
|
Image: rc.ExprEval.Interpolate(ctx, spec.Image),
|
||||||
Username: serviceUsername,
|
Username: username,
|
||||||
Password: servicePassword,
|
Password: password,
|
||||||
Cmd: interpolatedCmd,
|
Cmd: interpolatedCmd,
|
||||||
Env: envs,
|
Env: envs,
|
||||||
Mounts: serviceMounts,
|
Mounts: serviceMounts,
|
||||||
@@ -489,7 +475,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
// For Gitea, `jobContainerNetwork` should be the same as `networkName`
|
// For Gitea, `jobContainerNetwork` should be the same as `networkName`
|
||||||
jobContainerNetwork := networkName
|
jobContainerNetwork := networkName
|
||||||
|
|
||||||
rc.JobContainer = newContainer(&container.NewContainerInput{
|
rc.JobContainer = container.NewContainer(&container.NewContainerInput{
|
||||||
Cmd: nil,
|
Cmd: nil,
|
||||||
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
||||||
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
||||||
@@ -797,13 +783,7 @@ func (rc *RunContext) Executor() (common.Executor, error) {
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
res, err := rc.isEnabled(ctx)
|
res, err := rc.isEnabled(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Record the failure so a job whose if-expression fails to evaluate
|
rc.caller.setReusedWorkflowJobResult(rc.JobName, "failure") // For Gitea
|
||||||
// gets a result (and therefore a stop time) instead of being left
|
|
||||||
// unfinished. rc.caller is only set for reusable workflows.
|
|
||||||
rc.result("failure")
|
|
||||||
if rc.caller != nil { // For Gitea
|
|
||||||
rc.caller.setReusedWorkflowJobResult(rc.JobName, "failure")
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if res {
|
if res {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/container"
|
|
||||||
"gitea.com/gitea/runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
@@ -203,93 +202,6 @@ jobs:
|
|||||||
assert.Empty(t, password)
|
assert.Empty(t, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fakeContainer turns every container operation into a no-op, so startJobContainer
|
|
||||||
// runs without a Docker daemon. The embedded interface is nil, so any method the
|
|
||||||
// test does not exercise panics rather than silently doing the wrong thing.
|
|
||||||
type fakeContainer struct {
|
|
||||||
container.ExecutionsEnvironment
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fakeContainer) Pull(bool) common.Executor { return func(context.Context) error { return nil } }
|
|
||||||
func (fakeContainer) Start(bool) common.Executor { return func(context.Context) error { return nil } }
|
|
||||||
func (fakeContainer) Remove() common.Executor { return func(context.Context) error { return nil } }
|
|
||||||
func (fakeContainer) Close() common.Executor { return func(context.Context) error { return nil } }
|
|
||||||
func (fakeContainer) GetActPath() string { return "/var/run/act" }
|
|
||||||
func (fakeContainer) Create([]string, []string) common.Executor {
|
|
||||||
return func(context.Context) error { return nil }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fakeContainer) Copy(string, ...*container.FileEntry) common.Executor {
|
|
||||||
return func(context.Context) error { return nil }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regression test: a service without a `credentials:` block resolves to empty
|
|
||||||
// credentials, which used to overwrite the job container's own credentials.
|
|
||||||
func TestStartJobContainerKeepsJobCredentialsWithServices(t *testing.T) {
|
|
||||||
workflow, err := model.ReadWorkflow(strings.NewReader(`
|
|
||||||
name: test
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: registry.example/private:latest
|
|
||||||
credentials:
|
|
||||||
username: job-user
|
|
||||||
password: job-password
|
|
||||||
services:
|
|
||||||
redis:
|
|
||||||
image: redis:latest
|
|
||||||
db:
|
|
||||||
image: postgres:latest
|
|
||||||
credentials:
|
|
||||||
username: db-user
|
|
||||||
password: db-password
|
|
||||||
steps: []
|
|
||||||
`))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var inputs []*container.NewContainerInput
|
|
||||||
origNewContainer := newContainer
|
|
||||||
newContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
|
||||||
inputs = append(inputs, input)
|
|
||||||
return fakeContainer{}
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { newContainer = origNewContainer })
|
|
||||||
|
|
||||||
rc := &RunContext{
|
|
||||||
Name: "test",
|
|
||||||
Config: &Config{
|
|
||||||
Workdir: "/tmp",
|
|
||||||
// no daemon: an explicit network mode creates no network, and
|
|
||||||
// reusing containers short-circuits the volume cleanup executors
|
|
||||||
ContainerNetworkMode: "host",
|
|
||||||
ReuseContainers: true,
|
|
||||||
Env: map[string]string{},
|
|
||||||
Secrets: map[string]string{},
|
|
||||||
},
|
|
||||||
Env: map[string]string{},
|
|
||||||
Run: &model.Run{
|
|
||||||
JobID: "job",
|
|
||||||
Workflow: workflow,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(t.Context())
|
|
||||||
|
|
||||||
require.NoError(t, rc.startJobContainer()(t.Context()))
|
|
||||||
|
|
||||||
credentials := map[string][2]string{}
|
|
||||||
for _, in := range inputs {
|
|
||||||
credentials[in.Image] = [2]string{in.Username, in.Password}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the job container keeps its own credentials, whichever services exist
|
|
||||||
require.Equal(t, [2]string{"job-user", "job-password"}, credentials["registry.example/private:latest"])
|
|
||||||
// each service keeps its own, and a service without credentials gets none
|
|
||||||
require.Equal(t, [2]string{"db-user", "db-password"}, credentials["postgres:latest"])
|
|
||||||
require.Equal(t, [2]string{"", ""}, credentials["redis:latest"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
||||||
rctemplate := &RunContext{
|
rctemplate := &RunContext{
|
||||||
Name: "TestRCName",
|
Name: "TestRCName",
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ type Config struct {
|
|||||||
ArtifactServerAddr string // the address the artifact server binds to
|
ArtifactServerAddr string // the address the artifact server binds to
|
||||||
ArtifactServerPort string // the port the artifact server binds to
|
ArtifactServerPort string // the port the artifact server binds to
|
||||||
NoSkipCheckout bool // do not skip actions/checkout
|
NoSkipCheckout bool // do not skip actions/checkout
|
||||||
DisableActEnv bool // do not inject the ACT=true environment variable into jobs
|
|
||||||
RemoteName string // remote name in local git repo config
|
RemoteName string // remote name in local git repo config
|
||||||
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
ReplaceGheActionWithGithubCom []string // Use actions from GitHub Enterprise instance to GitHub
|
||||||
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
ReplaceGheActionTokenWithGithubCom string // Token of private action repo on GitHub.
|
||||||
|
|||||||
@@ -303,7 +303,6 @@ func TestRunEvent(t *testing.T) {
|
|||||||
// services
|
// services
|
||||||
{workdir, "services", "push", "", platforms, secrets},
|
{workdir, "services", "push", "", platforms, secrets},
|
||||||
{workdir, "services-with-container", "push", "", platforms, secrets},
|
{workdir, "services-with-container", "push", "", platforms, secrets},
|
||||||
{workdir, "services-empty-image", "push", "", platforms, secrets},
|
|
||||||
|
|
||||||
// local remote action overrides
|
// local remote action overrides
|
||||||
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
|
{workdir, "local-remote-action-overrides", "push", "", platforms, secrets},
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
name: services-empty-image
|
|
||||||
on: push
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: ${{ false && 'postgres:16' || '' }}
|
|
||||||
steps:
|
|
||||||
- run: echo "empty-image service was skipped"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"required": "required input",
|
"required": "required input",
|
||||||
"boolean": true
|
"boolean": "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ NOTE: `dind-docker.yaml` uses the native sidecar pattern (init container with `r
|
|||||||
|
|
||||||
NOTE: A helm chart for `gitea-runner` also exists for easier deployments https://gitea.com/gitea/helm-actions
|
NOTE: A helm chart for `gitea-runner` also exists for easier deployments https://gitea.com/gitea/helm-actions
|
||||||
|
|
||||||
Each example persists **two** things, and it is worth knowing which is which:
|
|
||||||
|
|
||||||
- `/data` is the runner's working directory. It holds the `.runner` registration file and, optionally, the config file — so the runner re-attaches to the server instead of registering again.
|
|
||||||
- The Docker daemon's data root holds the images pulled for jobs (`/var/lib/docker` for the dind sidecar, `/home/rootless/.local/share/docker` for `dind-rootless`). It is *not* under `/data`. If you drop this volume, the examples still work, but the image cache is discarded whenever the pod is recreated and every job re-pulls its images.
|
|
||||||
|
|
||||||
Files in this directory:
|
Files in this directory:
|
||||||
|
|
||||||
- [`dind-docker.yaml`](dind-docker.yaml)
|
- [`dind-docker.yaml`](dind-docker.yaml)
|
||||||
@@ -18,6 +13,3 @@ Files in this directory:
|
|||||||
|
|
||||||
- [`rootless-docker.yaml`](rootless-docker.yaml)
|
- [`rootless-docker.yaml`](rootless-docker.yaml)
|
||||||
How to create a rootless Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.
|
How to create a rootless Deployment and Persistent Volume for Kubernetes to act as a runner. The Docker credentials are re-generated each time the pod connects and does not need to be persisted.
|
||||||
|
|
||||||
- [`statefulset-dind.yaml`](statefulset-dind.yaml)
|
|
||||||
StatefulSet variant of the dind example. Each replica gets a stable identity and its own persistent volume via `volumeClaimTemplates`, so the runner keeps its `.runner` registration across restarts and reschedules instead of trying to register again.
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Holds the runner's working directory (/data): the .runner registration file
|
|
||||||
# and, optionally, the config file.
|
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
@@ -12,21 +10,6 @@ spec:
|
|||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
storageClassName: standard
|
storageClassName: standard
|
||||||
---
|
---
|
||||||
# Holds the Docker daemon's data root (/var/lib/docker), i.e. the images pulled
|
|
||||||
# for jobs. Without it, the image cache is lost whenever the pod is recreated
|
|
||||||
# and every job re-pulls its images. Size it for the images you expect to cache.
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: docker-vol
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 20Gi
|
|
||||||
storageClassName: standard
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
data:
|
data:
|
||||||
# The registration token can be obtained from the web UI, API or command-line.
|
# The registration token can be obtained from the web UI, API or command-line.
|
||||||
@@ -62,9 +45,6 @@ spec:
|
|||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: runner-vol
|
claimName: runner-vol
|
||||||
- name: docker-data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: docker-vol
|
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: docker
|
- name: docker
|
||||||
image: docker:28.2.2-dind
|
image: docker:28.2.2-dind
|
||||||
@@ -73,8 +53,6 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: docker-socket
|
- name: docker-socket
|
||||||
mountPath: /var/run
|
mountPath: /var/run
|
||||||
- name: docker-data
|
|
||||||
mountPath: /var/lib/docker
|
|
||||||
startupProbe:
|
startupProbe:
|
||||||
exec:
|
exec:
|
||||||
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Holds the runner's working directory (/data): the .runner registration file
|
|
||||||
# and, optionally, the config file.
|
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
metadata:
|
metadata:
|
||||||
@@ -12,21 +10,6 @@ spec:
|
|||||||
storage: 1Gi
|
storage: 1Gi
|
||||||
storageClassName: standard
|
storageClassName: standard
|
||||||
---
|
---
|
||||||
# Holds the rootless Docker daemon's data root, i.e. the images pulled for jobs.
|
|
||||||
# Without it, the image cache is lost whenever the pod is recreated and every job
|
|
||||||
# re-pulls its images. Size it for the images you expect to cache.
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: docker-vol
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 20Gi
|
|
||||||
storageClassName: standard
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
data:
|
data:
|
||||||
# The registration token can be obtained from the web UI, API or command-line.
|
# The registration token can be obtained from the web UI, API or command-line.
|
||||||
@@ -60,12 +43,7 @@ spec:
|
|||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: runner-vol
|
claimName: runner-vol
|
||||||
- name: docker-data
|
|
||||||
persistentVolumeClaim:
|
|
||||||
claimName: docker-vol
|
|
||||||
securityContext:
|
securityContext:
|
||||||
# The dind-rootless image runs as the `rootless` user (UID/GID 1000);
|
|
||||||
# fsGroup makes both volumes writable for it.
|
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
containers:
|
containers:
|
||||||
- name: runner
|
- name: runner
|
||||||
@@ -90,7 +68,4 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
# The rootless daemon keeps its images here, not under /data.
|
|
||||||
- name: docker-data
|
|
||||||
mountPath: /home/rootless/.local/share/docker
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
# StatefulSet variant of the dind example.
|
|
||||||
#
|
|
||||||
# Unlike the Deployment, a StatefulSet gives each replica a stable identity and,
|
|
||||||
# via volumeClaimTemplates, its own persistent volume. That means every runner
|
|
||||||
# pod keeps its own `.runner` registration file across restarts and reschedules,
|
|
||||||
# so it re-attaches to the server instead of trying to register again.
|
|
||||||
apiVersion: v1
|
|
||||||
data:
|
|
||||||
# The registration token can be obtained from the web UI, API or command-line.
|
|
||||||
# You can also set a pre-defined global runner registration token for the Gitea instance via
|
|
||||||
# `GITEA_RUNNER_REGISTRATION_TOKEN`/`GITEA_RUNNER_REGISTRATION_TOKEN_FILE` environment variable.
|
|
||||||
token: << base64 encoded registration token >>
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: runner-secret
|
|
||||||
type: Opaque
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: StatefulSet
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: runner
|
|
||||||
name: runner
|
|
||||||
spec:
|
|
||||||
serviceName: runner
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: runner
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: runner
|
|
||||||
spec:
|
|
||||||
restartPolicy: Always
|
|
||||||
volumes:
|
|
||||||
- name: docker-socket
|
|
||||||
emptyDir: {}
|
|
||||||
initContainers:
|
|
||||||
- name: docker
|
|
||||||
image: docker:28.2.2-dind
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
volumeMounts:
|
|
||||||
- name: docker-socket
|
|
||||||
mountPath: /var/run
|
|
||||||
# Keeps the images pulled for jobs across restarts. Without this, the
|
|
||||||
# daemon's data root is ephemeral and every job re-pulls its images.
|
|
||||||
- name: docker-data
|
|
||||||
mountPath: /var/lib/docker
|
|
||||||
startupProbe:
|
|
||||||
exec:
|
|
||||||
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
|
||||||
livenessProbe:
|
|
||||||
exec:
|
|
||||||
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
|
||||||
restartPolicy: Always
|
|
||||||
containers:
|
|
||||||
- name: runner
|
|
||||||
image: gitea/runner:nightly
|
|
||||||
env:
|
|
||||||
- name: GITEA_INSTANCE_URL
|
|
||||||
value: http://gitea-http.gitea.svc.cluster.local:3000
|
|
||||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: runner-secret
|
|
||||||
key: token
|
|
||||||
volumeMounts:
|
|
||||||
- name: runner-data
|
|
||||||
mountPath: /data
|
|
||||||
- name: docker-socket
|
|
||||||
mountPath: /var/run
|
|
||||||
volumeClaimTemplates:
|
|
||||||
# The runner's working directory: the .runner registration file and, optionally,
|
|
||||||
# the config file.
|
|
||||||
- metadata:
|
|
||||||
name: runner-data
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
storageClassName: standard
|
|
||||||
# The Docker daemon's data root: the images pulled for jobs. Size it for the
|
|
||||||
# images you expect to cache.
|
|
||||||
- metadata:
|
|
||||||
name: docker-data
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 20Gi
|
|
||||||
storageClassName: standard
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Running the runner as a systemd service
|
|
||||||
|
|
||||||
[`gitea-runner.service`](./gitea-runner.service) is an example unit for running
|
|
||||||
the runner as a background service on a systemd host.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. Install the `gitea-runner` binary (e.g. to `/usr/local/bin/gitea-runner`).
|
|
||||||
2. Create a dedicated user and working directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo useradd --system --home-dir /var/lib/gitea-runner --create-home gitea-runner
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Generate a config and register the runner (as the service user), so the
|
|
||||||
`.runner` file ends up in the working directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo -u gitea-runner gitea-runner generate-config > /etc/gitea-runner/config.yaml
|
|
||||||
cd /var/lib/gitea-runner
|
|
||||||
sudo -u gitea-runner gitea-runner register --config /etc/gitea-runner/config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Install and enable the unit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp gitea-runner.service /etc/systemd/system/gitea-runner.service
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now gitea-runner
|
|
||||||
```
|
|
||||||
|
|
||||||
Adjust the binary path, config path, working directory and user to match your
|
|
||||||
installation. If jobs use the host's Docker daemon, uncomment the
|
|
||||||
`docker.service` dependencies in the unit.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Gitea Actions runner
|
|
||||||
Documentation=https://gitea.com/gitea/runner
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
# Uncomment when jobs use the local Docker daemon:
|
|
||||||
# After=docker.service
|
|
||||||
# Requires=docker.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
# Adjust the binary path, config path and working directory to your setup.
|
|
||||||
# The working directory is where the .runner registration file is read from
|
|
||||||
# unless runner.file is set to an absolute path in the config.
|
|
||||||
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml
|
|
||||||
WorkingDirectory=/var/lib/gitea-runner
|
|
||||||
User=gitea-runner
|
|
||||||
Group=gitea-runner
|
|
||||||
|
|
||||||
# Restart automatically so the runner survives transient failures, e.g. the
|
|
||||||
# Gitea instance being temporarily unreachable at startup.
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
|
|
||||||
# Allow running jobs to finish before the runner is stopped. Keep this in sync
|
|
||||||
# with runner.shutdown_timeout in the config.
|
|
||||||
TimeoutStopSec=3h
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
6
go.mod
6
go.mod
@@ -29,7 +29,6 @@ require (
|
|||||||
github.com/opencontainers/selinux v1.15.1
|
github.com/opencontainers/selinux v1.15.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/prometheus/client_model v0.6.2
|
|
||||||
github.com/rhysd/actionlint v1.7.12
|
github.com/rhysd/actionlint v1.7.12
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
@@ -38,8 +37,8 @@ require (
|
|||||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
||||||
go.etcd.io/bbolt v1.5.0
|
go.etcd.io/bbolt v1.5.0
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.46.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.44.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
tags.cncf.io/container-device-interface v1.1.0
|
tags.cncf.io/container-device-interface v1.1.0
|
||||||
@@ -85,6 +84,7 @@ require (
|
|||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -262,13 +262,9 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadBugReportCmd prints environment details that are useful when opening a
|
|
||||||
// bug report, so users can paste them straight into an issue.
|
|
||||||
func loadBugReportCmd() *cobra.Command {
|
|
||||||
return &cobra.Command{
|
|
||||||
Use: "bug-report",
|
|
||||||
Short: "Print information useful when filing a bug report",
|
|
||||||
Args: cobra.MaximumNArgs(0),
|
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
||||||
w := cmd.OutOrStdout()
|
|
||||||
fmt.Fprintf(w, "Runner version: %s\n", ver.Version())
|
|
||||||
fmt.Fprintf(w, "Go version: %s\n", runtime.Version())
|
|
||||||
fmt.Fprintf(w, "OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
|
||||||
fmt.Fprintf(w, "NumCPU: %d\n", runtime.NumCPU())
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,8 +35,7 @@ func Execute(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode")
|
||||||
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address")
|
||||||
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token (or set the GITEA_RUNNER_REGISTRATION_TOKEN envvar)")
|
registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token")
|
||||||
registerCmd.Flags().StringVar(®Args.TokenFile, "token-file", "", "Path to a file containing the runner token")
|
|
||||||
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name")
|
||||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||||
registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
|
registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)")
|
||||||
@@ -56,9 +55,6 @@ func Execute(ctx context.Context) {
|
|||||||
// ./gitea-runner exec
|
// ./gitea-runner exec
|
||||||
rootCmd.AddCommand(loadExecCmd(ctx))
|
rootCmd.AddCommand(loadExecCmd(ctx))
|
||||||
|
|
||||||
// ./gitea-runner bug-report
|
|
||||||
rootCmd.AddCommand(loadBugReportCmd())
|
|
||||||
|
|
||||||
// ./gitea-runner config
|
// ./gitea-runner config
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: "generate-config",
|
Use: "generate-config",
|
||||||
|
|||||||
@@ -176,12 +176,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
} else {
|
} else {
|
||||||
go poller.Poll()
|
go poller.Poll()
|
||||||
|
|
||||||
// Stop either on an external cancellation or when the poller shuts
|
<-ctx.Done()
|
||||||
// itself down (e.g. after the runner has been unregistered).
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
case <-poller.Done():
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
log.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
|
||||||
@@ -194,10 +189,6 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
|
log.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if poller.Unregistered() {
|
|
||||||
return errors.New("runner is no longer registered with the server; please register it again")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type executeArgs struct {
|
|||||||
runList bool
|
runList bool
|
||||||
job string
|
job string
|
||||||
event string
|
event string
|
||||||
eventpath string
|
|
||||||
workdir string
|
workdir string
|
||||||
workflowsPath string
|
workflowsPath string
|
||||||
noWorkflowRecurse bool
|
noWorkflowRecurse bool
|
||||||
@@ -442,7 +441,6 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
ArtifactServerPort: execArgs.artifactServerPort,
|
ArtifactServerPort: execArgs.artifactServerPort,
|
||||||
ArtifactServerAddr: execArgs.artifactServerAddr,
|
ArtifactServerAddr: execArgs.artifactServerAddr,
|
||||||
NoSkipCheckout: execArgs.noSkipCheckout,
|
NoSkipCheckout: execArgs.noSkipCheckout,
|
||||||
EventPath: execArgs.resolve(execArgs.eventpath),
|
|
||||||
// PresetGitHubContext: preset,
|
// PresetGitHubContext: preset,
|
||||||
// EventJSON: string(eventJSON),
|
// EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
|
ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName,
|
||||||
@@ -498,9 +496,8 @@ func loadExecCmd(ctx context.Context) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
|
execCmd.Flags().BoolVarP(&execArg.runList, "list", "l", false, "list workflows")
|
||||||
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID; when several workflow files define that job, also pass --workflows/-W to select the file")
|
execCmd.Flags().StringVarP(&execArg.job, "job", "j", "", "run a specific job ID")
|
||||||
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
|
execCmd.Flags().StringVarP(&execArg.event, "event", "E", "", "run a event name")
|
||||||
execCmd.Flags().StringVarP(&execArg.eventpath, "eventpath", "e", "", "path to a JSON event payload file exposed as the event that triggered the workflow")
|
|
||||||
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
execCmd.PersistentFlags().StringVarP(&execArg.workflowsPath, "workflows", "W", "./.gitea/workflows/", "path to workflow file(s)")
|
||||||
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
execCmd.PersistentFlags().StringVarP(&execArg.workdir, "directory", "C", ".", "working directory")
|
||||||
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
execCmd.PersistentFlags().BoolVarP(&execArg.noWorkflowRecurse, "no-recurse", "", false, "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag")
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ type registerArgs struct {
|
|||||||
NoInteractive bool
|
NoInteractive bool
|
||||||
InstanceAddr string
|
InstanceAddr string
|
||||||
Token string
|
Token string
|
||||||
TokenFile string
|
|
||||||
RunnerName string
|
RunnerName string
|
||||||
Labels string
|
Labels string
|
||||||
Ephemeral bool
|
Ephemeral bool
|
||||||
@@ -94,8 +93,6 @@ const (
|
|||||||
StageExit
|
StageExit
|
||||||
)
|
)
|
||||||
|
|
||||||
const registerTokenEnvVar = "GITEA_RUNNER_REGISTRATION_TOKEN"
|
|
||||||
|
|
||||||
var defaultLabels = []string{
|
var defaultLabels = []string{
|
||||||
"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",
|
||||||
@@ -210,27 +207,10 @@ func (r *registerInputs) assignToNext(stage registerStage, value string, cfg *co
|
|||||||
return StageUnknown
|
return StageUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
func initInputs(regArgs *registerArgs) (*registerInputs, error) {
|
func initInputs(regArgs *registerArgs) *registerInputs {
|
||||||
var token string
|
|
||||||
switch {
|
|
||||||
case regArgs.TokenFile != "":
|
|
||||||
tokenBytes, err := os.ReadFile(regArgs.TokenFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot read the token file: %s, %v", regArgs.TokenFile, err)
|
|
||||||
}
|
|
||||||
token = string(tokenBytes)
|
|
||||||
case regArgs.Token != "":
|
|
||||||
token = regArgs.Token
|
|
||||||
default:
|
|
||||||
envToken, ok := os.LookupEnv(registerTokenEnvVar)
|
|
||||||
if !ok || envToken == "" {
|
|
||||||
return nil, fmt.Errorf("missing token, token-file argument, or %s environment variable", registerTokenEnvVar)
|
|
||||||
}
|
|
||||||
token = envToken
|
|
||||||
}
|
|
||||||
inputs := ®isterInputs{
|
inputs := ®isterInputs{
|
||||||
InstanceAddr: regArgs.InstanceAddr,
|
InstanceAddr: regArgs.InstanceAddr,
|
||||||
Token: token,
|
Token: regArgs.Token,
|
||||||
RunnerName: regArgs.RunnerName,
|
RunnerName: regArgs.RunnerName,
|
||||||
Ephemeral: regArgs.Ephemeral,
|
Ephemeral: regArgs.Ephemeral,
|
||||||
}
|
}
|
||||||
@@ -239,7 +219,7 @@ func initInputs(regArgs *registerArgs) (*registerInputs, error) {
|
|||||||
if regArgs.Labels != "" {
|
if regArgs.Labels != "" {
|
||||||
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
inputs.Labels = strings.Split(regArgs.Labels, ",")
|
||||||
}
|
}
|
||||||
return inputs, nil
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
func registerInteractive(ctx context.Context, configFile string, regArgs *registerArgs) error {
|
||||||
@@ -255,10 +235,7 @@ func registerInteractive(ctx context.Context, configFile string, regArgs *regist
|
|||||||
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
if f, err := os.Stat(cfg.Runner.File); err == nil && !f.IsDir() {
|
||||||
stage = StageOverwriteLocalConfig
|
stage = StageOverwriteLocalConfig
|
||||||
}
|
}
|
||||||
inputs, err := initInputs(regArgs)
|
inputs := initInputs(regArgs)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
cmdString := inputs.stageValue(stage)
|
cmdString := inputs.stageValue(stage)
|
||||||
@@ -315,10 +292,7 @@ func registerNoInteractive(ctx context.Context, configFile string, regArgs *regi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
inputs, err := initInputs(regArgs)
|
inputs := initInputs(regArgs)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// specify labels in config file.
|
// specify labels in config file.
|
||||||
if len(cfg.Runner.Labels) > 0 {
|
if len(cfg.Runner.Labels) > 0 {
|
||||||
if regArgs.Labels != "" {
|
if regArgs.Labels != "" {
|
||||||
|
|||||||
@@ -139,103 +139,18 @@ func TestRegisterInputsAssignToNext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInitInputs(t *testing.T) {
|
func TestInitInputs(t *testing.T) {
|
||||||
t.Run("missing token", func(t *testing.T) {
|
inputs := initInputs(®isterArgs{
|
||||||
_, err := initInputs(®isterArgs{
|
InstanceAddr: "http://localhost:3000",
|
||||||
InstanceAddr: "http://localhost:3000",
|
Token: "token",
|
||||||
RunnerName: "runner",
|
RunnerName: "runner",
|
||||||
Ephemeral: true,
|
Ephemeral: true,
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
||||||
})
|
|
||||||
require.EqualError(t, err, "missing token, token-file argument, or GITEA_RUNNER_REGISTRATION_TOKEN environment variable")
|
|
||||||
})
|
})
|
||||||
|
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
||||||
|
require.Equal(t, "token", inputs.Token)
|
||||||
|
require.Equal(t, "runner", inputs.RunnerName)
|
||||||
|
require.True(t, inputs.Ephemeral)
|
||||||
|
require.Equal(t, []string{"ubuntu:host ", " ubuntu:docker://node:18"}, inputs.Labels)
|
||||||
|
|
||||||
t.Run("empty token", func(t *testing.T) {
|
require.Nil(t, initInputs(®isterArgs{Labels: " "}).Labels)
|
||||||
t.Setenv(registerTokenEnvVar, "")
|
|
||||||
_, err := initInputs(®isterArgs{
|
|
||||||
InstanceAddr: "http://localhost:3000",
|
|
||||||
Token: "",
|
|
||||||
TokenFile: "",
|
|
||||||
RunnerName: "runner",
|
|
||||||
Ephemeral: true,
|
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
|
||||||
})
|
|
||||||
require.EqualError(t, err, "missing token, token-file argument, or GITEA_RUNNER_REGISTRATION_TOKEN environment variable")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid token file", func(t *testing.T) {
|
|
||||||
t.Setenv(registerTokenEnvVar, "from-env")
|
|
||||||
_, err := initInputs(®isterArgs{
|
|
||||||
InstanceAddr: "http://localhost:3000",
|
|
||||||
TokenFile: "/tmp/nonexistent",
|
|
||||||
RunnerName: "runner",
|
|
||||||
Ephemeral: true,
|
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
|
||||||
})
|
|
||||||
require.EqualError(t, err, "cannot read the token file: /tmp/nonexistent, open /tmp/nonexistent: no such file or directory")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid token", func(t *testing.T) {
|
|
||||||
t.Setenv(registerTokenEnvVar, "from-env")
|
|
||||||
inputs, err := initInputs(®isterArgs{
|
|
||||||
InstanceAddr: "http://localhost:3000",
|
|
||||||
Token: "from-plain-arg",
|
|
||||||
RunnerName: "runner",
|
|
||||||
Ephemeral: true,
|
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
|
||||||
require.Equal(t, "from-plain-arg", inputs.Token)
|
|
||||||
require.Equal(t, "runner", inputs.RunnerName)
|
|
||||||
require.True(t, inputs.Ephemeral)
|
|
||||||
require.Equal(t, []string{"ubuntu:host ", " ubuntu:docker://node:18"}, inputs.Labels)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("valid token file", func(t *testing.T) {
|
|
||||||
t.Setenv(registerTokenEnvVar, "from-env")
|
|
||||||
tokenFile, createErr := os.CreateTemp(t.TempDir(), "from-file")
|
|
||||||
require.NoError(t, createErr)
|
|
||||||
defer tokenFile.Close()
|
|
||||||
_, writeErr := tokenFile.WriteString("from-file")
|
|
||||||
require.NoError(t, writeErr)
|
|
||||||
_ = tokenFile.Sync()
|
|
||||||
|
|
||||||
inputs, err := initInputs(®isterArgs{
|
|
||||||
InstanceAddr: "http://localhost:3000",
|
|
||||||
TokenFile: tokenFile.Name(),
|
|
||||||
RunnerName: "runner",
|
|
||||||
Ephemeral: true,
|
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
|
||||||
require.Equal(t, "from-file", inputs.Token)
|
|
||||||
require.Equal(t, "runner", inputs.RunnerName)
|
|
||||||
require.True(t, inputs.Ephemeral)
|
|
||||||
require.Equal(t, []string{"ubuntu:host ", " ubuntu:docker://node:18"}, inputs.Labels)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("token from environment variable", func(t *testing.T) {
|
|
||||||
t.Setenv(registerTokenEnvVar, "from-env")
|
|
||||||
inputs, err := initInputs(®isterArgs{
|
|
||||||
InstanceAddr: "http://localhost:3000",
|
|
||||||
RunnerName: "runner",
|
|
||||||
Ephemeral: true,
|
|
||||||
Labels: " ubuntu:host , ubuntu:docker://node:18 ",
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "http://localhost:3000", inputs.InstanceAddr)
|
|
||||||
require.Equal(t, "from-env", inputs.Token)
|
|
||||||
require.Equal(t, "runner", inputs.RunnerName)
|
|
||||||
require.True(t, inputs.Ephemeral)
|
|
||||||
require.Equal(t, []string{"ubuntu:host ", " ubuntu:docker://node:18"}, inputs.Labels)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("empty labels", func(t *testing.T) {
|
|
||||||
inputs, _ := initInputs(®isterArgs{
|
|
||||||
Token: "from-plain-arg",
|
|
||||||
Labels: " ",
|
|
||||||
})
|
|
||||||
require.Nil(t, inputs.Labels)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ type Poller struct {
|
|||||||
shutdownJobs context.CancelFunc
|
shutdownJobs context.CancelFunc
|
||||||
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
|
||||||
// unregistered is set when the server rejects the runner with an
|
|
||||||
// Unauthenticated response, meaning the runner is no longer registered.
|
|
||||||
unregistered atomic.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// workerState holds the single poller's backoff state. Consecutive empty or
|
// workerState holds the single poller's backoff state. Consecutive empty or
|
||||||
@@ -141,19 +137,6 @@ func (p *Poller) PollOnce() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done returns a channel that is closed once polling has fully stopped,
|
|
||||||
// allowing callers to react when the poller shuts itself down (e.g. after the
|
|
||||||
// runner has been unregistered) rather than only on an external cancellation.
|
|
||||||
func (p *Poller) Done() <-chan struct{} {
|
|
||||||
return p.done
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregistered reports whether polling stopped because the server rejected the
|
|
||||||
// runner as unregistered (an Unauthenticated response).
|
|
||||||
func (p *Poller) Unregistered() bool {
|
|
||||||
return p.unregistered.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Poller) runIdleMaintenance() {
|
func (p *Poller) runIdleMaintenance() {
|
||||||
if idleRunner, ok := p.runner.(IdleRunner); ok {
|
if idleRunner, ok := p.runner.(IdleRunner); ok {
|
||||||
idleRunner.OnIdle(p.jobsCtx)
|
idleRunner.OnIdle(p.jobsCtx)
|
||||||
@@ -281,15 +264,6 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
|
|||||||
metrics.PollFetchDuration.Observe(time.Since(start).Seconds())
|
metrics.PollFetchDuration.Observe(time.Since(start).Seconds())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// An Unauthenticated response means the server no longer knows this
|
|
||||||
// runner (e.g. it was deleted). Retrying forever is pointless, so stop
|
|
||||||
// polling and let the daemon exit with an error instead of spinning.
|
|
||||||
if connect.CodeOf(err) == connect.CodeUnauthenticated {
|
|
||||||
log.WithError(err).Error("server rejected the runner as unregistered, stopping poller")
|
|
||||||
p.unregistered.Store(true)
|
|
||||||
p.shutdownPolling()
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
log.WithError(err).Error("failed to fetch task")
|
log.WithError(err).Error("failed to fetch task")
|
||||||
s.consecutiveErrors++
|
s.consecutiveErrors++
|
||||||
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultError).Inc()
|
||||||
|
|||||||
@@ -78,35 +78,6 @@ func TestPoller_FetchErrorIncrementsErrorsOnly(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), s.consecutiveEmpty)
|
assert.Equal(t, int64(0), s.consecutiveEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPoller_FetchUnauthenticatedStopsPolling verifies that an Unauthenticated
|
|
||||||
// response marks the runner as unregistered and cancels the polling context so
|
|
||||||
// the daemon can exit instead of retrying forever.
|
|
||||||
func TestPoller_FetchUnauthenticatedStopsPolling(t *testing.T) {
|
|
||||||
client := mocks.NewClient(t)
|
|
||||||
client.On("FetchTask", mock.Anything, mock.Anything).Return(
|
|
||||||
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
|
|
||||||
return nil, connect_go.NewError(connect_go.CodeUnauthenticated, errors.New("unregistered runner"))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
cfg, err := config.LoadDefault("")
|
|
||||||
require.NoError(t, err)
|
|
||||||
p := New(cfg, client, nil)
|
|
||||||
|
|
||||||
s := &workerState{}
|
|
||||||
_, ok := p.fetchTask(context.Background(), s)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
assert.True(t, p.Unregistered(), "runner should be marked unregistered")
|
|
||||||
assert.Equal(t, int64(0), s.consecutiveErrors, "unauthenticated must not drive error backoff")
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.pollingCtx.Done():
|
|
||||||
default:
|
|
||||||
t.Fatal("expected polling context to be cancelled after an Unauthenticated response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPoller_CalculateInterval verifies the exponential backoff math is
|
// TestPoller_CalculateInterval verifies the exponential backoff math is
|
||||||
// correctly driven by the workerState counters.
|
// correctly driven by the workerState counters.
|
||||||
func TestPoller_CalculateInterval(t *testing.T) {
|
func TestPoller_CalculateInterval(t *testing.T) {
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
} else if t := task.Secrets["GITHUB_TOKEN"]; t != "" {
|
||||||
preset.Token = t
|
preset.Token = t
|
||||||
}
|
}
|
||||||
|
applyPullRequestTargetCheckoutContext(preset)
|
||||||
|
|
||||||
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
|
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
|
||||||
envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
|
envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
|
||||||
@@ -445,7 +446,6 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
NoSkipCheckout: true,
|
NoSkipCheckout: true,
|
||||||
DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv,
|
|
||||||
PresetGitHubContext: preset,
|
PresetGitHubContext: preset,
|
||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
@@ -575,6 +575,41 @@ func postInternalCache(url, secret string, body map[string]string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyPullRequestTargetCheckoutContext(preset *model.GithubContext) {
|
||||||
|
if preset == nil || preset.EventName != "pull_request_target" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headSHA := nestedString(preset.Event, "pull_request", "head", "sha")
|
||||||
|
if headSHA == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preset.Sha = headSHA
|
||||||
|
preset.Ref = headSHA
|
||||||
|
if headRef := nestedString(preset.Event, "pull_request", "head", "ref"); headRef != "" {
|
||||||
|
preset.HeadRef = headRef
|
||||||
|
preset.RefName = headRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedString(m map[string]any, keys ...string) string {
|
||||||
|
var current any = m
|
||||||
|
for _, key := range keys {
|
||||||
|
next, ok := current.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
current, ok = next[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value := current.(string)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Runner) RunningCount() int64 {
|
func (r *Runner) RunningCount() int64 {
|
||||||
return r.runningCount.Load()
|
return r.runningCount.Load()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/model"
|
||||||
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||||
"gitea.com/gitea/runner/internal/pkg/config"
|
"gitea.com/gitea/runner/internal/pkg/config"
|
||||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
"gitea.com/gitea/runner/internal/pkg/ver"
|
||||||
@@ -92,6 +93,67 @@ func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
|
|||||||
require.Nil(t, r.cacheHandler)
|
require.Nil(t, r.cacheHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyPullRequestTargetCheckoutContextUsesHeadSHA(t *testing.T) {
|
||||||
|
preset := &model.GithubContext{
|
||||||
|
EventName: "pull_request_target",
|
||||||
|
Sha: "base-sha",
|
||||||
|
Ref: "refs/heads/main",
|
||||||
|
RefName: "main",
|
||||||
|
HeadRef: "feature",
|
||||||
|
Event: map[string]any{
|
||||||
|
"pull_request": map[string]any{
|
||||||
|
"head": map[string]any{
|
||||||
|
"sha": "head-sha",
|
||||||
|
"ref": "contributor-branch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPullRequestTargetCheckoutContext(preset)
|
||||||
|
|
||||||
|
require.Equal(t, "head-sha", preset.Sha)
|
||||||
|
require.Equal(t, "head-sha", preset.Ref)
|
||||||
|
require.Equal(t, "contributor-branch", preset.RefName)
|
||||||
|
require.Equal(t, "contributor-branch", preset.HeadRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPullRequestTargetCheckoutContextNoOpsWithoutHeadSHA(t *testing.T) {
|
||||||
|
preset := &model.GithubContext{
|
||||||
|
EventName: "pull_request_target",
|
||||||
|
Sha: "base-sha",
|
||||||
|
Ref: "refs/heads/main",
|
||||||
|
RefName: "main",
|
||||||
|
Event: map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPullRequestTargetCheckoutContext(preset)
|
||||||
|
|
||||||
|
require.Equal(t, "base-sha", preset.Sha)
|
||||||
|
require.Equal(t, "refs/heads/main", preset.Ref)
|
||||||
|
require.Equal(t, "main", preset.RefName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPullRequestTargetCheckoutContextNoOpsForOtherEvents(t *testing.T) {
|
||||||
|
preset := &model.GithubContext{
|
||||||
|
EventName: "pull_request",
|
||||||
|
Sha: "merge-sha",
|
||||||
|
Ref: "refs/pull/1/merge",
|
||||||
|
Event: map[string]any{
|
||||||
|
"pull_request": map[string]any{
|
||||||
|
"head": map[string]any{
|
||||||
|
"sha": "head-sha",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPullRequestTargetCheckoutContext(preset)
|
||||||
|
|
||||||
|
require.Equal(t, "merge-sha", preset.Sha)
|
||||||
|
require.Equal(t, "refs/pull/1/merge", preset.Ref)
|
||||||
|
}
|
||||||
|
|
||||||
func taskWithDefaultActionsURL(url string) *runnerv1.Task {
|
func taskWithDefaultActionsURL(url string) *runnerv1.Task {
|
||||||
return &runnerv1.Task{
|
return &runnerv1.Task{
|
||||||
Context: &structpb.Struct{
|
Context: &structpb.Struct{
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/internal/pkg/ver"
|
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
|
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
|
||||||
"gitea.dev/actions-proto-go/runner/v1/runnerv1connect"
|
"gitea.dev/actions-proto-go/runner/v1/runnerv1connect"
|
||||||
@@ -38,7 +36,6 @@ func New(endpoint string, insecure bool, uuid, token string, opts ...connect.Cli
|
|||||||
|
|
||||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
req.Header().Set("User-Agent", "gitea-runner/"+ver.Version())
|
|
||||||
if uuid != "" {
|
if uuid != "" {
|
||||||
req.Header().Set(UUIDHeader, uuid)
|
req.Header().Set(UUIDHeader, uuid)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,6 @@ runner:
|
|||||||
# When true (the default), fetch only the requested ref of an action repository (e.g. actions/checkout@v4) at depth 1 instead of cloning every branch's full history.
|
# When true (the default), fetch only the requested ref of an action repository (e.g. actions/checkout@v4) at depth 1 instead of cloning every branch's full history.
|
||||||
# Set to false to clone the full history.
|
# Set to false to clone the full history.
|
||||||
action_shallow_clone: true
|
action_shallow_clone: true
|
||||||
# When true (the default), inject the ACT=true environment variable into jobs.
|
|
||||||
# Set to false so workflows gated on `if: ${{ !env.ACT }}` behave like they do on GitHub.
|
|
||||||
set_act_env: true
|
|
||||||
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
|
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
|
||||||
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||||
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
|
# Find more images provided by Gitea at https://gitea.com/gitea/runner-images .
|
||||||
@@ -113,11 +110,6 @@ cache:
|
|||||||
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
|
|
||||||
# address on the runner container's network that job containers cannot reach
|
|
||||||
# when the runner creates a separate per-job network. In that case, set this
|
|
||||||
# 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.
|
# 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.
|
||||||
@@ -141,8 +133,6 @@ container:
|
|||||||
# Specifies the network to which the container will connect.
|
# Specifies the network to which the container will connect.
|
||||||
# Could be host, bridge or the name of a custom network.
|
# Could be host, bridge or the name of a custom network.
|
||||||
# If it's empty, runner will create a network automatically.
|
# If it's empty, runner will create a network automatically.
|
||||||
# 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.
|
# 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
|
||||||
@@ -178,9 +168,8 @@ container:
|
|||||||
# If it's "-", runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
|
# If it's "-", runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
|
||||||
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
||||||
docker_host: ""
|
docker_host: ""
|
||||||
# Pull docker image(s) even if already present.
|
# Pull docker image(s) even if already present
|
||||||
# Defaults to false when the key is omitted.
|
force_pull: true
|
||||||
force_pull: false
|
|
||||||
# Rebuild docker image(s) even if already present
|
# Rebuild docker image(s) even if already present
|
||||||
force_rebuild: false
|
force_rebuild: false
|
||||||
# Always require a reachable docker daemon, even if not required by runner
|
# Always require a reachable docker daemon, even if not required by runner
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ type Runner struct {
|
|||||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
||||||
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
||||||
ActionShallowClone *bool `yaml:"action_shallow_clone"` // ActionShallowClone fetches only the requested ref of an action repository at depth 1 instead of cloning every branch's full history. It is a pointer to distinguish between false and not set; if not set, it defaults to true.
|
ActionShallowClone *bool `yaml:"action_shallow_clone"` // ActionShallowClone fetches only the requested ref of an action repository at depth 1 instead of cloning every branch's full history. It is a pointer to distinguish between false and not set; if not set, it defaults to true.
|
||||||
SetActEnv *bool `yaml:"set_act_env"` // SetActEnv controls whether the ACT=true environment variable is injected into jobs. It is a pointer to distinguish between false and not set; if not set, it defaults to true. Set it to false so workflows gated on `if: ${{ !env.ACT }}` behave like on GitHub.
|
|
||||||
AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends.
|
AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends.
|
||||||
PostTaskScript string `yaml:"post_task_script"` // PostTaskScript is the path to an executable script run on the host after each task's cleanup completes. Empty disables the hook. On Windows use .exe/.bat/.cmd; PowerShell (.ps1) is not supported yet as the configured path.
|
PostTaskScript string `yaml:"post_task_script"` // PostTaskScript is the path to an executable script run on the host after each task's cleanup completes. Empty disables the hook. On Windows use .exe/.bat/.cmd; PowerShell (.ps1) is not supported yet as the configured path.
|
||||||
PostTaskScriptTimeout time.Duration `yaml:"post_task_script_timeout"` // PostTaskScriptTimeout caps how long the post-task script may run. Default is 5m when post_task_script is set.
|
PostTaskScriptTimeout time.Duration `yaml:"post_task_script_timeout"` // PostTaskScriptTimeout caps how long the post-task script may run. Default is 5m when post_task_script is set.
|
||||||
@@ -157,20 +156,13 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
b := true
|
b := true
|
||||||
cfg.Runner.ActionShallowClone = &b
|
cfg.Runner.ActionShallowClone = &b
|
||||||
}
|
}
|
||||||
if cfg.Runner.SetActEnv == nil {
|
|
||||||
b := true
|
|
||||||
cfg.Runner.SetActEnv = &b
|
|
||||||
}
|
|
||||||
if cfg.Cache.Enabled == nil {
|
if cfg.Cache.Enabled == nil {
|
||||||
b := true
|
b := true
|
||||||
cfg.Cache.Enabled = &b
|
cfg.Cache.Enabled = &b
|
||||||
}
|
}
|
||||||
if *cfg.Cache.Enabled {
|
if *cfg.Cache.Enabled {
|
||||||
if cfg.Cache.Dir == "" {
|
if cfg.Cache.Dir == "" {
|
||||||
home, err := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cache.dir is unset and the user home directory could not be determined: %w", err)
|
|
||||||
}
|
|
||||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||||
}
|
}
|
||||||
if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" {
|
if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" {
|
||||||
@@ -181,10 +173,7 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
cfg.Container.WorkdirParent = "workspace"
|
cfg.Container.WorkdirParent = "workspace"
|
||||||
}
|
}
|
||||||
if cfg.Host.WorkdirParent == "" {
|
if cfg.Host.WorkdirParent == "" {
|
||||||
home, err := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("host.workdir_parent is unset and the user home directory could not be determined: %w", err)
|
|
||||||
}
|
|
||||||
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
cfg.Host.WorkdirParent = filepath.Join(home, ".cache", "act")
|
||||||
}
|
}
|
||||||
if cfg.Runner.FetchTimeout <= 0 {
|
if cfg.Runner.FetchTimeout <= 0 {
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Size limits for the outputs reported to the server.
|
|
||||||
const (
|
|
||||||
maxOutputKeyLen = 255
|
|
||||||
maxOutputValueLen = 1024 * 1024 // 1 MiB
|
|
||||||
)
|
|
||||||
|
|
||||||
type Reporter struct {
|
type Reporter struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -396,15 +390,13 @@ func (r *Reporter) SetOutputs(outputs map[string]string) {
|
|||||||
defer r.stateMu.Unlock()
|
defer r.stateMu.Unlock()
|
||||||
|
|
||||||
for k, v := range outputs {
|
for k, v := range outputs {
|
||||||
if l := len(k); l > maxOutputKeyLen {
|
if len(k) > 255 {
|
||||||
log.Warnf("ignore output %q because the key is too long: %d > %d", k, l, maxOutputKeyLen)
|
r.logf("ignore output because the key is too long: %q", k)
|
||||||
r.logf("ignore output %q because the key is too long: %d > %d", k, l, maxOutputKeyLen)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if l := len(v); l > maxOutputValueLen {
|
if l := len(v); l > 1024*1024 {
|
||||||
log.Warnf("ignore output %q because the value is too long: %d > %d", k, l, maxOutputValueLen)
|
log.Println("ignore output because the value is too long:", k, l)
|
||||||
r.logf("ignore output %q because the value is too long: %d > %d", k, l, maxOutputValueLen)
|
r.logf("ignore output because the value %q is too long: %d", k, l)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if _, ok := r.outputs.Load(k); ok {
|
if _, ok := r.outputs.Load(k); ok {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1020,24 +1020,11 @@ func TestReporter_SetOutputs(t *testing.T) {
|
|||||||
got, _ = r.outputs.Load("foo")
|
got, _ = r.outputs.Load("foo")
|
||||||
assert.Equal(t, "bar", got)
|
assert.Equal(t, "bar", got)
|
||||||
|
|
||||||
// keys longer than maxOutputKeyLen are dropped
|
// keys longer than 255 chars are dropped
|
||||||
longKey := strings.Repeat("k", maxOutputKeyLen+1)
|
longKey := strings.Repeat("k", 256)
|
||||||
r.SetOutputs(map[string]string{longKey: "v"})
|
r.SetOutputs(map[string]string{longKey: "v"})
|
||||||
_, ok = r.outputs.Load(longKey)
|
_, ok = r.outputs.Load(longKey)
|
||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
|
|
||||||
// values longer than maxOutputValueLen are dropped
|
|
||||||
longValue := strings.Repeat("v", maxOutputValueLen+1)
|
|
||||||
r.SetOutputs(map[string]string{"big": longValue})
|
|
||||||
_, ok = r.outputs.Load("big")
|
|
||||||
assert.False(t, ok)
|
|
||||||
|
|
||||||
// a value at exactly the limit is still stored
|
|
||||||
maxValue := strings.Repeat("v", maxOutputValueLen)
|
|
||||||
r.SetOutputs(map[string]string{"atlimit": maxValue})
|
|
||||||
got, ok = r.outputs.Load("atlimit")
|
|
||||||
require.True(t, ok)
|
|
||||||
assert.Len(t, got, maxOutputValueLen)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReporter_EffectiveCloseTimeout(t *testing.T) {
|
func TestReporter_EffectiveCloseTimeout(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user