1. Restructures the README into installation, quickstart, deployment, configuration, jobs and operation, and shortens it to user-facing prose. Option details are left to `config.example.yaml` and `docs/`, existing heading anchors are kept where possible.
2. FreeBSD builds used the Docker stub, so `docker://` labels, services and Docker actions failed there. The Docker code works on FreeBSD against a remote daemon, so it is enabled, and host jobs report `runner.os` as `FreeBSD`.
3. `make build` on Windows now writes `gitea-runner.exe`, taking the suffix from `go env GOEXE`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1367
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
In `scripts/s6/docker/run`, `s6-notifyoncheck` runs without the `-d` flag:
```bash
exec s6-notifyoncheck -n 0 dockerd-entrypoint.sh
```
Without `-d`, `s6-notifyoncheck` performs a single fork to execute the background checking process. The parent immediately replaces itself via `exec` with `dockerd-entrypoint.sh` (which calls `exec rootlesskit` on `dind-rootless`).
Because only a single fork occurred, the background checking child remains parented to `rootlesskit`. When the check passes and the process terminates:
1. `rootlesskit` did not spawn this process and never reaps it via `waitpid()`.
2. Under standard Linux process semantics, `s6-svscan` (PID 1) cannot adopt or reap the child because its parent (`rootlesskit`) is still running.
3. The terminated process remains as a permanent zombie (`[s6-notifyoncheck] <defunct>`) for the container's entire lifespan.
Adding `-d` instructs `s6-notifyoncheck` to double-fork, detaching the background check so it is immediately adopted by `s6-svscan` (PID 1) and cleanly reaped upon exit.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1364
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: adamaru <232090+adamaru@noreply.gitea.com>
Follow-up to https://gitea.com/gitea/runner/pulls/1362, whose crash e2e missed because it only drove the daemon on Docker.
- `dispatch`, `failure_paths` and `reusable_workflow` run through the daemon and `exec`, each on Docker and one shared k3s cluster.
- Fixed sleeps become gates the test releases, the job timeout uses a 15s runner deadline, k3s boots alongside Gitea, Gitea's data is on tmpfs.
- Outside a container, `make test-e2e` runs `go test` in a Linux container on its own network like CI, so local runs take the same paths.
Local wall time:
| | Before | After |
|---|---|---|
| Suite | 84s | 27–35s |
| `job_timeout` | 64s | 17s |
| `reconnect` | 22s | 4.5s |
| `capacity/serial` | 20s | 2s |
| `cli_shutdown_drains` | 14s | 2.3s |
| `kubernetes` | 31s | 21–23s |
_Written by Claude (Opus 5.5)._
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1363
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The Kubernetes backend copies actions and workspaces into job containers with `tar` over `pods/exec`, so every byte goes through the API server. On managed clusters that often sits in other zones than the nodes, so each byte can cross zones twice, and the copies load the API server.
With `kubernetes.transfer.mode: auto` or `direct`, an init container copies `gitea-runner` into the job pod, and the job container fetches directories and copies of 64 KiB or more gzipped from the runner over the pod network. `auto` falls back to the API server for a job pod that cannot reach the runner, `direct` fails the job.
Copies can hold secrets, so each is served once, over TLS 1.3 pinned to a certificate the runner generates, only to the job pod's IPs.
Copying setup-go@v5, cache@v4, setup-java@v4 and build-push-action@v6 sends 11.6 MB through the API server on main and 532 B with this PR.
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1354
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
`gitea-runner exec --no-skip-checkout` panics with a nil pointer dereference on any job that calls a local reusable workflow (`uses: ./.gitea/workflows/x.yml`).
Fetching a local reusable workflow from the instance was gated on `NoSkipCheckout`, but it reads the repository and SHA from the task's preset context, which only the daemon has. `exec` always has the workflow on disk, so the fetch is now gated on the preset instead, and `--no-skip-checkout` only governs checkout again. Daemon behavior is unchanged.
The `exec` e2e test now also runs the existing reusable caller/callee pair with `--no-skip-checkout`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1362
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Cache entries were only scoped by repository, so an entry a pull request saved could be restored on the default branch, for example through a `restore-keys` prefix.
Entries now carry the ref of the job that saved them. As on GitHub ([Restrictions for accessing a cache](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache)), a job restores entries of its own ref first, then of its pull request's base branch, then of the default branch. Pull request events are scoped by their pull request number.
Entries saved before this change are no longer restored and expire through `cache.retention`, so caches start cold once after the upgrade. Runners sharing a cache, and an external `cache-server`, should be upgraded together.
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1361
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: JorgeDLMV <jormoreva@gmail.com>
Closes https://gitea.com/gitea/runner/issues/1317, which makes https://github.com/go-gitea/gitea/issues/39813 redundant as the runner sets these variables.
Every built-in `GITHUB_*` environment variable the runner sets is now also available as `GITEA_*` with the same value, so Gitea-native workflows don't have to rely on GitHub naming. The `GITEA_*` names mirror the `GITHUB_*` ones after their last write, so they stay equal and are unset together.
Workflow, job and runner `envs` values for these `GITEA_*` names are now overwritten, the same as for `GITHUB_*`.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1358
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: KenanZhu <imnanoki@qq.com>
`github.action_repository`/`github.action_ref` and their env vars now belong to the step executing an action, like on GitHub: set for remote actions, empty for `run`, `docker://` and local steps. Before, a local action nested in a remote composite resolved `$/` against the remote action instead of the workflow's repository.
Updates actionslib to v1.5.1, which accepts `$/` as the repository root action.
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1359
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
When the probe's deadline expired during `ContainerCreate`, dockerd still created the container but the probe never got its ID, leaving it in `Created` with its image's anonymous volumes. A slow daemon leaked one per job.
The probe now runs detached with its own timeout, so the create is never cancelled and the container is always removed. Callers still fall back after their own deadline, and the runner waits for pending probes before exiting.
Fixes: https://gitea.com/gitea/runner/issues/1353
Reviewed-on: https://gitea.com/gitea/runner/pulls/1357
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
The Kubernetes backend copies actions and other files into the job container with `tar` over `pods/exec`, so the data goes through the API server, which on managed clusters often sits in other zones than the nodes (each copied byte then crosses zones twice). Actions are megabytes of JavaScript bundles that gzip about fivefold:
| Action | tar | gzip |
|---|---|---|
| actions/setup-go@v5 | 8.0 MB | 1.5 MB |
| actions/cache@v4 | 11.6 MB | 2.0 MB |
| actions/setup-java@v4 | 15.3 MB | 2.5 MB |
| docker/build-push-action@v6 | 12.9 MB | 3.0 MB |
The startup probe now also reports whether the job container has `gzip`. If it does, `CopyDir` and copies of 64 KiB or more are gzipped at `BestSpeed` and extracted with `gzip -dc | tar -xf -`. Smaller copies (run scripts, file command files) stay uncompressed, where gzip would save next to nothing and cost a process in the pod. Images without `gzip` keep the plain tar stream, so the image requirements do not change.
This is independent of #1354 and gets most of its traffic saving with no new moving parts.
---
*Written by Codet (AI coding agent) on behalf of @lunny.*
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1355
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Extends the e2e suite with scenarios for host labels, local `workflow_call`, label routing, `runner.capacity`, a dropped and restored connection to Gitea, and the daemon's `--once`, `--labels` and `shutdown_timeout` behaviour.
Stacked on https://gitea.com/gitea/runner/pulls/1346, so its commit shows here until that merges. The new scenarios pass locally against gitea/gitea:latest and main-nightly.
Assisted-by: Codet:codet
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1347
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
`exec` forwarded the toolkit's ArtifactService calls to an instance it never registered, and its random runtime token held no backend IDs, so `actions/upload-artifact` and `actions/download-artifact` v4 and later failed.
The local artifact server now serves the v4 API, including the single Put Blob uploads docker/build-push-action uses. `exec` gives each workflow its own unsigned JWT runtime token and artifact run, so artifact names conflict and carry over between runs as on GitHub. Artifacts default to a temporary directory instead of the workspace that jobs copy.
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1341
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
The e2e suite started the runner in-process and never exercised failing jobs. This adds a scenario that builds the binary and drives `register` then `daemon` against the Gitea fixture, and one for failed, skipped, continue-on-error and timed-out jobs.
Verified locally against gitea/gitea:main-nightly.
Assisted-by: Codet:codet
Reviewed-on: https://gitea.com/gitea/runner/pulls/1346
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
`uses:` steps and job hooks kept their "Run …" group open until they finished. Gitea doesn't nest groups, so the action's own first `::group::` closed the header and everything before it stayed hidden there, e.g. all of goreleaser's output in https://gitea.com/gitea/cli/actions/runs/1121069/jobs/1519275. The header group now closes right after its details, as in actions/runner.
The action header also matches actions/runner's otherwise now: it is titled by the `uses:` reference, lists the resolved inputs including `action.yml` defaults without empty ones, is shown for pre stages too, and post stages start with `Post job cleanup.`.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1345
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Without a docker daemon six `act/runner` tests failed instead of passing. A dry run still created and removed job networks, the job teardown dropped the dry-run flag along with every other context value, and the container mocks asked the daemon for the runner arch.
This also speeds up the tests and CI without dropping assertions. Most of the time went to git and docker round trips, which are now pinned, batched or coalesced, and the dind and e2e steps cache or overlap more of their setup. On CI, `act/runner` went from 77s to 55s and `test-dind` from 108s to 55s. Locally, `act/runner` without docker went from 53s to 7s.
Split out of https://gitea.com/gitea/runner/pulls/1348, which adds Windows CI on top. The go cache changes moved to https://gitea.com/gitea/runner/pulls/1350.
Written by Claude Code.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1349
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The toolkit patch rewrote bundles in place in the shared action cache. Since https://gitea.com/gitea/runner/pulls/1316 trees are never refreshed, so a bundle left truncated failed every later job using it.
Patched bundles now only overwrite the job container's copy, so the runner no longer writes to an extracted tree.
Extraction records each file's size beside the tree, and a tree that no longer matches is fetched again, offline mode included, as a missing one always was. This also catches damage from outside the runner, such as storage losing a write, for a stat per file, a fraction of the copy each stage already makes. Trees from older runners have no record and download once more, except in offline mode, which keeps using them as before.
Generated with Claude Code.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1352
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Like Gitea's CI, only a `cache-seeder` workflow on main saves the go caches, all other jobs only restore them. Caches are scoped per repository, so until now any pull request, forks included, could write a cache that other runs restore. The seeder compiles the tests without running them, so no test result is ever cached.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1350
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Contains a number of small bug fixes where jobs behaved differently from GitHub or failed silently.
1. Hooks fail on a refused `::set-env::` or `::add-path::` and no longer inherit the last step's identity
1. Docker build, exec and wait errors are returned instead of swallowed, token-only registry credentials work, logs of `--tty` services decode
1. Env files split lines like GitHub on each OS, so a carriage return is kept on Linux and CRLF ends a line on Windows, empty heredoc names or delimiters are rejected
1. Post steps run after their pre step even when main was skipped, composites see undeclared inputs, `runs.args` wins without `with.args` being parsed
1. Docker actions skip evaluating defaults of inputs given in `with` and read `entryPoint` and `args` in any case, a lowercase `dockerfile` is found, undeclared inputs get GitHub's warning
1. A user-set `CI` is kept, `GITHUB_REPOSITORY_ID` and the other missing `GITHUB_*` variables are exported
1. `timeout-minutes` can read the step's own `env`, a step-level `ACTIONS_ALLOW_UNSECURE_COMMANDS: false` beats a job-level `true` while the runner's own opt-in still wins, `pwsh` quotes the script path
1. A step `env:` given as one expression such as `${{ fromJSON(matrix.env) }}` is applied, `hashFiles` fails with GitHub's messages on script errors and after 120 seconds, a job image without node still gets an empty hash
1. A step whose command files cannot be reset fails instead of importing stale ones, a timeout between steps fails the job instead of reporting success
1. One named volume can be mounted at several paths, host-mode PTY steps keep their last output when they fail, host scratch directories are removed when job setup fails
1. Cancelling a step right after it starts kills its whole process tree
1. Cache prefix restores pick the newest entry created within the same second, also on S3, cache commits with a wrong size are rejected
1. Artifact chunks are written at their offset, download URLs are escaped, listing a run without artifacts no longer crashes
1. Poll backoff reaches `fetch_interval_max`, proxy passwords are masked in more forms, secrets are also masked in step names and in their quoted form in the runner log, a failed final report is logged
1. On Windows hosts a differently cased unsecure-commands opt-in or proxy variable counts, and an early cancel no longer leaks a job object handle
1. Interactive `register` prompts for a missing token and stops on SIGTERM, `exec --list --detect-event` no longer panics without events, `config set` works on `cache.s3` keys
Reviewed-on: https://gitea.com/gitea/runner/pulls/1338
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A new node can run pods before its kubelet has a serving certificate, so the first exec into the job container fails with `error dialing backend: remote error: tls: internal error` and the job fails during setup.
That exec only reads the arch and PATH, so it now retries for up to a minute unless the API server rejects it (4xx) or the command ran.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1337
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Removes dead code and redundant logic and consolidates tests, so there is less to read and maintain. No behavior change.
1. **act/runner**: inline single-use wrappers, compile regexes once instead of per call, embed the trampoline as a string, drop the unused legacy result of `parseActionCommand`
2. **act/container**: `sanitizeConfig` and `mergeContainerConfigs` stop returning the configs they already mutate in place, drop never-set option fields and the unused `user` parameter of `ExecWithCmdLine`
3. **act/common and cache server**: drop the unused `Trace`/`Tracef` loggers and the short SHA of `FindGitRevision`, `RegisterJob` releases through `RevokeJob`
4. **daemon, poller and reporter**: one shutdown wait for both poll modes, drop an unreachable fetch timeout branch, guard the reporter's last report time with its existing lock instead of an atomic
5. **build**: drop unused Makefile and release workflow variables, collapse the goreleaser ignore list to the same build targets
6. **tests**: fold near-duplicate tests into tables with shared setup
Throughout, hand-rolled logic gives way to `cmp.Or`, `strings.Cut`, `slices` and `min`/`max`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1335
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
1. Replace bolthold with direct bbolt access in the cache server. bolthold has not changed since March 2024 and kept `encoding/gob` in the binary although the store was already JSON. The on-disk layout stays the same, so existing caches keep working, and the index buckets bolthold left behind are dropped on the first sweep. Queries now run against an in-memory copy of the entries that is only re-read after another process wrote to the store, so lookups and sweeps no longer decode the whole store.
2. Replace gofakes3 with a small in-memory S3 fake in the one test that used it, which removes aws-sdk-go-v2, afero, mgo and x/tools from the module graph.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1336
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Migrates the runner to `connectrpc.com/connect/v2` by pinning actionslib v1.5.0, which carries https://gitea.com/gitea/actionslib/pulls/28. The wire behaviour towards Gitea is unchanged and works against Gitea servers on connect v1 and v2. The client keeps v1's unlimited response size, and the error codes and messages the runner checks stay the same.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1325
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Commands the runner applies itself, like the `::set-output::` older actions still print, showed up raw in the job log. The reporter also re-parsed every line with one job-wide stop-commands and echo state, while act keeps one command handler per step, stage, composite child and hook, as GitHub does. A `::stop-commands::` left active in one of them kept the reporter suppressed for the others, so their `::add-mask::` values were never registered, and post steps logged command lines twice.
- act's command handler decides what reaches the log, as GitHub's runner does: applied commands only under `ACTIONS_STEP_DEBUG` or `::echo::on`, `::stop-commands::` and its resume line shown with long tokens masked
- Masks go straight to the reporter, which no longer parses command lines
- One parser follows GitHub's rules: leading whitespace, `##[...]` anywhere in a line, registered commands only
- Service logs honour only `::add-mask::`
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1333
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Patch the scripts beside each node action entrypoint too, so code-split bundles like rust-cache v2.9.2, setup-java v6 and setup-deno v2 reach the v2 cache API. Entrypoints may point anywhere in the action's repository, as `gradle/actions/setup-gradle` does with `../dist`.
The `.LOCALHOST` match is now anchored on the `.GHE.COM` test before it, so another library's uppercase localhost check is never rewritten.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1328
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
- Look up containers by name instead of listing all of them
- On Linux, cache the docker proxy discovery instead of starting a probe container per job
- Download a job's actions concurrently and share identical fetches between concurrent jobs
- Cache `runner.arch` per run and poll service health every 250ms
- Cache server: queue on an in-process lock instead of polling the database file lock, upload S3 objects up to 64MiB in one request and larger ones in 4 concurrent parts
| `exec`, median of 5 | Before | After |
|---|---:|---:|
| 1 step | 1.70s | 0.87s |
| 10 steps | 2.84s | 1.97s |
| Service + step | 3.78s | 2.18s |
| checkout + Node action | 3.64s | 2.52s |
Reviewed-on: https://gitea.com/gitea/runner/pulls/1327
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Renovate gets no release timestamps for Docker Hub images with more than 1000 tags (https://github.com/renovatebot/renovate/issues/46925) or for quay.io, so the 5-day minimum release age holds them back forever. The e2e workflows, the Kubernetes examples and the toolkit test pins are outside its managers, so these are bumped by hand.
The builder follows the current Alpine instead of the frozen 3.23.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1330
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
After https://gitea.com/gitea/runner/pulls/1304, logrus stayed only to forward logs from docker/cli and moby/go-archive. The runner used a small part of those: volume parsing, the build context tar, the `ssh://` connection and registry credentials. Own code that matches them replaces them, so logrus leaves the module graph. docker/cli stays for its logrus-free `opts` and `connhelper/ssh` packages.
Other dependencies that the standard library or existing ones cover go too: mergo, httprouter (replaced by `http.ServeMux`), retry-go, go-isatty, go-selinux, container-device-interface, gotest.tools and go-cmp (replaced by testify). The binary shrinks by 5%.
Behaviour differences:
1. An unreadable file in a build context is skipped with a warning, where go-archive left a truncated tar.
2. Build contexts no longer carry `security.capability` xattrs.
3. The cache and artifact servers answer HEAD on GET routes, and no longer redirect trailing slashes or answer OPTIONS.
*Written by Claude.*
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1319
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
On Kubernetes a service is reached by its id made a DNS label, so some service ids might become unreachable. For example, the `devstoreaccount1.azurite.local` service of Gitea's [MSSQL tests](2fe74d3cdd/.github/workflows/pull-db-tests.yml (L229)) is only in the pod's hosts as `devstoreaccount1-azurite-local`, while the tests connect to the id as written. Container names must be DNS labels, but `hostAliases` accepts DNS subdomains, so an id that is one is now added there too, and to `no_proxy`, as on Docker where a service is reached by its id.
The warning for a service not reachable by its id now also covers ids that differ only in case, as Kubernetes hostnames are lowercase.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1320
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Job teardown ran every step on one shared minute. When removing the job container took longer, the network removal ran on an expired context and the network stayed behind until the orphan reclaim, which only takes networks older than `workdir_cleanup_age`, 24h by default. All that time it held a subnet of the daemon's address pool.
The job and service containers are now removed in parallel, and the docker proxy sweep, the network and the volumes get a fresh budget after them. The idle reclaim takes unused networks and volumes once they are 10 minutes old, and network removal treats a network that is already gone as removed.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1318
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Each step refreshed its own clone of an action, so a composite using one action repository ten times fetched it ten times per job, and a single network error failed the job.
Like actions/runner, a job now downloads each action repository and ref once, as an archive extracted into a directory per commit. Each action still gets its own copy in the job container. This restores the per-job dedupe of https://gitea.com/gitea/runner/pulls/1178, which only reached a since-deleted branch.
How a download works:
1. The action URL is taken after `insteadOf` rewriting, as git would use it.
2. The archive comes from `<url>/archive/<ref>.tar.gz`, or from codeload.github.com for github.com, as for actions/runner.
3. A cached tag or branch is revalidated with its ETag, which GitHub and Gitea 28+ answer with a 304. A cached commit pin needs no request.
4. Transient failures (5xx, 408, 429 honouring `Retry-After`, dropped transfers) are retried three times with a 10 to 30 second backoff.
5. Git takes over, cloning and exporting the commit into the same directory layout, when the URL is not HTTP(S), the ref is a short commit, the host serves no usable archive, the host cannot be connected to, or the retries run out.
Median of 3 runs, git (shallow clone, then fetch) against archives, on a cold and a warm cache:
| Action | git cold | git warm | archive cold | archive warm |
|---|---|---|---|---|
| github.com actions/checkout@v4 | 937ms | 726ms | 286ms | 171ms |
| github.com actions/cache@v4 | 1006ms | 801ms | 516ms | 161ms |
| github.com actions/setup-node@v4 | 1046ms | 786ms | 463ms | 156ms |
| gitea.com actions/checkout@v4 | 2007ms | 1165ms | 667ms | 222ms |
The action cache layout changes, so existing caches download once more.
*Written by Claude.*
Fixes: https://gitea.com/gitea/runner/issues/1220
Reviewed-on: https://gitea.com/gitea/runner/pulls/1316
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The runner's own messages between steps (not step output) were uploaded to the job log at `log.level`, so raising `log.level` for local debugging also changed what workflow authors see in Gitea, and lowering it hid messages from them.
Add `runner.log_report_level` to set that independently. Unset, it still follows `log.level`, so existing setups behave as before. An invalid value is rejected when the config loads.
Assisted-by: Codet
Assisted-by: Claude Code:claude-opus-5-5
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1310
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
The action cache refresh fetched with `--prune` and the unqualified refspec `+<ref>:refs/runner/<hash>`. Git cannot map that ref back to a remote ref, so it deleted it on every other refresh. The first job after a branch or tag moved then failed with `cannot lock ref`, and a moved tag in a full clone could silently run its old commit. `--no-prune` fixes this and also overrides an inherited `fetch.prune`.
Fixes: https://gitea.com/gitea/runner/issues/1311
Reviewed-on: https://gitea.com/gitea/runner/pulls/1312
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
actionslib v1.4.1 no longer logs through logrus, so the workflow model no longer needs the logrus bridge. The bridge stays for docker/cli and moby/go-archive, which still log through logrus's global logger; the comment now says so.
actionslib now logs through slog's global logger, which the runner did not capture. A new slog bridge forwards it to the process-wide logger, so its messages keep the runner's level, format and secret masking.
Assisted-by: Codet
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1313
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
A runner nested behind another runner's docker proxy, as in gitea.com CI, had its `com.gitea.runner.job` label overwritten by the outer proxy, so its job cleanup removed nothing. This failed `TestDockerProxyWithDaemon` depending on the CI host, for example on https://gitea.com/gitea/runner/pulls/1312.
Each proxy now sets `com.gitea.runner.job.<job>` and cleans up by that key, so nested proxies keep each other's. This replaces the `com.gitea.runner.job=<job>` label.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1314
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Gitea derives the steps of a job from the workflow file, so the `pre` and `post` steps of actions only show up inside "Set up job" and "Complete job", see https://github.com/go-gitea/gitea/issues/24604.
The runner now also reports the steps as it runs them (https://gitea.com/gitea/actions-proto-def/pulls/19): "Set up job", a "Pre" and "Post" step for each action that has one, the workflow steps with their evaluated names, and "Complete job", as actions/runner shows them. The per-step state Gitea reads today is unchanged, and the job log only moves "Complete job name" and "Matrix:" ahead of the pre steps, so older Gitea versions keep showing everything as before. Job hooks and containers stay part of "Set up job" and "Complete job".
The `replace` of `gitea.dev/actionslib` is swapped for the release once the proto change is merged.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1293
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Replace go-git in `exec --use-gitignore` filtering with `git ls-files`. The git CLI is already required for action clones since https://gitea.com/gitea/runner/pulls/1277, so this drops `go-git` and 14 other modules, and filtering now follows git's own ignore rules.
Also stop deleting a remote action's `.gitignore` before copying it. Tracked files are never filtered, so the deletion only made copied actions differ from GitHub's action tarballs.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1309
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Log through `log/slog`, with a console `slog.Handler` that writes the text the runner has always written (sorted `key=value` fields on a plain stream, a colored prefix on a terminal), so nothing changes for users. I compared its output byte for byte with logrus's `TextFormatter` over plain, colored, timestamped and caller cases.
Why not `slog.TextHandler`: its output differs from what users and their log parsers see today, and `ReplaceAttr` cannot close the gap.
- level is `WARN` where logrus wrote `warning`, and time has milliseconds and no quotes
- fields keep call order instead of being sorted
- the caller is one `source=` key, where the runner writes `func=` and `file=` (reshaped by `callPrettyfier`), and `ReplaceAttr` returns a single attribute
- no colored prefix on a terminal
- no hook to rewrite a whole formatted line, which the process-wide secret masking relies on
Libraries that log through logrus's global logger (the workflow model, the Docker client) are forwarded to the process-wide logger, so they keep following its level, format and secret masking. This is why logrus stays in `go.mod`.
Stacked on https://gitea.com/gitea/runner/pulls/1303.
Assisted-by: Codet
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1304
Reviewed-by: bircni <bircni@icloud.com>
Move the log calls in `internal` onto `logging`, and add what the process-wide logger setup and its tests need: `SetTextFormat` with a line filter for secret masking (replacing `report.MaskingFormatter`), `SetReportCaller`, `SetDefault` and `logtest.SetDefault`. After this only the `logging` package imports logrus.
Stacked on https://gitea.com/gitea/runner/pulls/1301.
Assisted-by: Codet
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1302
Reviewed-by: bircni <bircni@icloud.com>
Add `act/common/logging`: a printf-style `Logger` on a slog-shaped `Handler`, with a logrus-backed handler as the only backend, plus a `logtest` handler for tests. Move the job logger onto it: `common.Logger` returns `*logging.Logger`, the job log formatters and the `Reporter` hook become handlers, and composite step IDs travel in the context.
Logging is tightly coupled to logrus types (hooks, formatters, `Entry.Data`). Going through our own interface first lets the remaining call sites move over with unchanged behavior, and lets the backend be swapped for `log/slog` in one place afterwards. The global process logger and its masking formatter stay on logrus for now.
Assisted-by: Codet
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1295
Reviewed-by: bircni <bircni@icloud.com>
- Keep `GITHUB_ENV`, `GITHUB_OUTPUT`, `GITHUB_STATE` and `GITHUB_PATH` when a job times out or is cancelled, so post steps still get their state
- Retry the startup Declare while Gitea is unreachable instead of exiting, still failing fast on rejected registrations, auth and misconfiguration
- Reuse a local image when a platform variant such as `linux/arm/v7` is requested instead of pulling again
- Read `PATH` from the created job container so cross-architecture jobs get tool paths for the right architecture
- Extract action copies relative to the act volume so they never cross an image's `/var/run` symlink, which rootless Podman rejects
- Keep the job container's entrypoint when container options set `--entrypoint`, warning instead of appending it to the keep-alive command
- Lock action cache entries across runner processes that share a cache directory
- `exec`: omit `GITHUB_SERVER_URL` and `GITHUB_API_URL` without `--gitea-instance` instead of exporting a bare `https://`, and read owner and repository from any origin remote
Reviewed-on: https://gitea.com/gitea/runner/pulls/1294
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Sends the OTLP metrics under [OpenTelemetry semantic convention](https://opentelemetry.io/docs/specs/semconv/) names where the runner's data matches the definition, like `cicd.worker.count`, `cicd.system.errors` and the `go.*` and `process.*` runtime metrics, and under `gitea.runner.*` otherwise. OTel dashboards then work without remapping, as they already do for the traces from https://gitea.com/gitea/runner/pulls/1206.
Adds a `gitea_runner_state` gauge (`busy`, `idle`, `unavailable`), and `/readyz` now fails once the runner shuts down.
**Breaking**
1. OTLP metric names, units, types and attribute values change, and the info and start time metrics move to resource attributes.
1. Prometheus `gitea_runner_client_errors_total` gains a `code` label, and `gitea_runner_job_total` counts jobs stopped by `runner.timeout` as `timeout` instead of `failure`.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1282
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: wmTJc9IK0Q <231276+wmtjc9ik0q@noreply.gitea.com>
The Docker proxy labels every container, network and volume a job creates and removes them when the job ends, which breaks jobs that deploy long-running services to the runner's own Docker daemon. Document that behavior and add `container.sweep` (default `true`) so operators who deploy to the same daemon can opt out. Kept resources stay labeled with `com.gitea.runner.job`.
Fixes: https://gitea.com/gitea/runner/issues/1243
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1275
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Add `runner.client_cert_file` and `runner.client_key_file` for Gitea instances that require mutual TLS. The runner presents the certificate on its RPCs, job summary uploads, and the action, reusable workflow and `builtin:checkout` clones from the instance. The files are read on use, so renewed certificates apply without a restart. Job steps do not get them. An incomplete or mismatched pair fails startup.
Clones now decide the token, client certificate and `runner.insecure` with one host check, which also gives reusable workflow clones the TLS settings they lacked. `runner.insecure` still only covers the registered address.
Fixes: https://gitea.com/gitea/runner/issues/321
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1273
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Config values can reference the runner's environment as `${NAME}`, for example `capacity: ${RUNNER_CAPACITY}`, so container deployments can configure the runner without templating the file. An unset `NAME` fails the load instead of silently becoming empty, and `$${NAME}` writes a literal `${NAME}`. Keys are never expanded.
Breaking: an existing value containing `${NAME}`, such as a shell command in a Kubernetes `pod_template`, now needs `$${NAME}`.
Closes https://gitea.com/gitea/runner/issues/551
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1274
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Add `runner.extra_headers` for reverse proxies that require request headers on the runner's API requests to Gitea, like registration, polling and log and status reports.
Headers the runner sets itself (authentication, User-Agent, protocol headers) stay authoritative. Extra headers are dropped when a redirect changes the hostname or leaves HTTPS, while the runner's own authentication headers follow redirects as before. Invalid or case-insensitively duplicated header names are rejected at config load.
Fixes: https://gitea.com/gitea/runner/issues/171
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1269
Co-authored-by: bircni <bircni@icloud.com>
Allow `valid_volumes` entries ending in `:ro` to permit only read-only mounts, so a workflow can no longer mount writable a host path the runner only shares read-only, like a certificate store. `**:ro` allows any volume read-only. An entry without `:ro` that matches too still allows writable mounts.
A named volume allowed only through `:ro` is mounted with `nocopy`, because Docker otherwise fills an empty volume from the image even when it is mounted read-only.
Fixes: https://gitea.com/gitea/runner/issues/407
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1272
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
The runner now uses the `git` command line instead of go-git to download actions and reusable workflows. go-git had recurring bugs and does not support SHA-256 repositories, and full clones of actions are several times faster.
`builtin:checkout` now runs `git` where the job runs and behaves like `actions/checkout`:
1. A branch is checked out as a local branch tracking `origin`, tag and pull request runs keep their ref, and a triggering tag that moved fails the step.
1. Later steps can fetch and push with the persisted token or SSH key.
1. It supports the `ssh-*`, `lfs`, `submodules`, `sparse-checkout`, `filter`, `fetch-tags`, `clean`, `persist-credentials`, `show-progress` and `set-safe-directory` inputs and the `ref` and `commit` outputs.
1. SHA-256 repositories work for actions, reusable workflows and `builtin:checkout`.
Git keeps the token off its command line, ignores `GIT_DIR` and similar variables inherited by the runner, and keeps action files LF regardless of `core.autocrlf`. A cancelled job lets git remove its lock files.
This is a breaking change: the runner needs Git 2.34.1 or newer on its `PATH`, and so do job images that use `builtin:checkout`. The official runner images include it.
Fixes: https://gitea.com/gitea/runner/issues/1077
Fixes: https://gitea.com/gitea/runner/issues/1267
Fixes: https://gitea.com/gitea/runner/issues/1279
Fixes: https://gitea.com/gitea/runner/issues/1284
Fixes: https://gitea.com/gitea/runner/issues/1285
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1277
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Support tmpfs in container `options` via `--mount type=tmpfs,...`, like GitHub does. The `valid_volumes` check silently dropped these mounts because they have no source. A tmpfs is backed by memory and exposes nothing from the host, same as `--tmpfs`, which already passes. Every other mount still goes through the allowlist.
Both forms now work:
```yaml
services:
mysql:
image: mysql:8.4
options: --tmpfs /var/lib/mysql # already worked
```
```yaml
services:
mysql:
image: mysql:8.4
options: --mount type=tmpfs,destination=/var/lib/mysql # was dropped, now works
```
Reviewed-on: https://gitea.com/gitea/runner/pulls/1289
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The Kubernetes backend extracted CopyDir archives at "/" with entries like
"var/run/act/actions/<sha>/...". Ubuntu's patched tar refuses to extract
through an absolute symlink, failing with "Invalid cross-device link" on
images that link /var/run to /run, so actions never reached the job
container. Extract at the destination with relative entries instead, like
Copy already does.
This is a similar issue to #1128, but for k8s instead of Docker.
I built the image and deployed it to my k8s cluster to verify the fix.
---------
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1286
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Drew Inglis <drew@drewinglis.com>
Since https://github.com/go-gitea/gitea/pull/39578 (backported to v28), Gitea answers an unregistered runner with a Connect `Unauthenticated` error instead of a gRPC status wrapped as `Unknown`. `IsRegistrationRejected` missed it, so an ephemeral runner kept its spent registration file and failed on every restart. This also fails the `gitea/gitea:main-nightly` e2e job.
Match on the `unregistered runner` message suffix, which both shapes share. The code can't distinguish Gitea from a proxy, because a proxy's 401 with a JSON body also decodes as a wire `Unauthenticated` error.
Written by Claude Code.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1287
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
- Docker actions evaluate `runs.args` and `runs.env` with their own `inputs`, so a same-named `workflow_dispatch` input no longer replaces the step's `with:` value
- Dockerfile action images are tagged per step and job and removed afterwards, so changed Dockerfiles rebuild and concurrent jobs never share an image
- Workflow and job `env` are evaluated before job and service container credentials and service `env` use them
- Docker actions in host jobs get the host workspace and file-command directory mounted, and no longer inherit the runner's own `PATH`
- Cache server address detection accepts a single interface address
Fixes: https://gitea.com/gitea/runner/issues/390
Fixes: https://gitea.com/gitea/runner/issues/406
Fixes: https://gitea.com/gitea/runner/issues/578
Related: https://gitea.com/gitea/runner/issues/64
Reviewed-on: https://gitea.com/gitea/runner/pulls/1268
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
`gitea-runner register` pinged the instance in an unbounded loop, so a permanent failure like an untrusted certificate or a wrong URL never returned. It now pings once and returns the error, like other one-shot commands.
The image entrypoint keeps retrying registration indefinitely by default, like the daemon, so a container without a restart policy still recovers once Gitea is reachable. `GITEA_MAX_REG_ATTEMPTS` had no effect because its counter was incremented outside the loop. When set, it now caps the attempts, after which the entrypoint exits non-zero.
Fixes: https://gitea.com/gitea/runner/issues/1195
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1197
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Fixes https://gitea.com/gitea/runner/issues/1263
- Wait for dockerd without a check limit. s6 readiness gave up after 7 checks, so the runner never started on slow hosts, also seen in https://gitea.com/gitea/runner/issues/712, https://gitea.com/gitea/runner/issues/652 and https://gitea.com/gitea/runner/issues/758.
- Let the runner run the docker service's readiness check itself instead of reading s6 status, which can be missing at startup or stale after an unclean stop.
- Exit the container with the first failing service's exit code instead of always 0, so `restart: on-failure` works. A requested stop still exits 0.
- Stop the runner before dockerd, so running jobs can finish within `shutdown_timeout`. The stop grace period has to cover that drain.
- Check the supervision exit codes in `make test-dind`.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1265
Reviewed-by: bircni <bircni@icloud.com>
Push the runner's Prometheus metrics to the OTLP endpoint next to the job traces, so both land in the same backend.
The `/metrics` registry is converted as the [Prometheus compatibility spec](https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/) describes and sent through the existing gRPC-free client, so names stay unchanged and no second metrics stack is linked. Metrics are on by default and pushed every interval and once on shutdown, so ephemeral runners report too.
New standard variables, each `OTEL_EXPORTER_OTLP_*` one with `TRACES` and `METRICS` variants:
- `OTEL_METRICS_EXPORTER`, `none` turns metrics off
- `OTEL_METRIC_EXPORT_INTERVAL`, `OTEL_METRIC_EXPORT_TIMEOUT`
- `OTEL_EXPORTER_OTLP_TIMEOUT`, `OTEL_EXPORTER_OTLP_COMPRESSION`
- `OTEL_EXPORTER_OTLP_CERTIFICATE`, `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE`, `OTEL_EXPORTER_OTLP_CLIENT_KEY`
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1248
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: wmTJc9IK0Q <231276+wmtjc9ik0q@noreply.gitea.com>
Ephemeral registrations are single-use, but the daemon kept `.runner` after the task, so a restarted container failed `Declare` with `unregistered runner` forever.
An ephemeral runner now removes its registration file as soon as it fetches a task, so a container killed mid-job registers afresh on restart. It also removes the file when Gitea answers `unregistered runner` at startup or while polling. A proxy's 401 does not count. Persistent runners keep their file, because Gitea sends the same reply for a token mismatch.
Closes https://gitea.com/gitea/runner/issues/818
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1256
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Adds a Kubernetes backend. A `name:kubernetes://image` label runs the job as one pod through the Kubernetes API, with services as containers of the same pod. Step env is exported by a script sent over exec stdin, keeping it out of the pod spec and process arguments, and service env goes into a per-job Secret. Docker actions and container options are rejected. See `docs/kubernetes.md`.
Operators shape job pods with `kubernetes.pod_template`, with per-label `pod_templates` merged in `runs-on` order and a `$services` entry for service containers. The idle cleanup removes pods and Secrets a runner left behind.
The client calls the Kubernetes API directly and runs step commands over `coder/websocket` (same dependency used in gitea) instead of pulling in the heavyweight `kubernetes/client-go`. Includes an e2e test against k3s.
Replaces: https://gitea.com/gitea/runner/pulls/1000
Fixes: https://gitea.com/gitea/runner/issues/31
Assisted-by: Claude Code:claude-opus-5-5
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1260
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
The Docker entrypoint always checked `.runner` in `/data` and ignored `runner.file` from `CONFIG_FILE`. When `runner.file` pointed elsewhere, it re-registered on every start, or skipped registration because of a stale `.runner`. It now reads the path from a new `config runner-file` subcommand, which resolves `runner.file` the same way as `register` and `daemon`.
Closes https://gitea.com/gitea/runner/issues/585
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1259
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
1. Host mode is selected only through `runs-on` again, as in upstream act.
1. A workflow volume that `valid_volumes` drops no longer displaces the runner's own socket, workspace, tool cache or `/var/run/act` mount, so `/var/run/docker.sock:/var/run/docker.sock` works on hosts whose daemon socket lives elsewhere.
1. `docker://` is stripped from job and service images, as on GitHub.
1. Each created service's log is printed at job end, and service log dumps no longer run workflow commands, as on GitHub.
1. A `--privileged` dropped because privileged mode is off now logs a warning.
1. README: the proxy variables change per step through `env:` or for the rest of a job through `$GITHUB_ENV`, `container.env` does not override them.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1253
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Fixes https://github.com/go-gitea/gitea/issues/38705
Gitea side: https://github.com/go-gitea/gitea/pull/39452
Jobs of a called workflow currently see `gitea.event_name == "workflow_call"`, and their `gitea.event.inputs` is replaced by the caller's `with:`. On GitHub they keep the caller's event, and their `inputs` are the run's `workflow_dispatch` inputs overlaid with the caller's `with:`.
For compatibility with older runners, Gitea keeps sending that form and adds a `gitea_workflow_call` key with the original trigger event name and inputs of the run, and the `inputs` Gitea resolved for the job. When the key is present, the runner restores `gitea.event_name` and `gitea.event.inputs` from it, and uses the resolved `inputs` for the job's expressions and the setup log.
Without that key (older Gitea, or jobs outside called workflows), nothing changes.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1250
Reviewed-by: bircni <bircni@icloud.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Store the cache used by `actions/cache` in an S3-compatible bucket such as AWS S3 or RustFS, with no `cache-server`. Runners with the same settings use the same cache.
A cache saved on one runner can be restored on the others within 30 seconds. The runner talks to the bucket over the S3 REST API with SigV4, without an SDK. Lifecycle rules on the bucket expire old caches.
*Description rewritten by Claude.*
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1244
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Make cache requests go through the runner and remove outbound traffic from the cache server. The runner forwards them to `cache.external_server` when one is set, and attaches itself to each job network so jobs can reach it.
Benefits:
1. Jobs need no http proxy rules for the cache server, which `@actions/http-client` mishandles for CIDR ranges in `no_proxy` (https://github.com/actions/toolkit/pull/2495).
2. Only runners reach a shared cache server, which needs no route to Gitea.
3. A runner container sharing the host Docker socket now caches, where jobs previously could not reach it.
Breaks:
1. Jobs that reach `external_server` but not their runner, such as a remote `DOCKER_HOST`. The built-in cache has always needed that reachability, so both modes now agree. Set `cache.host` and `cache.port` to an address those jobs reach.
2. Jobs on `container.network: bridge`, where Docker rejects the alias, so the runner cannot attach and their cache fails as it does today. Leave `container.network` empty, or name a user-defined network.
*Written by Claude.*
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1229
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
`valid_volumes` patterns now match with doublestar instead of gobwas/glob, which was compiled without separators so `*` crossed slashes. doublestar is already in the module graph, keeps `*` within one path segment and matches host paths with the OS separator, which fixes Windows patterns where a backslash was read as an escape.
Breaking: a host pattern that relied on `*` matching nested paths needs `**`, e.g. `/data/**` instead of `/data/*`. Named volumes and the bare `**` are unaffected.
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1214
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Adds built-in actions, native Go actions shipped with the runner and selected with `uses: builtin:<name>`. Jobs need no action download and no Node in their image.
The first one is `builtin:checkout`, a subset of `actions/checkout` with `repository`, `ref`, `token`, `path` and `fetch-depth`. Other inputs fail the step. The runner clones shallowly on its host and copies the tree into the job, while `gitea-runner exec` copies the local working directory for the workflow's own ref.
`actions/checkout` and other remote actions are unchanged. A clone ref of `HEAD` now shallow-clones the remote's default branch, which checkout uses for other repositories.
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/871
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Updates actionslib to https://gitea.com/gitea/actionslib/releases/tag/v1.2.1.
1. A job whose `if:` fails to evaluate, like `fromJSON` of an invalid job output, is skipped with the error in its log instead of failing, like on GitHub Actions.
1. Workflows the runner reads itself, like with `exec`, are rejected when an expression does not parse, like an unclosed `${{` or an unknown context, instead of failing later.
1. Task payloads from Gitea are read without that check, because they hold evaluated text that may contain `${{`.
1. `if: False`, `if: ~` and other boolean, null or number conditions work.
Gitea PR: https://github.com/go-gitea/gitea/pull/39358
Reviewed-on: https://gitea.com/gitea/runner/pulls/1247
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The dind images run `s6-svscan` as PID 1, so the `docker-init` that the upstream entrypoint puts in front of `dockerd` cannot reap and warns on every start. `TINI_SUBREAPER` makes it a subreaper for the daemon's subtree, which is what tini recommends when it is not PID 1.
The warning was cosmetic. Orphaned `containerd-shim` processes re-parented to `s6-svscan`, which reaps them just as well, so no zombies accumulated. With this change they re-parent to `docker-init` instead, and the warning is gone.
Seen in https://gitea.com/gitea/runner/issues/1240
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1241
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A composite action's child run context starts with empty step results, so a nested action's `post-if: success()` saw the composite's own result and still ran after the job had failed, which let `actions/cache` save caches from failed jobs.
The `job` context now always comes from the job's run context. Only `success()` and `failure()` in a composite's own main steps read the composite's result, matching GitHub Actions.
Assisted-by: Claude Code:claude-fable-5-1
Reviewed-on: https://gitea.com/gitea/runner/pulls/1242
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Re-sync `act/container/docker_cli.go` and `act/container/docker_cli_test.go` to docker/cli v29.8.1, the version go.mod pins. The copies lacked the `--umask` flag added in v29.8.0, so `container.options` rejected it as unknown. Daemons below API 1.56 ignore the setting.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1238
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Podman creates a missing bind source instead of rejecting it, so the Docker proxy probe passed even when the daemon could not see the runner's files. Jobs of Podman runners in a container got an empty directory at `/var/run/docker.sock` (https://gitea.com/gitea/runner/issues/1193#issuecomment-1700501).
The probe now binds the directory and stats its marker through the created container. A runner in a container stats the marker through its own container, so it no longer creates probe containers, which cost up to 2s per job. `make test-dind TARGET=podman` runs the probe against Podman in CI.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1231
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
`waitForRunningJobLog` gave one minute to run creation, scheduling, the container start and the first log flush, then blamed the missing log line, which is how https://gitea.com/gitea/runner/actions/runs/927661 failed. It now gets the suite's `runTimeout` and reports the job's status with the run logs on timeout, and the job holds ten seconds instead of two, so neither the marker nor the cancellation arrives after it has exited.
Against gitea 1.28+ this costs nothing, since the cancel ends the job as soon as the marker appears. Against 1.27, which has no cancel route and waits the job out, the suite goes from about 20s to about 23s, as the tests run in parallel.
*Written by Claude.*
Reviewed-on: https://gitea.com/gitea/runner/pulls/1230
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Containers a job starts through its Docker socket, for example `docker run -v "$PWD:/src"`, `./data:/data` in docker compose, or actions like dockerfile-roast, can now bind-mount the workspace and other paths the job sees, as on a host, without `bind_workdir`.
The per-job Docker proxy rewrites container and volume create requests. A bind source, or the device of a `local` volume with `o: bind`, that lies under one of the job container's mounts is pointed at that mount's path on the daemon, read from inspecting the job container. Paths that already name a daemon path, like `GITEA_DOCKER_WORKSPACE`, and paths outside the job's mounts pass through unchanged.
The proxy now also starts when the runner runs in a container given the host's Docker socket, by placing its socket in the runner's working directory, and in rootless dind, by granting the daemon socket's group through an ACL.
Fixes https://gitea.com/gitea/runner/issues/1219
Fixes https://gitea.com/gitea/runner/issues/1193
Reviewed-on: https://gitea.com/gitea/runner/pulls/1226
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Mount the job workspace volume, or the `bind_workdir` directory, above `<owner>/<repo>` instead of at the repository, like GitHub's work directory.
With the repository as a mount point, pnpm 12 puts its store in `node_modules/.pnpm-store` (https://github.com/pnpm/pnpm/pull/13536), so a restored `setup-node` pnpm cache creates `node_modules` before install. Renames into `$RUNNER_WORKSPACE` fail with `EXDEV` and removing the workspace fails with `EBUSY`.
Verified against GitHub hosted and container jobs with cold and warm caches, on volume and `bind_workdir` runners, covering checkout, cache, artifacts, github-script, the node, pnpm, bun, python, uv, go, rust, java, gradle, dotnet, ruby and terraform setup actions, docker and Dockerfile actions, buildx, compose via `GITEA_DOCKER_WORKSPACE`, services, host mode, `container.volumes` on the workspace and `exec`.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1224
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Every cache hit rewrote the entry's access time, costing a bolt write and fsync per restore. Completed entries accessed within the last minute are no longer rewritten, and the in-use and retention windows grow by that minute so nothing is evicted earlier. Same-second rewrites are skipped, reservations are inserted in one transaction, and prefix lookups use the repository index instead of scanning every entry.
The store is still opened per request. Keeping it open, as this commit first did, is faster under concurrency but stops a second process sharing the cache directory from starting its cache server.
The README caching section is condensed and corrected.
| Benchmark, v2 | main | store kept open | this PR |
| --- | ---: | ---: | ---: |
| Exact hit, 1 client | 16.42 | 0.15 | 0.24 |
| Exact hit, 8 clients | 13.70 | 0.08 | 1.28 |
| Exact hit, 32 clients | 16.01 | 0.07 | 4.26 |
| Prefix hit, 8 clients | 18.34 | 0.12 | 1.28 |
| Miss, 8 clients | 3.21 | 0.11 | 1.28 |
| Save and restore 1 MiB, 8 clients | 74.4 | 22.9 | 34.9 |
| Geomean, all 22 benchmarks | 14.03 | 0.31 | 2.11 |
Mean ms per request, medians of 4 rounds of 300 requests over 1,000 entries on an Apple M3 Max.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1222
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Implement speedups to job start and shutdown.
- Create the job container while services are still becoming healthy, and poll their health at a flat one second instead of a 2s to 32s doubling backoff
- Pull each service image once instead of twice, and fetch a warm action cache once instead of twice
- Report the job result before reclaiming its volumes, and reap volumes stranded by a runner that died mid-job
| Step | Scenario | Before | After |
| --- | --- | --- | --- |
| Complete job | Large workspace volume | 4.2s | 0.4s |
| Set up job | One service, 2s health interval | 7.08s | 3.26s |
| Set up job | Two cached actions from github.com | 1.81s | 1.34s |
| Set up job | Two cached actions from gitea.com | 2.42s | 1.95s |
| Set up job | Two actions, cold action cache | 6.62s | unchanged |
| Set up job | Minimal job, no services or actions | 0.62s | unchanged |
Assisted-by: Claude Code:Opus 5
Reviewed-on: https://gitea.com/gitea/runner/pulls/1218
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Three independent fixes, one commit each.
1. Hosts and ports were interpolated directly when building URLs, so a literal IPv6 address produced an unbracketed authority, making `ACTIONS_CACHE_URL`, `ACTIONS_RUNTIME_URL` and the artifact server listener unusable. Hosts are expected bare, as documented for `cache.host`, so an address that already carries brackets is no longer accepted.
2. `cache-server` waited on its own `os.Interrupt` channel and ignored SIGTERM, so service managers and container runtimes had to kill it.
3. Task admission used a separate `Load` and `Store`, so two concurrent dispatches of the same task id could both be admitted.
Assisted-by: Claude Code:Opus 5
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1217
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Cache v2 makes the cache server the `ACTIONS_RESULTS_URL` origin, so artifact calls arrived there and were proxied on to Gitea, failing whenever it could not reach the instance.
- Artifact calls are answered with a redirect, so the cache server opens no connection to Gitea. A scheme change or an untrusted instance is still proxied, but there the cache server is the runner itself, which already reaches Gitea.
- Failures answer in twirp, not an empty `502` that clients report as `Unexpected end of JSON input`.
- `cache.v2: false` really points artifacts at Gitea now.
- Cache reservations are bound to the job that made them, so two jobs saving one key cannot commit against each other's upload, and a retry after a lost answer no longer fails a saved entry.
- The toolkit patch, which edits the GitHub-host check out of an action's bundle, was left in the shared checkout where a job running with `runner.patch_actions: false` could inherit it. It is put back after the job's copy.
- `exec` names an origin for the cache v2 it advertises, and masks its runtime token.
Behaviour changes: `no_proxy` no longer exempts `cache.external_server`, and `cache.enabled: false` also stops external registration.
Fixes https://gitea.com/gitea/runner/issues/1208
Fixes https://gitea.com/gitea/runner/issues/1211
Assisted by Claude (Opus 5).
Reviewed-on: https://gitea.com/gitea/runner/pulls/1216
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Fixes https://gitea.com/gitea/runner/issues/1213
Fix the DooD regression that mounts `/var/run/docker.sock` as a directory. Keep the Docker proxy available through job and post steps. Clean stale resources before opening it, then remove containers before their networks and volumes during teardown.
Use a unique filesystem probe and preserve socket ownership. Fall back to direct access when proxying is unsupported. Preserve exec output and clean up active streams and failed starts.
Add a real Docker job test for mounted socket access, post steps and resource cleanup.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1215
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
1. Dependency updates: gobwas/glob v1.0.0, which replaced its `Glob` interface with `*Pattern`, logrus v1.10.2, prometheus/client_model v0.6.3, golangci-lint v2.13.2, Go 1.27.1, docker/setup-qemu-action and nginx digests.
2. `make fmt-check` and `make tidy-check` report only what the formatter or `go mod tidy` would change, so unrelated uncommitted work no longer fails `make checks`.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1212
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
1. Add `GITEA_DOCKER_WORKSPACE` which holds the workspace path as the daemon sees it, enabling `${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data` in a compose file without having to resort to `bind_workdir` (which causes much more problems like breaking `actions/cache` because of unstable workspace paths).
2. Add container/network/volume cleanup for containers started within jobs, for example via `docker compose` inside a job. It works by running a lightweight docker socket proxy and injecting a `com.gitea.runner.job` label into every container creation and that label is used to remove containers started by that job at the end. Perf impact of this is near-zero.
Docs: https://gitea.com/gitea/docs/pulls/535
Assisted by Claude (Fable 5.1).
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1204
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
An action pinned to a full commit hash cloned the whole repository, and a cached one hit the network on every run. Now only the pinned commit is fetched at depth 1, a cached commit is reused with no network at all, and the action's `.git` directory no longer ships into the job container, matching GitHub.
```
repo cold ms warm ms cache KiB
actions/checkout 5807 → 787 1064 → 12 11492 → 2349
actions/setup-node 15257 → 1013 1365 → 21 64303 → 9924
actions/cache 18782 → 760 1589 → 16 60890 → 12507
actions/setup-go 5513 → 764 589 → 20 16600 → 9104
docker/login-action 25809 → 1225 1288 → 10 81709 → 12503
```
Reviewed-on: https://gitea.com/gitea/runner/pulls/1209
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
An expression that failed to evaluate was logged and replaced by an empty string, so a `run:` step executed an empty script and reported success. The same swallow covered `shell:`, `working-directory:`, step `env:`, `with:`, `uses:`, the job's `env:`, `container:`, `services:`, `runs-on:` and outputs, and a called workflow's `with:` and `secrets:`. Every interpolation now propagates its error as actions/runner does: step-level values fail the step, job-level values fail the job at setup, `timeout-minutes` logs the error and runs unbounded, and a job or step name keeps its source text.
`defaults.run` and a called workflow's inputs and secrets are resolved once at job setup with the job context rather than per step, and the job's image is resolved once, so host mode and `ImageOS` derive from the image the job started with.
Closes https://gitea.com/gitea/runner/issues/392
Closes https://gitea.com/gitea/runner/issues/555
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1199
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Automatically allow workspace mounts in `valid_volumes` for the mounts done via `bind_workdir`, this obsoletes the need for `/workspace/**` or other insecure configurations which would expose workspaces between tasks.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1203
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Node resolves an ESM main to its realpath but leaves `process.argv[1]` as passed. `/var/run/act` reaches the action through the `/var/run` -> `/run` symlink most images ship, so the two disagree and actions comparing them skip their own `run()`. Passing `--preserve-symlinks-main` makes them match.
Trade-off: an action whose `runs.main` is a symlink now resolves dependencies from the link's directory rather than the target's.
Fixes https://gitea.com/gitea/runner/issues/1201
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1202
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@noreply.gitea.com>
This PR contains the following updates:
| Package | Update | Change |
|---|---|---|
| [node](https://github.com/nodejs/node) | digest | `934240a` → `be23f54` |
| [node](https://github.com/nodejs/node) | digest | `3638d9a` → `ba849c6` |
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Reviewed-on: https://gitea.com/gitea/runner/pulls/1198
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Fixes 51 bugs discovered via comparison with `actions/runner`. Every fix has test coverage.
### Secrets
- A short secret registered no shifted-base64 form, so `base64("user:$TOKEN")` printed in the clear
- Encoded forms came only from the whole trimmed value, missing padded and per-line spellings
- Masks split only on `\n`, so `::add-mask::a%0Db` registered neither half
- Adds XML, expression-string and quote-trimming encoders
### Workflow commands
- Split at the last `::` or `]` rather than the first, so `::add-mask::a::b` registered no mask
- A command on the last line without a newline was ignored, and `::ADD-MASK::` did nothing
- `##[...]` did not decode `%3B`/`%5D`, properties lost anything after a second `=`
- `$GITHUB_ENV` and `::set-env::` now refuse `NODE_OPTIONS`
### Status
- `continue-on-error` reported failed, a cancelled job reported success, an `if:` error reported cancelled
- File commands ran after `continue-on-error`, failing the job while the step stayed green
- A bad job output aborted the whole run instead of that job
### Steps and actions
- `${{ matrix.* }}` and `${{ strategy.* }}` were empty inside composite actions
- Composite inputs leaked into nested actions as `INPUT_*`, `with:` matched case-sensitively, `pre` failures were dropped
- Docker actions dropped `runs.env` when the caller passed `with: args:`, and caller `args`/`entrypoint` beat the manifest
- An implicit shell ran with `pipefail`, a `shell:` without `{0}` passed without running
- `container.env` overrode job env and every `$GITHUB_ENV` write, heredocs lost leading blank lines, `$GITHUB_PATH` was not BOM-decoded
Written by Claude Opus 5.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1194
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Service containers were built without a volume policy, so every bind and mount they declared was dropped, whatever `valid_volumes` allowed. GitHub passes a service's declared volumes straight to `docker create`, so a workflow that mounts into a service silently did nothing here. Services now get the configured policy, but not `validVolumes()`, which would also hand them the docker daemon socket that GitHub mounts only into the job container.
### What changes for users
On the default `valid_volumes: []` nothing changes: a service's volumes are still dropped, now with a warning rather than in silence. Once `valid_volumes` is configured, a service's declared volumes are honored under it instead of discarded, which is what that setting already documents. No workflow that worked before stops working, and a service can reach no volume the policy does not already allow the job container, so this is not a breaking change.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1186
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A step's `if:`, its `continue-on-error:` and its `run:` script resolved `inputs.*` from the step's own `INPUT_*` env. A `with:` key colliding with a workflow input flipped conditions, and any `INPUT_`-shaped variable from `env:` or a `GITHUB_ENV` write forged an input that never existed.
GitHub evaluates all three in the enclosing scope: the workflow inputs, or for a composite action's steps that action's inputs. Action-input interpolation is the one place that legitimately sees a step's own `with:`, so it keeps its own evaluator.
Fixes https://gitea.com/gitea/runner/issues/1191, ports https://github.com/nektos/act/pull/2473 and extends it to the pre and post stages.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1192
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: ABiscuitttt <773542570@qq.com>
A secret in a matrix value reached the log in the clear:
```yaml
strategy:
matrix:
include: "${{ github.token }}"
```
Chasing that one route is pointless, so this masks every sink a secret leaves a job by: the uploaded log rows and the on-disk `job.log`, both through one choke point in `appendLogRow`; the runner's own log, which is where planning errors like that one land with no job logger in reach; the job logger's stdout under debug logging; job summaries; job outputs; and the job name that becomes a container name.
Values the runner knows but the job never declared, the proxy password and the task token, are hidden the same way. Masks apply longest first, since `strings.Replacer` matches in argument order and one secret prefixing another would otherwise mask the prefix and print the rest.
### What changes for users
An output whose value carries a secret is skipped with a warning instead of sent, matching GitHub. Output that showed a secret now shows `***`. `ACTIONS_STEP_DEBUG` and `ACTIONS_RUNNER_DEBUG` are never masked, also matching GitHub, so an output of `true` still reaches the jobs that need it.
Each fix has a test that fails without it.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1188
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Gitea slices a task's single log stream into steps by the LogIndex/LogLength that UpdateTask carries. The reporter's daemon flushed log rows without those counters, which only left on the separate state ticker, so rows the server acked between two state reports belonged to no step. The web UI attributes them to no step while the job runs, and a runner that stops reporting in that window leaves them under "Complete job" for good once Gitea finalizes the task.
The three log-flush paths now report the state describing the rows the server just took, so the gap is at most one RPC. Live output also stops waiting up to `state_report_interval` to become visible. An idle job adds no requests.
This does not explain why the runner in that job stopped reporting for 13m40s until Gitea reaped the task as a zombie, that needs the runner host's own log.
Fixes https://gitea.com/gitea/runner/issues/1184
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1189
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
The runner's own `container.options` and the workflow's were joined into one string before parsing, so the host-escape filter added in https://gitea.com/gitea/runner/pulls/1058 dropped the administrator's options along with the workflow's. Setups that need `--device` or `--security-opt` from the config file had no way left to get them short of enabling privileged mode.
`NewContainerInput` now carries the two sources apart, as `RunnerOptions` and `WorkflowOptions`, down to the point where the filter runs. With privileged mode off, the host-escape fields are reset to what the runner's own options parse to on their own, so only the workflow's contribution is dropped.
Three further ways a workflow's options reached past its container, all resolved on the runner before anything reaches the daemon:
1. `--env-file` and `--label-file` name files that are read on the runner, so any file it could read became container environment or labels. Both are refused from a workflow now, and still serve the runner's own options.
2. A bare `--env NAME` was resolved from the runner's own environment by docker's validator. That lookup is gone, for every source. Use `runner.envs` or `runner.env_file` to pass a variable on.
3. A volume driver decides for itself what it mounts, and the local driver's `device=` option turns a name `valid_volumes` allows into a bind of any host path. A workflow's mounts may no longer carry one.
`--isolation`, `--volume-driver` and the two paths `--security-opt systempaths=unconfined` lands in were also missing from the fields a workflow may not set.
Last, the `--network and --net in the options will be ignored.` warning fired for every container, because the runner's own network mode is fed into the parsed options before the check runs.
Fixes https://gitea.com/gitea/runner/issues/1142
Reviewed-on: https://gitea.com/gitea/runner/pulls/1151
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A `GetMatrixes` error was logged and discarded, leaving a nil matrix list. That collapsed `maxParallel` to zero, so no executor was built and the parallel executor returned nil for an empty list: the job reported success without running anything.
It now fails the run. Every error it returns is a workflow validation failure that GitHub rejects too, so nothing that runs there starts failing here.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1187
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Adds a real-Gitea compatibility suite against stable and nightly to catch runner and API drift before release.
The suite shares one Gitea and regular runner, using repository runners only for cache v1/v2 and ephemeral behavior. It covers registration, payload and log encoding, secrets, variables, services, artifacts, outputs, matrices, cache, dispatch, live logs, cancellation, and ephemeral teardown.
CI runs both images in parallel. Warm local timings:
| Image | Before | After |
| --- | ---: | ---: |
| `gitea/gitea:latest` | ~70s | 26.54s |
| `gitea/gitea:main-nightly` | ~54s | 22.53s |
The former serial matrix took about 2 minutes. Parallel suite execution is now bounded by the slower ~26.5-second variant.
Shared Renovate matcher: https://gitea.com/gitea/renovate-config/pulls/552
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1180
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Parallelize isolated workflow tests, consolidate redundant fixtures, and replace fixed waits with deterministic synchronization. Keep the readable curl service probe and use `getent` for hostname resolution.
Measured on the same machine with `make test`:
1. Wall time: 170.50s to 136.12s, down 34.38s or 20.2%.
1. `act/runner`: 138.417s to 124.291s, down 14.126s or 10.2%.
1. `act/runner` coverage: unchanged at 85.2%.
1. `TestDockerExecAbort`: 2.514s to 0.012s package time.
Stability checks:
1. Cancellation and deadline tests: 100 race-enabled repetitions.
1. Host runner suite: 10 race-enabled repetitions.
1. Changed Docker fixtures: 3 consecutive repetitions.
Full race suite, Go and Windows lint, source checks, and security scan pass.
Assisted-by: Codex:GPT-5
Reviewed-on: https://gitea.com/gitea/runner/pulls/1181
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
`log.job.dir` makes the runner write a copy of every task's log to that directory, as `<start time>-task-<id>.log`: the rows exactly as Gitea received them, with the same masking and the job's result on the last line. Off by default, and what Gitea shows does not change.
`log.job.retention` (default `168h`) and `log.job.max_size` (default `1GB`) bound the directory. Documented in the README.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1165
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
A step that failed, for any reason, made the runner restore the action's stock bundle and mark it never to be patched again. Every later `actions/upload-artifact` run then failed with `GHESNotSupportedError`, and nothing in the log said why.
The edit is now made as the action is copied into the job container, under the lock that guards the copy, and nothing reverts it. That also closes the race where another job's checkout reset the bundle mid-job.
`cache.v2` no longer decides whether the edit is made, it only withdraws the v2 advertisement, so artifacts work whatever the cache is set to.
Also added a new `runner.patch_actions` option to turn the edit off if it ever breaks an action.
Fixes https://gitea.com/gitea/runner/issues/1176
Reviewed-on: https://gitea.com/gitea/runner/pulls/1177
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A job whose `runs-on` matches none of the runner's labels, which includes every job that sets no `runs-on` at all, runs in `runner.default_image`. It defaults to `docker.gitea.com/runner-images:ubuntu-latest` as before, so a mirror can be pointed at instead.
A runner with no reachable docker daemon now runs such a job on the host, rather than failing on an image it cannot pull. Runners that use docker are unaffected and never probe for one.
This matters most to a host-mode runner, one whose labels are all `host`. Such a runner has no daemon to pull an image with, so a job matching none of its labels used to fail at container start. It now runs on the host, where that runner runs everything else anyway, and it takes no configuration to get there. A host-mode runner that does have a daemon within reach keeps using the image, unchanged.
Supersedes https://gitea.com/gitea/runner/pulls/642
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1164
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Jobs occasionally go silent ([example](https://gitea.com/gitea/runner/actions/runs/805045/jobs/1055123)) mid-run and Gitea reaped them after `ZOMBIE_TASK_TIMEOUT`, with no error in the log. This contains a number of related fixes, all with full test coverage:
1. Bound every RPC to Gitea with a timeout, a stalled report otherwise parked logs and heartbeats for the whole job.
2. Cap `runner.fetch_timeout` at that ceiling.
3. Let only the daemon loop close its own channel, the race panicked the process.
4. Stop the job on any terminal server result, not just `RESULT_CANCELLED`.
5. Report that result instead of relabelling it as cancelled.
6. Log reporting failures once at each end of an outage instead of discarding them.
7. Clamp the acknowledged log index, a too-large ack panicked on a slice bound.
8. Stop reading server health from a `FetchTask` deadline, it marked the runner healthy and reset the error backoff on a timeout.
9. Return an error from the Docker version probe instead of a `logrus` panic.
10. Pass the context to go-git's fetch and pull.
11. Fail the clone when a refresh dies on a cancelled context.
12. Set `terminationGracePeriodSeconds` in the Kubernetes examples.
Also contains a deprecation fix for goreleaser.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1174
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The current shared tools cache is not concurrency-safe, e.g. multiple jobs can write and corrupt it, for example `setup-go` with explicit go version under concurrency reliably corrupts the tool cache and fails all jobs.
This adds a new `runner.tool_cache_mode` (and `--tool-cache-mode` exec option) option which defaults to unshared tools cache:
- `none` mounts nothing, so a job uses what its image ships there and discards what it installs
- `shared` keeps the single volume every job reuses, and warns when `runner.capacity` is above 1
Under `none` effective tool cache can only come from the image or host, which is the same as it is on GitHub Actions which ships many preinstalled tools in its fat VM images.
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1171
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The cache server retired entries 30 days after creation regardless of use, so a job that ran often enough to keep its cache warm still lost it on a fixed schedule. Nothing bounded the disk either.
Retention now counts from last access alone, and a repository over its limit sheds least recently accessed entries until it fits, enforced on commit as well as on the periodic sweep.
```yaml
cache:
retention: 168h # remove entries not accessed for seven days
repo_size_limit: 10GB # cap each repository
size_limit: 0 # cap the whole cache, off by default
sweep_interval: 1h # minimum time between sweeps
```
Sizes accept `10GB`, `512mb`, `1TiB` or a plain byte count, binary either way. Leave a key out for its default; `0` turns a limit off, and `0s` does the same for `retention`. Whatever these allow, the cache also sheds entries to keep free space above `health_check.min_free_disk_space_mb` when health checks are enabled, so it cannot grow past the point where the runner stops accepting work.
Supporting fixes: serving an entry stamps its access time, so a find cannot hand a job a download URL for an entry the next eviction is about to remove; an entry larger than the limit is dropped on its own account rather than emptying its repository to make room; and a blob that cannot be unlinked keeps its row, so the next sweep retries instead of orphaning bytes no limit can account for.
Closes https://gitea.com/gitea/runner/issues/1168
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1170
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Trigger-time inputs used to be visible in the job log but are no longer shown after the "Set up job" section was reshaped. This restores them as an "Inputs" group listing each provided input and its value, rendered only when the event payload carries inputs.
For a run dispatched with `required=required input`, `with_default=default` and `boolean=true`, the "Set up job" log now shows:
```
gitea-com-gitea-0003(version:v3.0.2)
▸ Runner Information
Task: 268506
Job: test
Repository: gitea/runner
Triggered by event: workflow_dispatch
▸ Inputs
boolean: true
required: required input
with_default: default
▸ Operating System
Ubuntu 24.04.4 LTS
linux/amd64
```
Closes https://gitea.com/gitea/runner/issues/1166
Reviewed-on: https://gitea.com/gitea/runner/pulls/1167
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Release artifacts were uploaded to both AWS S3 and Cloudflare R2 during the migration period. Drop the goreleaser `blobs:` S3 pipe and the AWS_*/S3_* secrets from the release workflows, so artifacts are published to Cloudflare R2 only.
Assisted-by: Codet:GPT-5.1-Codex
Reviewed-on: https://gitea.com/gitea/runner/pulls/1169
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
The cache server built every URL it hands a job from its own listen address, so jobs whose runner reaches it through a reverse proxy were sent to the internal one. This covered the v1 `archiveLocation`, the v2 signed cache URLs and `ACTIONS_RESULTS_URL`.
Runners now register the address their jobs reach the server at, next to the instance URL they already send. The cache-server needs no configuration of its own, and runners that reach it differently each get their own correct address.
Closes https://gitea.com/gitea/runner/issues/1152
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1153
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: Max P. <mail@0xMax42.io>
Gitea needs the workflow model and the expression evaluator to parse workflows and to build the task payload this runner consumes, so today it depends on `gitea.com/gitea/runner` just for `act/model` and `act/exprparser`. Both packages now live in `gitea.dev/actionslib` (`pkg/model`, `pkg/exprparser`), the module both sides already share, and this repository consumes them from there.
### Changes
- `act/model` and `act/exprparser` are deleted, all imports point at `gitea.dev/actionslib/pkg/...`.
- New `act/ghcontext` package: the `GithubContext` helpers that need a git checkout on disk (`SetRef`, `SetSha`, `SetRepositoryAndOwner`) are runner only and would drag a git client plus the act context logger into the shared module, so they stay here as functions, with their tests. Only caller is `RunContext.getGithubContext`.
- `act/common.CartesianProduct` moved to the shared model package, `act/model` was its only user.
- `act/model/testdata/container-volumes` moved to `act/runner/testdata/container-volumes`, its only user is `runner_test.go`.
- `internal/pkg/client.UUIDHeader` / `TokenHeader` now alias `pkg/protocol`, so the header names cannot drift apart from Gitea.
### Notes
- No behaviour change intended: the moved files are unchanged apart from the import paths and the split described above.
- `go.mod` depends on the released `gitea.dev/actionslib v0.7.0`, which carries both https://gitea.com/gitea/actionslib/pulls/11 and the `model.UsesHash` port in https://gitea.com/gitea/actionslib/pulls/14 that `main` needs after https://gitea.com/gitea/runner/pulls/1150.
- Verified with `go build ./...`, `go vet ./...` and `go test ./act/... ./internal/...`; the docker based `act/runner` integration tests (`TestRunEvent`, `TestRunMatrixWithUserDefinedInclusions`) fail identically with and without this change in my environment.
Assisted-by: Codet:GPT-5.1-Codex
Reviewed-on: https://gitea.com/gitea/runner/pulls/1143
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Accepts GitHub's `$/` self-repository prefix in a step `uses:`. It resolves to the repository holding the file that wrote the `uses:`, at the ref being run, with no checkout. Inside a composite action that is the enclosing action, otherwise the workflow's own repo and commit.
The action cache is keyed on the resolved reference for these, because the same `$/x` names a different action per enclosing repository.
Related PR for job-level support: https://github.com/go-gitea/gitea/pull/38822
Reviewed-on: https://gitea.com/gitea/runner/pulls/1150
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Revert #1136 and use actionslib instead.
revert chore: bump the module path to `/v3`, take the version from the VCS stamp (#1136)
gitea can not consume the runner's api by version while it's version mismatches the module version:
```
go: gitea.com/gitea/runner@v3.0.1: invalid version: module contains a go.mod file,
so module path must match major version ("gitea.com/gitea/runner/v3")
```
Fix that by bumping the module version now. The existing `v3.0.0` and `v3.0.1` tags stay unusable, so a new tag is needed after this lands.
Also drop the version `-X` linker flags, which would otherwise have to repeat the new path in both `Makefile` and `.goreleaser.yaml`, where a stale path makes injection silently no-op. Go has recorded the module version in the build info since 1.24, so `Version()` reads it from there, keeping the variable as an override for builds without a VCS stamp.
That part started as https://gitea.com/gitea/runner/pulls/1137 but belongs here: the stamp resolves against the tags that are legal for the module path, so without the `/v3` bump it would report `v1.0.9-0.<ts>-<sha>`. Since `release-nightly.yml` triggers on every push to `main`, splitting them would publish a nightly with a `v1` version.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1136
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1148
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
gitea can not consume the runner's api by version while it's version mismatches the module version:
```
go: gitea.com/gitea/runner@v3.0.1: invalid version: module contains a go.mod file,
so module path must match major version ("gitea.com/gitea/runner/v3")
```
Fix that by bumping the module version now. The existing `v3.0.0` and `v3.0.1` tags stay unusable, so a new tag is needed after this lands.
Also drop the version `-X` linker flags, which would otherwise have to repeat the new path in both `Makefile` and `.goreleaser.yaml`, where a stale path makes injection silently no-op. Go has recorded the module version in the build info since 1.24, so `Version()` reads it from there, keeping the variable as an override for builds without a VCS stamp.
That part started as https://gitea.com/gitea/runner/pulls/1137 but belongs here: the stamp resolves against the tags that are legal for the module path, so without the `/v3` bump it would report `v1.0.9-0.<ts>-<sha>`. Since `release-nightly.yml` triggers on every push to `main`, splitting them would publish a nightly with a `v1` version.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1136
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Every `${{ }}` part was spliced as raw text into a synthesized `format('...', <raw>)` call and re-parsed, so unbalanced parentheses restructured the whole expression:
```yaml
run: echo ${{ 1) && (2 }} # panicked with "did not evaluate to a string"
if: ${{ 1 }} ${{ 0) && (0 }} # silently skipped the step
```
One scanner shaped like GitHub's template reader now splits every value, and each part is evaluated on its own, so nothing builds an expression out of text.
An empty input to `Evaluate` asked `success()` whatever the caller requested, so a post step running under `always()` asked the wrong question.
Same fix as https://github.com/go-gitea/gitea/pull/38754 on the Gitea side.
Written by Claude Opus 5.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1146
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
`config.example.yaml` now has every value commented out, as gitea's `app.example.ini` does, so it documents each option with its default instead of imposing it. Copying it no longer pins values the runner would otherwise pick, and a changed default reaches configs that never named the option.
`config init` writes the file to configure: a header comment and no option, so every option keeps its default. It refuses to overwrite an existing config without `--force`, and writes `config.yaml` in the working directory when `-c` is absent.
`config set`, `add` and `remove` keep such a file readable. Comments go back to the indentation they were written at, which the encoder drops for a comment block that has no key below it, and a file of only comments keeps its text instead of being emptied by the first edit.
Also rewrote the config docs sections for clarity.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1145
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Service containers were started and then left alone, so a job's first step could run while a database was still starting up. The runner now waits for every service whose image or `options` declare a healthcheck, as GitHub does. An unhealthy service fails the job with its container log, one that never becomes healthy fails it after `container.service_ready_timeout` (default `5m`, negative disables the wait), and one that exits without a healthcheck only gets its log and a warning.
The started containers also fill the `job` context, whose fields existed but were never populated: `job.container.{id,network}` and `job.services.<id>.{id,network,ports}`. `ports` is keyed by the plain container port, so `job.services.postgres.ports['5432']` resolves to the host port Docker picked.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1107
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Changing a setting after `generate-config` meant hand-editing YAML, which is awkward in provisioning scripts. `config` now edits an existing file in place:
```bash
./gitea-runner -c config.yaml config set runner.capacity 4
./gitea-runner -c config.yaml config set runner.envs.MY_VAR value
./gitea-runner -c config.yaml config add runner.labels 'ubuntu:docker://node:22'
./gitea-runner -c config.yaml config remove runner.labels 'ubuntu:docker://node:22'
./gitea-runner -c config.yaml config get runner.labels
```
Edits go through the YAML node tree, so comments, key order and the blank lines between top-level sections survive — a test asserts that appending a label to `config.example.yaml` changes nothing but the added line. Keys are resolved by reflecting over the `Config` struct's yaml tags, so an unknown key or a value of the wrong type is rejected before the file is touched. The write is atomic and keeps the file's symlink, owner, mode and line endings.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1140
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
`::set-env::` and `::add-path::` let a step rewrite the environment of every later step from its own output, which the runner honoured silently. They are now refused, as GitHub has done since 2020, and `ACTIONS_ALLOW_UNSECURE_COMMANDS` opts back in per step or job. Support for that variable is new here too, and is the only opt-in, matching GitHub rather than adding a runner config key on top.
Annotations keep their source location: Gitea has no annotation store and its web UI strips command properties, so `::error file=main.go,line=12::msg` is rendered as `::error::main.go:12: msg`.
`DEVELOPMENT.md` writes down the log line encoding rules this relies on.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1109
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
`ACTIONS_RESULTS_URL` names one origin serving every `github.actions.results.api.v1` service. Gitea serves the artifact half and this runner the cache half, so announcing `ACTIONS_CACHE_SERVICE_V2` while that URL pointed at Gitea was a promise the environment could not keep, and `docker buildx` posted its cache calls at Gitea and got a 404.
The cache server now forwards the artifact half to the instance each job registers with, so it is the whole results service and jobs are pointed at it. The announcement follows, and the bundle patch follows the cache URL instead.
Also fixes three things no JavaScript client reached: camelCase in the v2 responses where the Go clients read proto names, the missing `x-ms-request-id` on blob uploads that panics buildkit, and `cache.external_server` passed through without the trailing slash the v1 client concatenates onto.
Tests run the real actions against the services they look for: `actions/cache` over both API versions, the artifact actions up and back down through the forwarding, and `setup-node`. The regression itself is covered by asserting that whatever a job is handed as `ACTIONS_RESULTS_URL` answers a cache service call.
Fixes https://gitea.com/gitea/runner/issues/1139
Reviewed-on: https://gitea.com/gitea/runner/pulls/1141
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
`coerceToString` covered only a few kinds and returned the unconverted `reflect.Value` for the rest, so callers rendered contexts as `<*model.GithubContext Value>` and mangled sized integers and floats the same way. It now returns a string, which makes that placeholder unrepresentable, and is exported as `CoerceToString` so Gitea can drop its own diverging copy. It also takes an already reflected value, so internal callers need no conversion and no guard against the zero `Value`.
`format()` panicked whenever a lone `}` was followed by anything other than another `}`. Only a trailing `}` reached the existing unmatched-brace check, so an expression such as `format('a}b')`, which any workflow can write, took the process down instead. It now returns that same error.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1135
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
1. Drop `Masterminds/semver`, the Docker API version check needs dotted-numeric comparison and moby's client ships `versions.GreaterThanOrEqualTo`.
1. A malformed API version now reports as unsupported instead of panicking.
1. Drop `pkg/errors`, nothing used stack traces, `Wrap` or `Cause`.
1. Drop its depguard rule, and the `io/ioutil` one that only masked staticcheck's `SA1019`.
1. Re-sync `act/container/docker_cli.go` and `act/container/docker_cli_test.go`, copies of docker/cli's `opts.go` and `opts_test.go`, from a March and a 2022 commit to the version go.mod pins.
1. Record the local deviations in each header.
1. Fix `invalidParameter`, which lacked `Unwrap`, hiding the wrapped cause from `errors.Is` and `errors.As`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1132
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Docker 29.7 extracts copied archives through `os.Root`, which refuses to follow a symlink to an absolute target, so copying into `/var/run/act` fails with `path escapes from parent` on the many images that link `/var/run` to `/run`. The daemon now resolves every path component for us before any tar entry name is built, and the destination is created with one directory entry per missing component, which no daemon version rejects.
Verified against real daemons (29.4.0, 29.5.0, 29.5.1, 29.5.3, 29.6.2, 29.7.0-rc.1) with `debian:bookworm` (absolute symlink) and `alpine:3` (relative symlink), and against `moby/go-archive` v0.2.0 through the pending fix branch.
1. Fixes https://gitea.com/gitea/runner/issues/1128
1. Upstream bug: https://github.com/moby/moby/issues/53258
1. Supersedes the no-op change in https://gitea.com/gitea/runner/pulls/1129, which cannot help since the daemon strips leading slashes itself
Reviewed-on: https://gitea.com/gitea/runner/pulls/1130
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
`CopyTarStream` creates the destination directory by extracting a one-entry tarball with `DestinationPath: "/"`, but named that entry with the absolute `destPath`. Docker Engine 29.5+ tightened path validation on the copy API and rejects absolute entry names against a `/` destination with `statat var/run/act/actions/<sha>: path escapes from parent`, so the action directory never reached the job container and `actions/checkout` failed during "Set up job". Stripping the leading slash makes the entry relative, matching what the sibling `copyDir` already does and the upstream fix in nektos/act v0.2.89. Adds a regression test asserting the mkdir tarball entry is relative.
Fixes#1128
Reviewed-on: https://gitea.com/gitea/runner/pulls/1129
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Workflow-controlled `jobs.<job>.container.options` were merged directly into the
Docker `HostConfig`. When the runner's privileged mode is disabled, only
`Privileged` was forced to `false` — host namespace flags, capability expansion,
security-profile overrides, and device/runtime access from the workflow YAML
survived into the final `HostConfig`. A workflow author could therefore enter
host PID/IPC namespaces and execute commands as root on the runner host:
```yaml
container:
image: ubuntu:22.04
options: >-
--pid=host --ipc=host --cap-add=ALL
--security-opt seccomp=unconfined --security-opt apparmor=unconfined
```
## Fix
`mergeContainerConfigs()` now strips the dangerous options-derived `HostConfig`
fields before merging when privileged mode is off: `PidMode`, `IpcMode`,
`UTSMode`, `CgroupnsMode`, `UsernsMode`, `CapAdd`, `SecurityOpt`, `Devices`,
`DeviceCgroupRules`, `DeviceRequests`, `VolumesFrom`, `Runtime`, `CgroupParent`,
and `Sysctls`. Each strip emits a warning, matching the existing
`--network ignored` handling. Options remain honored when privileged mode is
enabled, since the administrator has already opted into host access.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1058
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Only the verbatim value of a secret was masked, so a secret leaked through an action that serialized it stayed readable: `toJSON(secrets)` escapes it, an Authorization header carries it base64-encoded, a URL percent-encodes it. Each secret and `::add-mask::` value is now masked in those forms too, matching the value encoders of GitHub's runner. Encodings that leave the value unchanged are skipped, so a plain token still costs a single replacement pair. Includes regression tests.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1108
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Starting two runner daemons with the same `.runner` file makes both present an
identical UUID+token, so Gitea treats them as one runner and they cancel each
other's jobs. This adds a non-blocking advisory lock on a sibling
`<runner-file>.lock`: the daemon (and `register`) acquire it at startup, and a
second process on the same host fails fast with a clear error instead of silently
interfering. The OS releases the lock when the process exits — including a hard
kill — so no stale lock is left behind. Legitimate multi-runner setups are
unaffected since each already uses its own `runner.file`.
Note: this covers the common single-host case; two hosts sharing a copied
`.runner` (e.g. over NFS) would still need server-side detection in Gitea.
---------
Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1099
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Serves `github.actions.results.api.v1.CacheService` next to the v1 cache API, sharing its store, plus the subset of the Azure blob protocol the toolkit uploads with. On by default via `cache.v2`, and works with `external_server`.
Clients reach it through two edits in the action's own bundle: the GHES check is opened, and the cache service URL is taken from `ACTIONS_CACHE_URL`.
The same GHES check is what makes the stock `actions/upload-artifact` and `download-artifact` abort on Gitea. Opening it makes them work without the `gitea-upload-artifact` fork, from `upload-artifact@v4.4.0` on.
Verified against 118 real bundles, every major version of 16 actions: 92 patched, the rest deliberately left alone, and every patched bundle checked with `node --check`. Also end to end against pinned `actions/cache@v6.1.0` with an unreachable results URL, so only the patch can make the cache work.
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1110
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Leaked per-job networks each hold a subnet of the daemon's address pool and nothing reclaimed them, so a host eventually fails every job with `all predefined address pools have been fully subnetted`. This is what CI hit in https://gitea.com/gitea/runner/actions/runs/742177.
- teardown no longer loses a container, and its network with it: a failed `ContainerRemove` was reported as success, a container whose id was never learned was skipped, and the daemon's own `AutoRemove` teardown was raced
- the idle cleanup reclaims what teardown cannot: networks carry `com.gitea.runner.uuid`, so a runner only touches its own, and a cutoff keeps a job starting during the pass out of scope
- pull failures reported mid-stream were discarded, surfacing later as a confusing `No such image`; they now propagate, and fall back to a local copy instead of failing the job
- `NetworkCreate` retries pool exhaustion briefly, then says which knobs to turn
- digest-pinned images are not re-pulled, and removal kills first so it never waits out Podman's stop timeout (measured 10.1s → 0.09s per container)
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1124
Reviewed-by: bircni <bircni@icloud.com>
Set `http_proxy`, `https_proxy` and `no_proxy` in the runner's environment and everything the runner controls uses them.
Go already read them for the runner's own requests. This adds jobs, in lower and upper case, service containers, and Dockerfile action builds.
Some hosts are added to `no_proxy` for jobs so they stay direct: the cache server, loopback, the job's service containers, and a `tcp://` Docker daemon. Without the last one the Docker client sends its API calls to the proxy and docker-in-docker breaks. Gitea is not added.
Images are pulled by the Docker daemon, which has its own proxy setting. In the `dind` images it reads these same variables. The runner warns at startup if it has a proxy and the daemon does not.
Fixes https://gitea.com/gitea/runner/issues/1118, originally reported as https://gitea.com/gitea/runner/issues/708.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1112
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
An unchecked type assertion panics on input it did not expect, as a missing `tool_cache` key did in https://gitea.com/gitea/runner/pulls/1122.
Every flagged site is now handled where it can fail, or typed so it cannot: a `lock.Keyed` replaces the two `sync.Map` mutex registries, and the reporter's outputs carry an explicit sent flag. Mocks keep their assertions, a mismatch there is a setup error the panic names.
Bugs it turned up (only the first is reachable from workflows):
1. A scalar `matrix.include` or `matrix.exclude`, e.g. `include: foo` or `include: [1, 2]`, panicked the runner with `interface conversion: interface {} is string, not map[string]interface {}`. Verified against `main`, it is now a workflow error. `OnSchedule` panicked the same way on a malformed `on.schedule` entry.
1. `ExternalURL()` panicked on the nil listener after `Close()`, the port is now resolved once at startup.
1. `errors.Is(err, git.ErrShortRef)` followed by `err.(*git.Error)` panics as soon as anything wraps that error, so it is `errors.As` now.
1. An output name the server acknowledged without ever being sent one was recorded as sent forever, which silently dropped a later value for that name.
48576ab3e5 fixes one discovered issue: a matrix key holding a nested object was logged and then run as if the job had no matrix, so it now fails like an unknown `exclude` key.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1123
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
1. Docker rejects two mounts on one target, so a `container.volumes:` or `--volume` aimed at `/opt/hostedtoolcache` failed the job with `Duplicate mount point`. Job and service volumes now displace the mount on the same path, and `name:/target:ro` no longer mounts read-write at the literal path `/target:ro`.
1. Setting `RUNNER_TOOL_CACHE` only changed what the variable said, the cache stayed where it was, so tools writing to it landed outside the mount and `${{ runner.tool_cache }}` disagreed with the variable. It now relocates the cache. Leaving it unset behaves as before.
1. Unknown `config.yaml` keys now warn instead of being dropped without a trace.
Fixes https://gitea.com/gitea/runner/issues/813
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1122
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Adds `runner.hooks.job_started` and `runner.hooks.job_completed`: operator scripts that run inside the job environment, before the job's first step and after its last one.
```yaml
runner:
hooks:
job_started: /hooks/started.sh
job_completed: /hooks/completed.sh
```
Equivalent to GitHub's `ACTIONS_RUNNER_HOOK_JOB_STARTED` / `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`, which are read when unset: output is scanned for workflow commands, `$GITHUB_ENV` and `$GITHUB_PATH` are read back, and a non-zero exit fails the job.
Fixes: https://gitea.com/gitea/runner/issues/779
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1111
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Full suite 178s → 96s, and a run's log 12MB → 2KB. `act/runner` was 177s of the 178s, serialised behind one docker daemon.
- Its fixtures now run in parallel, bounded by a slot count instead of `go test -parallel`, with a per-test container name prefix and a pinned `MaxParallel`.
- Fixtures asserting a job failure leaked their container and network (`AutoRemove` was at the act-CLI default), filling the daemon's address pool over time.
- Replaced sleeps used as synchronisation in the parallel-executor and cache-handler tests.
- Dropped duplicate coverage: two files re-testing `NewParallelExecutor`, a test asserting on its own semaphore, and `TestDockerActionForcePullForceRebuild`, whose config `runTest` discarded.
- `fmt-check`/`security-check` move from `make test` to a `checks` target; `security-check` no longer installs `xgo` and `gxz`.
- Test flags follow gitea: `GOTEST_FLAGS ?= -race -timeout 20m -parallel 8`, with `-cover`/`-coverprofile` left in the target. Dropped `-v`, since a failing package still prints its full output without it.
Coverage unchanged at 73.4%.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1121
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
A docker action can declare `runs.pre-entrypoint` and `runs.post-entrypoint` next to `runs.entrypoint` — the docker equivalent of a javascript action's `runs.pre`/`runs.post`. Neither key existed on `ActionRuns`, so the YAML decoder dropped them and docker actions silently skipped their setup and cleanup stages.
Only the entrypoint is stage specific. [`ContainerActionHandler`](https://github.com/actions/runner/blob/main/src/Runner.Worker/Handlers/ContainerActionHandler.cs) selects `Data.Pre`/`Data.Post` per stage but evaluates `runs.args` and `runs.env` unconditionally, so `docker create` receives the action's args and env on every stage. The `entrypoint` input stays main only, because it is read inside the main branch.
The stage is carried as the existing `stepStage` value rather than a separate parameter, and the pre and post stages reuse the action path resolution that `runPreStep`/`runPostStep` already open-coded three times, so the diff also drops those copies.
Known gap: `stepActionLocal.pre()` is a no-op for every `using`, so `pre-entrypoint` does not run for `uses: ./local-action` while `post-entrypoint` does. Closing it requires reading the action model before the main stage and adding a pre case to `getIfExpression` for `pre-if`, which also changes local node, go and composite actions — better as its own change.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1106
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Passes the `runner` context values act already knew but never reported to jobs:
1. `runner.name` / `RUNNER_NAME` — registered runner name, hostname for `exec`
2. `runner.environment` / `RUNNER_ENVIRONMENT` — `self-hosted`
3. `RUNNER_WORKSPACE` — parent of `GITHUB_WORKSPACE`
4. `runner.debug` / `RUNNER_DEBUG` — `1` when `ACTIONS_STEP_DEBUG` is set
`ImageOS` now prefers the release named in the resolved image tag, so `ubuntu-latest` mapped to `runner-images:ubuntu-24.04` reports `ubuntu24` instead of the hardcoded `ubuntu20`. The `runs-on` label stays the fallback.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1105
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Command data is percent-escaped on the wire because Gitea's job log cannot carry a newline, and the renderer decodes it.
1. Unescaping iterated a map, so order was random per process: `%250A` decoded to a newline instead of a literal `%0A` about two runs in three. Now a precompiled `strings.NewReplacer`.
1. Escape the data of command lines the runner writes itself (`##[error]`, `::group::Run …`), so decoding returns the original text instead of mangling a `%`. Multi-line runner errors also get real line breaks instead of a literal `\n`.
1. Register secrets in escaped form too — one containing `%` reaches the log as `%25…` and was never masked.
1. Decode in `jobLogFormatter`, so `gitea-runner exec` matches the web view.
## Relation to the Gitea PR
https://github.com/go-gitea/gitea/pull/38659 makes the renderer decode command data. Point 2 is required by it; the rest stand alone. Either order works — until both land, a new runner on old Gitea shows `%25`, an old runner on new Gitea shows the mangling point 2 fixes.
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1120
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Mirrors every release artifact into Cloudflare R2 alongside the existing AWS S3
upload, so both buckets carry the same objects during a parallel period. S3 is
untouched and stays authoritative for now; once R2 is confirmed complete it can
be dropped by deleting the `blobs:` block and pointing this at R2 alone.
### Why `publishers:` and not a second `blobs:` entry
goreleaser's blob pipe authenticates from the global `AWS_*` environment and has
no per-entry credential fields, so two different credential sets (AWS S3 and
Cloudflare R2) cannot coexist in `blobs:`. Custom publishers do support
per-entry `env:`, which is the supported way to isolate the two.
### Why `curl --aws-sigv4` and not `aws`/`rclone`
The CI image `docker.gitea.com/runner-images:ubuntu-latest` ships none of `aws`,
`rclone`, `s3cmd` or `mc`, but does ship curl 8.5 with `--aws-sigv4`. This keeps
the change zero-install. Credentials are fed to curl through a config file on
stdin rather than argv, so they never appear in the process list.
### Behaviour
- Object layout is identical to S3 (`gitea-runner/<version>/<artifact>`), so
migrating consumers later means changing only the host, not the path.
- Nightly gets R2 too, matching the existing nightly-to-S3 behaviour.
- Missing R2 configuration fails the build. Because custom publishers run as the
very last step of the publish pipeline, a preflight `--check-config` step runs
right after checkout so the failure happens before anything is built or
published rather than after the release already exists.
- Verified against a local MinIO instance (site region `auto`, matching R2):
successful upload byte-compared after download, plus the missing-variable,
wrong-credentials, missing-file and bad-argument paths.
### Required repository secrets
These must be configured before the next release run, otherwise the new
preflight step will fail the workflow:
- `R2_ENDPOINT` (full base URL, e.g. `https://<account>.r2.cloudflarestorage.com`)
- `R2_BUCKET`
- `R2_ACCESS_KEY_ID`
- `R2_SECRET_ACCESS_KEY`
### Known, deliberate wart
The publisher runs 109 times for 73 distinct object keys: goreleaser's release
pipe already registers `release.extra_files` as `UploadableFile` artifacts, and
`internal/exec` appends the publisher's own `extra_files` on top with no
de-duplication. It cannot be globbed away because `gobwas/glob` has no
substring-exclusion matcher, so `./**.sha256` cannot be narrowed to exclude
`*.xz.sha256`. It is harmless since PUT is idempotent, and the redundant
`./**.xz` glob is kept on purpose so the publisher declares its own complete
file set instead of implicitly depending on the `release:` block's globs. This
is documented in a comment above the block.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1114
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Fixes https://gitea.com/gitea/runner/issues/667
Fixes https://gitea.com/gitea/runner/pulls/1103
`container.options` is parsed with docker/cli's `addFlags`, which omits the flags docker/cli registers on the `create` and `run` commands themselves. Those were rejected as unknown — most visibly `--platform`.
| Flag | Behavior | Why |
| --- | --- | --- |
| `--platform` | overrides the runner-wide container architecture | the only way to pick an image architecture per job, e.g. across a matrix |
| `--pull always\|missing\|never` | `always` forces a pull, `never` skips it | `force_pull` is runner-wide config today, with no per-job control |
Both are resolved in `NewContainer` because the image pull runs before the container is created and the two have to agree.
`--name`, `--quiet` and `--disable-content-trust` are now accepted and ignored, and `--use-api-socket` is rejected pointing at `container.docker_host`, so a valid `docker create` line no longer fails outright.
<sub>Written by Claude (Opus 4.8).</sub>
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1104
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Fixes https://gitea.com/gitea/runner/issues/659
Docker teardown was chained with `Then`, which stops on the previous step's error and on a cancelled context, so the first failure orphaned everything downstream. A volume that is still in use, or a flaky daemon, left the service containers and the job network behind — matching the reports in the issue, where the leaks show up after failed or overloaded jobs.
Cleanup is now straight-line and best-effort: every step runs regardless of what failed before it, and the errors are joined. Two things follow from that:
- Service containers are removed before the job volumes, since a service can hold one via `--volumes-from` in its options. The network stays last, once every container has detached.
- Only job container and volume errors are returned. Service and network errors are logged, as before, because `stopJobContainer()` also runs as the pre-flight step of job start, where a network cleanup error must not abort the job.
The nil check on `rc.JobContainer` is kept, but it is not the leak reporters are hitting: `container.NewContainer` never returns nil in the docker build, so cleanup with no job container is only reachable in the `WITHOUT_DOCKER` stub.
Addresses https://gitea.com/gitea/runner/pulls/1066#issuecomment-1239073.
Not covered here: the `buildx_buildkit_*` volumes reported in the issue are created by buildx inside the job, not by the runner, so no runner-side teardown removes them.
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1066
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
This PR adds a new `cache.external_secret_file` config, which points at a file holding the secret. So the secret can come from a mounted Kubernetes/Docker secret while the rest of the config stays plain text.
```yaml
cache:
external_server: "http://cache-host:8088/"
external_secret_file: /path/to/cache_external_secret
```
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1100
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
`Statfs_t.Bavail` is unsigned on Linux but signed on FreeBSD, so `Bavail * uint64(Bsize)` in `internal/app/run/disk_unix.go` fails to compile for the freebsd targets goreleaser cross-builds, breaking the nightly release (introduced in https://gitea.com/gitea/runner/pulls/1090). The `checks` workflow only builds for the host, so it never cross-compiles freebsd and stayed green.
Casting both operands to `uint64` makes the arithmetic signedness-agnostic across all unix variants.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1098
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Opt-in local task-admission checks under a `health_check` config section (disabled by default):
- pause new task fetching when free disk space on the workspace volume is below the configured minimum
- optional executable health-check script — a non-zero exit, timeout, or start failure marks the runner unavailable
- checks run only while the runner is idle; the last result is reused while a job is active, and polling resumes automatically on recovery
- `/readyz` reports task-admission readiness (reusing the poll loop's last check); `/healthz` stays a process-liveness endpoint
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1090
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
## Problem
On host-mode Windows runners, a job can leave processes running after it finishes,
and those leftovers hold file handles that block deletion of the workspace.
Today a step's tree is only torn down when the step is *cancelled*
(`process.Killer`). A step that completes leaves whatever it spawned alive, and
the existing workspace scan in `terminateRunningProcesses` misses two shapes of
leftover: orphans whose parent already exited (no tree to walk, and their
executable often lives outside the workspace), and processes that merely *run in*
the workspace but reference no path from it — `Win32_Process` exposes no working
directory, so the scan cannot match them.
## Solution
Two additions in `internal/pkg/process`, both no-ops outside Windows:
- **`process.Group`** — a job-scoped Windows Job Object created with
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. Step processes are assigned to it before
they get their own `Killer`, so the step's job nests inside the job's:
cancellation still kills exactly the step's tree, while `Remove` closing the
group makes the kernel terminate everything still assigned, whatever its
parentage. The kernel also drops the handle when the runner exits, so a crashed
runner cannot strand processes.
- **`process.KillProcessesWithCWDUnder`** — a best-effort net for processes that
never joined the job (started via a service or scheduled task). It reads each
process's working directory from its PEB and terminates those under a workspace
dir. Processes it cannot open are skipped.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1080
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reshapes the job log's "Set up job" section to mirror `actions/runner`:
- runner name/version, then `Runner Information` (labels, task, job, repository, event) and `Operating System` groups
- every required action downloaded up front under `Prepare all required actions`, each as `Download action repository '<action>@<ref>' (SHA:<sha>)`
- `Complete job name` closes the section
Downloading up front is the one behavioral change: the same set was already fetched during the pre stage regardless of a step's `if`, now just before the first pre step, so a download failure is reported against the job rather than a step.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1089
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
`reportStepError` reported every step error as FAILURE, including a `context.Canceled` from a docker file-command read cancelled at job finalization — non-deterministic red CI. Classify `context.Canceled` as an interruption instead (deferring to the job context), so a genuine cancel reports cancelled and a stray teardown cancellation is ignored, never a failure.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1095
Reviewed-by: bircni <bircni@icloud.com>
Containers set `HostConfig.AutoRemove` but act also removes them explicitly, so the two removers race and the loser logs a 409 `removal of container X is already in progress` — seen at the end of nearly every `uses: docker://` step.
The explicit remove is redundant for `docker://` steps and docker actions (`Start(true)` already awaited exit), so it's skipped. Job and service containers keep both removers — their `sleep` entrypoint needs `AutoRemove` as a fallback reaper — so there the race is inherent and `remove()` now treats `NotFound` and `Conflict` as success.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1093
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
The `cancelled()`, `success()` and `failure()` expression functions dereferenced `Job.Status` unconditionally, so a nil `Job` context panicked the interpreter — which is why Gitea currently hands the runner a non-nil (empty) `JobContext` as a workaround. This routes all three through a `jobStatus()` helper that treats a nil `Job` as an empty status, keeping existing behaviour identical while removing the panic. Includes a regression test that panics on the old code and passes with the fix.
Related: https://github.com/go-gitea/gitea/pull/38495
Reviewed-on: https://gitea.com/gitea/runner/pulls/1092
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Gitea 1.27 resolves `workflow_call` inputs server-side and sends natively typed JSON values in `github.event.inputs`, so a `type: boolean` input now arrives as a real JSON boolean. The runner coerced booleans by comparing the `any` value against the string `"true"`, which a native bool never matches, so every boolean input evaluated to `false` — including when the callee relied on its default, since the server pre-fills defaults into the event payload and the string fallback is never reached. This accepts a native bool and keeps the string comparison as a fallback, since `workflow_dispatch` inputs are still strings and YAML defaults decode to strings; servers before 1.27 never put a native bool in the payload, so they take the exact same code path as before.
The same coercion is applied to `setupWorkflowInputs` (locally-called reusable workflows, `uses: ./.gitea/workflows/x.yml`), where a `type: boolean` input was previously a native bool when passed as `with: { flag: true }` but a string when interpolated or taken from `default:`. It is now always a bool, matching GitHub, whose `inputs` context "preserves Boolean values as Booleans instead of converting them to strings". **This is potentially breaking**: `inputs.flag == 'true'` now evaluates to `false` and must become `inputs.flag == true`. That pattern is already false on GitHub (a bool compared to a string coerces to `1 == NaN`), but it works on Gitea today, so I am happy to split this hunk into its own PR if you would rather keep this one backport-safe.
Fixes#1082
Reviewed-on: https://gitea.com/gitea/runner/pulls/1087
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: Nicolas <bircni@icloud.com>
`SetOutputs` logged "ignore output because the value is too long" for values
larger than 1 MiB but then fell through and stored the value anyway, sending
it upstream via `UpdateTask`. The key-too-long branch directly above correctly
skips oversized keys with `continue`; this adds the same `continue` to the
value branch so the size guard is actually enforced and the log message
matches the behavior.
Adds regression coverage in `TestReporter_SetOutputs` for an oversized value
(dropped) and a value at exactly the 1 MiB limit (retained).
---------
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1070
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
A batch of small, self-contained fixes and docs/example additions.
Fixes#625 - align the example config's `force_pull` with the actual default (`false`)
Fixes#804 - return an error instead of discarding `os.UserHomeDir()` when defaulting `cache.dir`/`host.workdir_parent`
Fixes#571 - send a `gitea-runner/<version>` User-Agent on API requests
Fixes#650 - detect an `Unauthenticated` fetch response and exit the daemon with an error instead of retrying forever
Fixes#766 - add `exec --eventpath` to supply a JSON event payload file
Fixes#256 - add a `bug-report` subcommand that prints version/Go/OS-arch/CPU info
Fixes#617 - add `runner.set_act_env` (default `true`) to optionally omit the `ACT=true` env var
Fixes#635 - record a failure result (and guard a nil reusable-workflow caller) when the job `if`-expression fails to evaluate
Fixes#1005 - remove README docs for config env-var overrides that were already removed from the code
Fixes#448 - clarify in `exec --job` help that `--workflows` may be needed to disambiguate
Fixes#209 - note that `host`-labelled runners still need Docker for `docker://` actions and service containers
Fixes#757 - add a systemd service example with automatic restart
Fixes#474 - add a Kubernetes StatefulSet example that persists the `.runner` registration across reschedules
Fixes#776 - build the basic (non-dind) docker image for `linux/riscv64`
Fixes#628 - build the basic (non-dind) docker image for `linux/s390x`Reviewed-on: https://gitea.com/gitea/runner/pulls/1075
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
GitHub Actions skips services whose image evaluates to an empty string, enabling conditional services via expressions like `image: ${{ matrix.image || '' }}`. Here such a service failed the job at `docker create`. Skip them before container creation, using GitHub's log message verbatim, and add a regression test.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1074
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Refs https://gitea.com/gitea/runner/issues/155
The remaining failure mode described in the issue comments is the job container timing out when it tries to reach ACTIONS_CACHE_URL. The generated config now explains that cache.host/cache.port must be reachable from job containers, and calls out dockerized runners with auto-created per-job networks as a case that may need a fixed published cache endpoint or a shared Docker network
Co-authored-by: bircni <bircni@icloud.com>
Fixes: https://gitea.com/gitea/runner/issues/1061
Related: https://github.com/moby/moby/pull/52886
Docker 29.6.0 changed `dockerd` to shell out to the external `nft` binary for firewall-rule cleanup instead of linking `libnftables` (release note: *"Mitigate a crash in libnftables when using nftables as the firewall backend by changing the default build option to execute the `nft` command instead."*). The upstream `docker:dind` image only ships `iptables`/`ip6tables`, so `dockerd` logs `failed to find nft tool` on startup and shutdown.
Networking is unaffected (default firewall backend is still iptables), but the errors are noisy. Install `nftables` in both dind variants to provide the binary.
*PR authored by Claude (Opus 4.8).*Reviewed-on: https://gitea.com/gitea/runner/pulls/1064
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
A composite action's `RunContext` is **seeded with its parent's `Masks` slice** in `newCompositeRunContext`. After the composite finished running, the *entire* `compositeRC.Masks` (the seeded parent masks plus any added during the run) was appended straight back into the parent:
```go
rc.Masks = append(rc.Masks, compositeRC.Masks...)
```
Because the composite already contained the parent's masks, every composite action roughly **doubled** the parent's `Masks` length. With many nested/repeated composite actions this grows exponentially:
```
before=1152 after=2304 compositeLen=1152
before=2304 after=4608 compositeLen=2304
before=4608 after=9216 compositeLen=4608
...
```
### Impact
The bloated `Masks` slice made per-log-line secret redaction (`strings.makeGenericReplacer`, rebuilt per line since #1001) extremely slow. Log writes fell behind enough to keep subprocess pipes open after the process had already exited, surfacing as seemingly random CI failures — most often in later steps of mask-heavy workflows:
```
Error: exec: WaitDelay expired before I/O complete
```
The split of `cmd.Run()` into `cmd.Start()` + `cmd.Wait()` (#883) is what turned this long-standing latency into a hard failure.
### Fix
Only append masks that aren't already present in the parent, via a small `appendUniqueMasks` helper. This keeps the mask count bounded by the number of *distinct* secrets instead of growing exponentially.
I kept `Masks` as a `[]string` rather than switching to a set type, since it's passed by pointer to loggers across `logger.go`, `run_context.go`, and `runner.go` — a type change would be a much wider, riskier refactor. The `O(n·m)` dedup is harmless precisely because `n` now stays small.
Fixes #1054Reviewed-on: https://gitea.com/gitea/runner/pulls/1059
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
When `DEFAULT_ACTIONS_URL=self`, action clone URLs (`uses: owner/repo@ref`) are
built from the Gitea **AppURL** (`gitea_default_actions_url`), but
`shouldCloneURLUseToken` compared the clone URL host only against the runner's
**registered address** (`GitHubInstance`).
When the runner registers with a different hostname than AppURL — same instance,
different DNS (e.g. `gitea.local` vs `gitea.my-nas.lan`, internal vs external) —
the strict `u1.Host == u2.Host` check returns false, so the task token is **not**
attached and the action clone goes out anonymously. Against an instance with
`REQUIRE_SIGNIN_VIEW=true` this fails with:
```
Unable to clone https://gitea.example/owner/action refs/heads/v1: authentication required
```
The current workaround is to make the runner's registered host exactly match
`AppURL`. This PR removes the need for that.
Refs: https://github.com/go-gitea/gitea/issues/27933
## Change
- `shouldCloneURLUseToken` now trusts the clone URL when its host matches **either**
the registered instance (`GitHubInstance`) **or** the self-hosted default-actions
instance (`DefaultActionInstance`). Embedded basic auth is still rejected, and the
empty-host cases are unchanged.
- A new `Config.DefaultActionInstanceIsSelfHosted` flag gates the second candidate.
It is set in the daemon layer (`run/runner.go`, `exec.go`), where `github.com` and
a configured `GithubMirror` are distinguishable, so the token is **never** attached
for off-instance hosts.Reviewed-on: https://gitea.com/gitea/runner/pulls/1056
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
## Summary
A nested `uses:` action inside a **local composite action** fails to clone with a bare `authentication required: Unauthorized` (401) when the instance resolves host-less action references against itself (`DEFAULT_ACTIONS_URL = self`). The job fails at the composite **main→post boundary**, even though the composite's own steps and all post-steps report success.
## Root cause
`newCompositeRunContext` nils `Config.Secrets` so composite steps do not see job secrets. But the action-clone path in `prepareActionExecutor` sourced its token via `getGitCloneToken` → `Config.GetToken()` → `Config.Secrets`, which is empty inside a composite RunContext. The nested action is therefore cloned anonymously → 401 against the authenticated instance.
Two details explain the exact symptom:
- It surfaces at the composite **main→post boundary** because the swallowed nested-step error is re-emitted by `common.JobError` at the end of the composite main pipeline.
- It carries **no clone URL** because, on a warm action cache, only `r.Fetch` runs (not `PlainClone`), and the go-git fetch error is returned verbatim.
## Fix
Source the clone token from `github.Token` instead of `Config.Secrets`. It is preserved across the composite config copy (`Config.Token` / `PresetGitHubContext`) and is identical to `Config.GetToken()` at the top level, so top-level and `act exec` behaviour is unchanged. The `shouldCloneURLUseToken` host gate is kept so the token is never sent to a foreign host. This also aligns the git-clone path with the ActionCache fetch path, which already uses `github.Token`.
Reusable workflows are unaffected — their RunContext keeps `Config.Secrets`.
## Before / after
Local composite action with a nested `uses:` (+ post step), followed by a marker step. Same workflow, same runner host — only the runner fix differs.
| Job step | Before fix | After fix |
| --- | --- | --- |
| `Run actions/checkout@v6` | ✅ success | ✅ success |
| `Run ./.gitea/actions/probe-composite` (nested `uses:` + post) | ❌ **failure** — bare 401 at the main→post boundary | ✅ success |
| `Step after composite` | ⊘ **skipped** | ✅ **ran** |
| Job result | ❌ **failed** | ✅ **succeeded** |
### Before — boundary log
```text
::endgroup::
##[error]authentication required: Unauthorized ← bare 401, no clone target
Run Post ./.gitea/actions/probe-composite
Success - Post ./.gitea/actions/probe-composite ← post steps run and succeed
Success - Post actions/checkout@v6
Job failed
```
The composite's own steps and both post-steps report `Success`; the job fails solely on the bare 401 emitted at the main→post boundary, and the next step is skipped.
### After — boundary log
```text
Run ./.gitea/actions/probe-composite
...composite steps...
Success - ./.gitea/actions/probe-composite
Run Marker after composite
PASSED composite boundary — no 401 (runner fix confirmed)
Success - Marker after composite
Job succeeded
```
## Reproduction
`.gitea/actions/probe-composite/action.yml`:
```yaml
name: probe-composite
runs:
using: composite
steps:
- uses: actions/setup-node@v6 # nested uses → has a post step
with: { node-version: 22 }
- run: echo "composite inner step OK"
shell: bash
```
`.gitea/workflows/probe.yaml`:
```yaml
on: [push]
jobs:
composite-boundary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./.gitea/actions/probe-composite
- run: echo "step after composite" # skipped before the fix; runs after
```
## Verification
- **Before:** bare 401 at the boundary, reproduced with a `delay=0` tail — rules out a token-TTL / expiry effect; it is a missing credential.
- **After:** the composite step succeeds, the step after the composite runs, the job succeeds, and there is no `authentication required` in the log.
- A reusable workflow (`workflow_call`) with the same nested `uses:` + post step never hit the 401, which isolated the bug to the composite main/post path.
## Tests
Adds `TestStepActionRemoteCloneTokenSurvivesNilSecrets` (`act/runner`): asserts the clone token is forwarded when the RunContext mirrors a composite (`Secrets == nil`, token via `Config.Token`), and that the host gate still withholds the token for a foreign host. Verified to fail without the fix and pass with it.
---------
Reviewed-on: https://gitea.com/gitea/runner/pulls/1041
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: Christian Heim <christian@heimdaheim.de>
Co-committed-by: Christian Heim <christian@heimdaheim.de>
Fixes#1039 — local docker actions referenced with `uses: ./` are incorrectly served from a cached image built for a *different* repository.
## Root cause
For a local docker action, the built image tag is derived from `actionName`, which is the **workspace-relative** path of the action. For a self-referencing action that is always `"./"`, regardless of which repository it lives in. In `execAsDocker` this collapsed to the constant tag `act-dockeraction:latest` for *every* repository.
Before building, the runner checks `ContainerImageExistsLocally` and skips the rebuild if a matching tag is present. On a shared docker daemon, once repository A built its image, repository B's `uses: ./` found the same tag, skipped its rebuild, and **ran repository A's action image** — the reported "the first action is run again, because the path is cached", including the cross-repository concern raised in the issue.
## Fix
Extracted the tag computation into a small, testable `dockerActionImageTag(repository, actionName, localAction)` function. For local actions the tag is now namespaced with the repository (`path.Join(repository, actionName)`):
- different repositories no longer collide (`act-owner-repo-a-dockeraction:latest` vs `act-owner-repo-b-dockeraction:latest`);
- image caching is preserved within a repository (tag stays stable);
- remote actions are unchanged — they already carry a unique, ref-scoped `actionName` (the uses hash).
---------
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1051
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-committed-by: bircni <bircni@icloud.com>
- fix Windows-only golangci-lint findings in `lp_windows.go` and process killer handle cleanup
- add a `lint-go-windows` target that runs golangci-lint with `GOOS=windows`
- include the Windows lint pass in `make lint` so Ubuntu CI covers `_windows.go` files
Reviewed-on: https://gitea.com/gitea/runner/pulls/1052
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
## Problem
When a workflow run is **cancelled**, the runner diverged from GitHub Actions:
- Main-stage `if: ${{ always() }}` / `if: ${{ cancelled() }}` steps **did not run** at all (unlike a *failed* run, where they do).
- `if: ${{ cancelled() }}` was **structurally impossible** to satisfy — it could never be true.
GitHub Actions runs `always()`/`cancelled()` steps (and post cleanup) even when a job is cancelled. This runner only honored that for action *post* steps (since #1016), leaving main-stage cleanup steps silently skipped.
## Root causes (both in `act/`)
1. **`getJobContext()`** derived the job status purely from step conclusions, so it could only ever return `"success"` or `"failure"`. Since `cancelled()` checks `Job.Status == "cancelled"`, it was impossible — and `success()` stayed *true* on a cancelled run, so the wrong `if` branch was taken everywhere.
2. **The main step pipeline** is chained with `Executor.Then()`, which short-circuits the moment `ctx.Err() != nil`. Once the server cancelled, every not-yet-started main step (including `always()` ones) was abandoned.
## Fix
- Add a per-`RunContext` `jobCancelled` flag + `markCancelled()`. `getJobContext()` now reports `"cancelled"` (taking precedence over success/failure), so `cancelled()`/`always()` are true and `success()`/`failure()` are false — matching GitHub's "only always()/cancelled() run on cancel" semantics.
- Replace the plain main-steps pipeline with `newMainStepsExecutor`. On interruption (`context.Canceled` from a server cancel, or `context.DeadlineExceeded` from the job timeout) it marks the job cancelled and runs the **remaining** steps under a fresh context (`context.WithoutCancel` + bounded timeout) so `always()`/`cancelled()` steps run for cleanup, while default-`success()` steps skip themselves. The original interrupt error is still propagated upward.
- Backstop `markCancelled()` in the post-step `Finally` so cancellations landing outside the main loop still surface the cancelled status to post steps.
Pre-steps keep normal short-circuit behavior, and reporting (`RESULT_CANCELLED`) is untouched — that remains handled by #1016.
## Reporting semantics (unchanged by this PR)
| Run state | failing post/`always()` step reported as |
| --------- | --------------------------------------------------------------------------------------------- |
| Normal | **FAILURE** |
| Timeout | **FAILURE** (deadline path preserves the job-error container) |
| Cancelled | **CANCELLED** — cancellation wins; the failing step is logged but doesn't flip the conclusion |
The new `always()` path runs under `context.WithoutCancel`, so the job-error container is preserved — a failing `always()` step records its failure at step level and does not panic in `SetJobError`.
Fixes#657
Reviewed-on: https://gitea.com/gitea/runner/pulls/1043
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
## Summary
When a workflow references a remote action (e.g. `uses: actions/checkout@v4`) the runner clones that repository during job setup.
Previously this was always a full clone(every branch and the complete history) even though only a single ref is needed.
This PR makes the runner shallow-clone the requested ref by default (`--depth=1`, single branch), falling back to a full clone when a shallow clone fails.
Notes:
- Existing on-disk caches are reused as-is; there is no forced re-clone on upgrade.
## Changes
- A new `runner.action_shallow_clone` option (default `true`) lets operators opt back into full clones.
- `cloneAtDepth`: attempt a shallow clone; fall back to a full clone when shallow clone fails.
- Keep a shallow cache cheap on update: fetch the single requested ref at depth 1 and skip `pull`.
---------
Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1053
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
Fixes https://gitea.com/gitea/runner/issues/1047
The runner panics during shutdown when jobs are interrupted:
```
panic in executor: interface conversion: interface {} is nil, not map[string]error
...
gitea.com/gitea/runner/act/common.SetJobError(...)
/data/gitea/runner/act/common/job_error.go:27
gitea.com/gitea/runner/act/runner.reportStepError(...)
/data/gitea/runner/act/runner/job_executor.go:62
```
`SetJobError` did an unchecked type assertion on the job-error container stored in the context:
```go
ctx.Value(jobErrorContextKeyVal).(map[string]error)["error"] = err
```
When the container is absent — e.g. on shutdown, when a job is interrupted and `reportStepError` → `SetJobError` runs on a context where `WithJobErrorContainer` was never called — `ctx.Value` returns `nil` and asserting `nil.(map[string]error)` panics.
## Fix
Use the comma-ok form so a missing container is a no-op, matching the safe pattern already used in the sibling `JobError` function. If there's no container, there's nowhere to record the error anyway, so skipping is correct.
```go
func SetJobError(ctx context.Context, err error) {
if container, ok := ctx.Value(jobErrorContextKeyVal).(map[string]error); ok {
container["error"] = err
}
}
```
Reviewed-on: https://gitea.com/gitea/runner/pulls/1050
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
Co-committed-by: bircni <bircni@icloud.com>
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| docker | stage | minor | `29.5.3-dind-rootless` → `29.6.0-dind-rootless` |
| docker | stage | minor | `29.5.3-dind` → `29.6.0-dind` |
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTEuMiIsInVwZGF0ZWRJblZlciI6IjQzLjE5MS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Reviewed-on: https://gitea.com/gitea/runner/pulls/1044
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
Two `jobs.<job_id>` workflow syntax fields were parsed from YAML but silently ignored. This PR implements both:
- **`jobs.<job_id>.timeout-minutes`** — applies a context deadline around the entire job execution (container start, pre-steps, main steps, post-steps). Mirrors the existing step-level `evaluateStepTimeout`. Supports expression interpolation (e.g. `${{ env.MY_TIMEOUT }}`).
- **`jobs.<job_id>.continue-on-error`** — evaluates the expression when a job fails. If all failing matrix combinations had `continue-on-error: true`, the job does not cause the workflow run to fail (`handleFailure` skips it), and the tolerated failure reports `success` to dependent jobs through the `needs` context so jobs gated on the default `if: success()` still run (matching GitHub). The "any firm failure wins" rule is serialised under the existing per-job lock, so parallel matrix combinations are safe.
Both features follow the same patterns already used at the step level (`evaluateStepTimeout` / `isContinueOnError` in `act/runner/step.go`).
## Version compatibility
These changes are backward compatible. With mismatched versions the feature degrades silently to the previous behaviour (field ignored) — no errors on either side.
- `timeout-minutes`: runner-only, no server dependency.
- `continue-on-error`: requires both this runner PR and the matching Gitea server PR to take full effect. With only one side updated, the field continues to be ignored.
Related: [Github](https://github.com/go-gitea/gitea/pull/38100)
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1032
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
- Adds `runner.post_task_script` and `runner.post_task_script_timeout` (default `5m`) to run a host executable after each task’s built-in cleanup (post-steps, container teardown, bind-workdir removal).
- Stops task heartbeats via `Reporter.StopHeartbeats()` while the script runs so Gitea won’t assign overlapping work; the final task acknowledgement still happens in `reporter.Close()`.
- Script output goes to the runner process log; non-zero exits are warned only and do not change the job result.
- Documents lifecycle, offline behavior, timeouts, and Windows limits (`.ps1` not supported yet) in `docs/post-task-script.md`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1026
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Interpolate job container.volumes in GetBindsAndMounts(), matching service container volumes and other container fields (image, options).
Fixes expressions like ${{ secrets.MAME }}:/path:ro being passed literally and rejected as invalid bind mounts
Reviewed-on: https://gitea.com/gitea/runner/pulls/1036
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Adds `ssh://` to the list of recognized URL schemes in `newRemoteAction`, so a
step can reference an action over SSH, e.g.:
```yaml
uses: ssh://git@gitea.example.com/actions/checkout@v4
```
Previously only `https://` / `http://` prefixes were parsed; an `ssh://` URL
fell through to the bare `org/repo` parser and failed.
### How auth works
SSH auth is delegated entirely to go-git's defaults — the runner configures no
SSH-specific options:
- **Which key?** go-git falls back to the host's **ssh-agent** (`$SSH_AUTH_SOCK`).
There is no key-file fallback, so the agent must hold a usable key. The SSH
**username** comes from the URL, so use `ssh://git@host/...` (a bare
`ssh://host/...` authenticates as an empty user and most servers reject it).
- **Host key trust?** Established out-of-band via the host's `known_hosts`
(`$SSH_KNOWN_HOSTS`, `~/.ssh/known_hosts`, `/etc/ssh/ssh_known_hosts`). The
runner host must already trust the remote; there is no accept-on-first-use.
- **Host key changes?** The clone fails with a host-key-mismatch error and stays
failed until `known_hosts` is updated on the host. Note `InsecureSkipTLS` does
**not** apply to SSH.
### Caching
The action cache path is derived from `{org}/{repo}` only (scheme/host are not
part of the key), so an `ssh://` action shares cache storage with the same
`org/repo` fetched over HTTP. This is unchanged by this PR and works in practice
(fetches resolve by SHA), but is worth noting.
### Tests
Adds `ssh://` cases to `Test_newRemoteAction` covering the scheme prefix, the
`git@` username placement, and a malformed-URL rejection. The agent/known_hosts
behavior lives in go-git and is not unit-tested here.
Fixes#841
Reviewed-on: https://gitea.com/gitea/runner/pulls/1035
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Cancelling a job on a Linux/macOS host runner can leave the spawned process
tree running and hang the runner — the same failure mode fixed for Windows in
#1011, just on the other platforms.
Steps are launched as process-group leaders (`Setpgid`, or `Setsid` for the PTY
path), but the default `exec.CommandContext` cancellation only kills the
**direct child**. When a step launches a shell that starts a child which in turn
spawns further background processes, cancelling the job leaves the descendants
running. Because those orphans inherited the step's stdout/stderr pipe, the read
end never hits EOF and `cmd.Wait()` blocks forever.
Because the step executor never returns:
- the orphaned processes keep running (the cancelled work is not actually
stopped), and
- end-of-job cleanup is never reached, so the runner appears to go offline / stop
picking up jobs.
## Fix
Apply the same tree-kill approach as Windows, using the Unix counterpart of a Job
Object: the **process group**.
- Add a Unix `processKiller` (`process_unix.go`) that captures the step's PGID
(== PID, since the step is launched as a group leader) and sends `SIGKILL` to
the whole group on cancellation. This also closes the inherited pipe handles so
`cmd.Wait()` can return. `ESRCH` (group already gone) is not treated as an error.
- Restrict the previous no-op stub (`process_other.go`) to `plan9` and have it
fall back to a single-process kill, preserving plan9's prior behaviour.
- Wire `cmd.Cancel` (tree kill) and `cmd.WaitDelay` (10s) **unconditionally** in
`exec()` instead of Windows-only. `WaitDelay` also covers a step that
backgrounds a process holding the pipe open after the main process exits.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1025
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
## Problem
Several runner code paths could drop the **tail** of a step's log output, so a
failing (or cancelled) step would show output that is missing its last line(s).
This was observed in practice and traced to four independent issues.
## Root causes & fixes
### 1. Trailing line without a newline was never flushed
`common.lineWriter` buffers output until it sees a `\n`. A final line **without**
a trailing newline (e.g. an error message printed right before a process exits,
a panic, `printf` without `\n`) stayed in the internal buffer and was never
emitted — the writer exposed no flush at all.
- Added `lineWriter.Flush()` (idempotent), a `Flusher` interface, and a
`FlushWriter(io.Writer)` helper.
- Flush at every stream EOF: the exec copy goroutine, the container `attach()`
streaming goroutine, and at step end (`useStepLogger`).
### 2. Cancellation/timeout truncated output
`waitForCommand` returned immediately on `ctx.Done()` and abandoned the
output-copy goroutine, losing output the command had already produced. It now
drains with a bounded grace period before returning. The response channel is
buffered so the goroutine can't leak if the drain times out.
### 3. `attach()` raced the final bytes
Container output was streamed in a fire-and-forget goroutine that `wait()` did
not synchronize with, so the step could proceed before the last bytes were
written. `wait()` now blocks on the streaming goroutine (bounded) so output is
fully drained and flushed first.
### 4. `::stop-commands::` silently dropped lines from the step log
Lines between `::stop-commands::<token>` and its end token were echoed without
the `raw_output` field **and** short-circuited the handler chain (`return false`),
so they never reached the step log (non-raw entries aren't appended while a step
is running). Now returns `true` so they are still captured.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1028
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Fixes#1023.
## Problem
In Windows host mode, a single stalled delete syscall (AV/EDR filter driver, unresponsive mount, dying disk) wedged the job forever at `Cleaning up container`. `HostEnvironment.Remove()` bounds every teardown phase (`terminateRunningProcesses`, both `removePathWithRetry` calls) except the `CleanUp` callback — an unbounded `os.RemoveAll(miscpath)` assigned in `startHostEnvironment`. The runner then held its capacity slot indefinitely, the task was reaped as a zombie, and there were no diagnostics.
## Fix
- **Bound the cleanup (availability):** `Remove()` now runs `CleanUp` under `hostCleanupTimeout` (30s) via `runWithTimeout`; on timeout it logs a warning and continues job completion. The stuck goroutine is left to finish (a delete syscall can't be interrupted). Added debug logs around the phase.
- **Reclaim the leak (disk hygiene):** a timed-out cleanup can leave a scratch dir behind, so the existing idle stale-dir sweep is extended to also remove orphaned host-mode scratch dirs (16-hex names) under `Host.WorkdirParent`, leaving the shared `tool_cache` and operator data untouched. The `bind_workdir` gate is dropped from `shouldRunIdleCleanup` so host-mode runners run the sweep.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1024
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Updates `golang.org/x/crypto` from `v0.50.0` to `v0.52.0` (and `golang.org/x/net` from `v0.53.0` to `v0.54.0` as a transitive bump).
## Why
`make security-check` (govulncheck) reported **7 vulnerabilities**, all in `golang.org/x/crypto/ssh` at `v0.50.0`, reachable through the git action cache fetch path (`act/runner/action_cache.go` → `git.Remote.FetchContext`):
| ID | Issue |
| --- | --- |
| GO-2026-5013 | Byte arithmetic underflow/panic in `ssh` |
| GO-2026-5015 | Server panic during `CheckHostKey`/`Authenticate` |
| GO-2026-5017 | Client can cause server deadlock on unexpected responses |
| GO-2026-5018 | Pathological RSA/DSA parameters may cause DoS |
| GO-2026-5019 | Bypass of FIDO/U2F physical interaction |
| GO-2026-5020 | Infinite loop on large channel writes |
| GO-2026-5021 | Auth bypass via unenforced `@revoked` status in `knownhosts` |
All are fixed in `v0.52.0`.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1027
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
Completes the runner side of the cancellation flow, superseding #825. Two parts:
### 1. Report cancellations correctly (`fix`)
When `Reporter.Close` ran with the state still `UNSPECIFIED` and the reporter's
context had been cancelled, the synthesised final state attributed the job to
`RESULT_FAILURE` with an "Early termination" log row — misreporting a
cancellation as a generic failure. `Close` now detects the cancelled context
and finalizes the task as `RESULT_CANCELLED`.
### 2. Advertise the `cancelling` capability (`feat`)
[actions-proto-go v0.6.0](https://gitea.com/gitea/actions-proto-go) adds a
`capabilities` field to `RegisterRequest`/`DeclareRequest`, so the runner can
now tell the server it understands the transitional cancelling state:
- Bumps `gitea.dev/actions-proto-go` to `v0.6.0`.
- Adds a single `RunnerCapabilities()` source of truth exposing
`CapabilityCancelling`.
- Sends `Capabilities` on both register and declare.
With this the server records `HasCancellingSupport` and can rely on the runner
running post-step cleanup before a task is finalized as `RESULT_CANCELLED`.
## Compatibility
Wire-compatible against older servers: the new field uses a previously unused
field number (8 on `RegisterRequest`, 3 on `DeclareRequest`) and the client uses
the binary protobuf codec, so a server predating the field silently ignores it —
registration and declaration succeed and the feature simply stays off. It
activates only once both runner and server are on v0.6.0.
## Server side
The matching Gitea change (read `GetCapabilities()`, persist
`HasCancellingSupport`) is a separate PR against `gitea/gitea`.
Supersedes #825.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1016
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Reviewed-by: wxiaoguang <29147+wxiaoguang@noreply.gitea.com>
## Background
`DOCKER_USERNAME` and `DOCKER_PASSWORD` are commonly used by workflows as ordinary secrets for logging in to a private registry and pushing images. However, the runner also treated these secret names as implicit Docker pull credentials.
These credentials carry no registry information, but they were attached to every pull unconditionally. As a result, a user who configured `DOCKER_USERNAME` / `DOCKER_PASSWORD` secrets for their private registry (e.g. to push images) would have those same credentials sent to Docker Hub when pulling a public image, causing the pull to fail with authentication failure.
## Changes
- Stop using `DOCKER_USERNAME` and `DOCKER_PASSWORD` as implicit pull credentials for job containers.
- Stop injecting `DOCKER_USERNAME` and `DOCKER_PASSWORD` as pull credentials for step containers.
## ⚠️ BREAKING ⚠️
This is a breaking change.
Workflows or runner setups that previously relied on `DOCKER_USERNAME` and `DOCKER_PASSWORD` being implicitly used for Docker image pulls must migrate to an explicit authentication mechanism.
Migration options:
- For private job container images, use `container.credentials`:
```yaml
jobs:
build:
container:
image: registry.example.com/image:tag
credentials:
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
```
- For private service container images, use service `credentials`.
- For private `uses: docker://...` or private Docker actions, configure Docker authentication in the runner environment before the job starts. For example, run `docker login` on the runner host.
`DOCKER_USERNAME` and `DOCKER_PASSWORD` can still be used as ordinary workflow secrets, for example with `docker/login-action` before pushing images.
---
Related:
- Fixes#386
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1007
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-committed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
- Add GitHub-style Actions **job summaries** support (writes to `GITHUB_STEP_SUMMARY` / `workflow/SUMMARY.md`) and render them in the run UI.
- Gitea stores summaries internally (DB) and serves them in the run view payload.
- `act_runner` uploads the summary **only when Gitea advertises support** (`X-Gitea-Actions-Capabilities: job-summary`), and warns on upload failures without failing the job.
## Compatibility
- New Gitea + old runner: no upload → no summary shown (no behavior change)
- New runner + old Gitea: capability not advertised → runner skips upload (no behavior change)
## Issue
- Fixesgo-gitea/gitea#23721
Reviewed-on: https://gitea.com/gitea/runner/pulls/917
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
## Background
Remote action cache directories can be keyed by the raw `uses` string. When Gitea's `DEFAULT_ACTIONS_URL` changes, the raw `uses` value may stay the same while the resolved clone URL changes.
In that case, an existing cached clone can still point to the old `origin` URL. Reusing it may fetch from the wrong remote with credentials for the new resolved URL, causing action clone failures until the user manually clears `~/.cache/act`.
## Changes
- Verify the cached clone's `origin` URL before reusing it in `CloneIfRequired`.
- Remove the cached clone and re-clone when the existing `origin` is different from the requested URL.
## Related
- Fixes#1010
Reviewed-on: https://gitea.com/gitea/runner/pulls/1014
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-committed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
## Problem
Cancelling a job on a Windows host runner can leave the spawned process
tree running and hang the runner. When a step launches a shell that
starts a child which in turn spawns further GUI/background processes,
cancelling the job kills only the direct child (the default
`exec.CommandContext` behaviour). The surviving descendants inherited
the step's stdout/stderr pipe, so the read end never hit EOF and
`cmd.Wait()` blocked forever.
Because the step executor never returned:
- the orphaned processes kept running (the cancelled work was not
actually stopped), and
- end-of-job cleanup (`Remove` → `terminateRunningProcesses`) was never
reached, so the runner appeared to go offline / stop picking up jobs.
`CREATE_NEW_PROCESS_GROUP` does not help here — it affects Ctrl-C signal
delivery, not handle inheritance or tree termination.
## Fix
- Assign each Windows step process to a **Job Object** immediately after
`cmd.Start()`. Descendants created afterwards are automatically part
of the job.
- Override `cmd.Cancel` to `TerminateJobObject`, so cancellation kills
the **entire descendant tree** atomically. This also closes the
inherited pipe handles, so `cmd.Wait()` can return.
- Set `cmd.WaitDelay` (10s) as a safety net: once the process has
exited, Wait force-closes the pipes and returns rather than blocking
forever — covering the case where the job-object setup fails (e.g.
nested-job restrictions), in which we fall back to the previous
single-process kill.
- The Job Object is created **without** `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`,
so closing the handle on normal completion does not kill legitimate
background processes; the tree is only torn down on explicit cancel.
Implemented behind `runtime.GOOS == "windows"` with a Windows-only
`processKiller` (Job Object) and no-op stubs elsewhere, so non-Windows
behaviour (default cancellation + `Setpgid`) is unchanged.
## Changes
- `act/container/process_windows.go` — Job Object `processKiller`
(create / assign / terminate).
- `act/container/process_other.go` — no-op stubs (`//go:build !windows`).
- `act/container/host_environment.go` — wire `cmd.Cancel` (tree kill)
and `cmd.WaitDelay` into `exec()`.
- `go.mod` / `go.sum` — promote `golang.org/x/sys` to a direct
dependency.
## Testing
I fully tested it already
## Notes
Follow-up to the Windows leftover-process reaping in #996: that sweep
now actually runs on cancellation because the step no longer hangs
before reaching it.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1011
Reviewed-by: techknowlogick <9+techknowlogick@noreply.gitea.com>
`containerReference.id` was cached from `Create()` and never re-validated, so a container torn down out-of-band (AutoRemove on an unexpected exit, daemon-side cleanup, sibling-job race in a parallel matrix) left a stale id behind. The next `Copy`/`Exec` then hit the daemon with that dead id and failed the otherwise-successful job with `Could not find the file /var/run/act/ in container <id>`.
`find()` now `ContainerInspect`s the cached id and clears it only on a definitive `NotFound`; transient errors trust the cache so cleanup pipelines don't abort on a daemon blip. Operations that need a live container (`copyContent`/`copyDir`/`CopyTarStream`/`exec`/`GetContainerArchive`) fail fast with a clear `container "<name>" does not exist` instead of the daemon's generic empty-id error.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1003
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-committed-by: silverwind <2021+silverwind@noreply.gitea.com>
`TestGetImagePullOptions` left docker/cli's process-global config dir pointed at `testdata/docker-pull-options` (which ships dummy `username:password` creds) via `config.SetDir`, without restoring it. Because that override is process-global, every later docker-gated test in the package then pulled with those creds — `TestDockerCopyToSymlinkPath`'s `alpine:latest` pull failed with `incorrect username or password` and broke CI. The workflow's `DOCKER_CONFIG` override can't mask this, since `SetDir` wins in-process.
Restore `config.Dir()` with `t.Cleanup`, and isolate the socket tests' leaks of the exported `CommonSocketLocations` and `DOCKER_HOST` behind an `isolateSocketEnv` helper.
Refs https://gitea.com/gitea/gitea.com/issues/83
---
This PR was written with the help of Claude Opus 4.8
Reviewed-on: https://gitea.com/gitea/runner/pulls/1004
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Running the full suite under `-race` (dropping `-short`) exposed pre-existing data races in parallel matrix-job execution, fixed by not sharing mutable state across combinations:
- `containerDaemonSocket()`/`validVolumes()` derive per-job values instead of mutating shared `Config`
- `getWorkflowSecrets` builds a fresh map, `rc.steps()` clones each step, and go-git workdir access is serialized
- every write to a shared `Job`'s result/outputs runs under a per-`Job` lock, each combo interpolating outputs from a pristine snapshot (last wins, as on GitHub)
### Test suite
- capability gates (docker / network / host-tools / Linux) replace the `-short` skips, and the suite runs offline via local fixtures (the artifact flow uses an in-process loopback server, only the docker-action force-pull needs the network)
- drops redundant tests, adds a regression test for https://gitea.com/gitea/runner/issues/981 and a docker-in-docker harness (`make test-dind`)
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/994
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
## Summary
- When Gitea cancels a job, the reporter cancels its own task context; the final Close() flush then aborted on that same cancelled context and Gitea never received the runner's acknowledgement (missing tail logs and final state).
- On Windows the cancelled context also neutralised terminateRunningProcesses, leaving step grandchildren alive in the workspace, holding file handles, so the runner could no longer clean up and pick up new work.
- Reporter.Close() now flushes on a detached, bounded context via a new rpcCtx() helper and configurable Runner.ReportCloseTimeout (default 10s).
- terminateRunningProcesses now PowerShell-enumerates Win32_Process and taskkill /T /F's every process whose ExecutablePath or CommandLine references the job's workspace directories, on a detached context.
- The daemon heartbeat loop still exits on <-r.ctx.Done(): the runner is intentionally seen as offline by Gitea during cleanup so it isn't handed a new task overlapping the in-progress teardown.
## Test plan
- [x] go test ./internal/pkg/report/... ./act/container/ -run 'TestReporter_ServerCancelStillFlushesFinal|TestBuildWindowsWorkspaceKillScript'
- [x] make fmt && make lint-go - 0 issues
- [x] GOOS=windows go build ./... - clean
- [x] Manual on a Windows runner: trigger a long-running workflow, cancel from Gitea UI; verify (a) the job ends with tail logs + cancelled state in Gitea, (b) workspace cleans up, (c) the runner picks up a new job without restart.
Authored-by: bircni
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/996
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
## Summary
- Non-raw_output log entries above the globally configured `log.level` are no longer forwarded to the Gitea job log output
- Step output (`raw_output=true`) is always forwarded regardless of level — it is actual job stdout/stderr, not runner internals
- State-machine fields (`stepResult`, `jobResult`) are always processed regardless of level, preserving correct tracking for skipped steps (whose `stepResult` is emitted at `DebugLevel` in `step.go`)
- Extracts a `shouldAppendLogRow` helper to avoid repeating the combined `!duringSteps() && entry.Level <= log.GetLevel()` guard in three places
## Why not the approach in #677
PR #677 adds `if entry.Level != log.GetLevel() { return nil }` at the top of `Fire()`. That has two bugs:
1. Uses `!=` instead of `>`, so `Error`/`Fatal` entries are dropped when the configured level is `Warn`
2. Returns early before processing `stepResult`/`jobResult` state fields — skipped steps (whose `stepResult` is logged at `DebugLevel`) would never be marked complete
This fix instead applies the level guard only at the `r.logRows` append sites, leaving state tracking unconditional.
Relates to #409.
Reviewed-on: https://gitea.com/gitea/runner/pulls/989
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Pin floating image tags:
- `golang` → `1.26-alpine3.23`
- `docker` dind variants → `29.5.2`
- `alpine` (basic stage + test fixture) → `3.23`
`ubuntu:24.04` and `scratch` left unchanged (no more-specific tag).
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/992
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
The `TestRunEvent*` integration tests are skipped in CI (`make test` runs `-short`), which hid several breakages that make them fail when run locally:
- `runTest` built the runner `Config` without `ContainerMaxLifetime`, so the job container ran `/bin/sleep 0` and exited immediately — every step failed with "container is not running". Set it to 1h.
- The root `.gitignore`'s unscoped `.env` and `dist` rules shadowed fixtures under `testdata/`. Anchored `dist` → `/dist` (the goreleaser output) and un-ignored `testdata/secrets/.env`.
- Added the missing `testdata/secrets/.env` fixture for `TestRunEventSecrets`.
- The `node24` local action referenced a `dist/index.js` bundle that was never committed (and was gitignored). Made the fixture self-contained (dependency-free ESM, `main: index.js`) so it runs without an `ncc` build. If you'd rather keep the `@actions/core`-based action and commit the built bundle instead, happy to switch.
Network-dependent subtests (remote `uses:`/composite actions) are out of scope.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/987
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
The teardown that removes a job's per-job network and container runs as a `Finally` on the step pipeline in `newJobExecutor`, which only executes after a successful start. When the start itself fails (e.g. a `docker cp` error from a buggy daemon), that `Finally` is skipped, so the network and container leak until Docker's address pool is exhausted and later jobs can no longer create networks.
This tears them down in `startContainer` when the start returns an error, reusing the existing `cleanUpJobContainer` teardown.
Exposed by the daemon regression in https://gitea.com/gitea/runner/issues/981, where every failed `docker cp` leaked a per-job network.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/986
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
The runner already enforces Gitea v1.21+ (see the `connect.CodeUnimplemented` check in `daemon.go`), so several shims kept for v1.20 compatibility have been dead since 2023:
- `compatibleWithOldEnvs` — the `GITEA_DEBUG`, `GITEA_TRACE`, `GITEA_RUNNER_CAPACITY`, `GITEA_RUNNER_FILE`, `GITEA_RUNNER_ENVIRON`, `GITEA_RUNNER_ENV_FILE` env vars (superseded by the config file)
- `VersionHeader` (`x-runner-version`) and the `version` param of `client.New`
- `AgentLabels` field in `RegisterRequest` (replaced by `Labels`)
Also replaces a verbose `strings.TrimRightFunc` closure with `strings.TrimRight(s, "\r\n")` in the log row parser.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/978
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Adds `org.opencontainers.image.source` and `org.opencontainers.image.version` labels to all three image variants (`basic`, `dind`, `dind-rootless`).
- `source` lets tools like renovate retrieve release notes from the source repo.
- `version` exposes the build version on the image itself.
Both `release-tag` and `release-nightly` workflows pass `VERSION` as a build arg so the label reflects the actual git tag (or `git describe` output for nightly).
---
This PR was written with the help of Claude Opus 4.7
---------
Reviewed-on: https://gitea.com/gitea/runner/pulls/975
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
My builds kept flaking out with errors like `invalid format delimiter 'ghadelimiter_...' not found before end of file` or just strange failures in the complete job. After some digging I found an issue in `parseEnvFile` and have tested this fix against the test case presented.
- `parseEnvFile` reads `$GITHUB_ENV` / `$GITHUB_OUTPUT` with a `bufio.Scanner` using the default 64 KiB token size, and never checks `s.Err()`.
- Any action that writes a multi-line value with a single line >64 KiB silently aborts the scan with `bufio.ErrTooLong`, which surfaces as the misleading `"invalid format delimiter
'ghadelimiter_…' not found before end of file"`.
- Real-world trigger: `docker/build-push-action`'s `metadata` output embeds the full `GITHUB_EVENT_PATH` payload via buildx provenance; a long PR description (e.g. a Renovate dependency
table) puts the body field on one JSON-escaped line well past 64 KiB.
- Raise the scanner buffer to 1 MiB so realistic outputs parse.
### Reproduction
Test this in an action. This removes the `docker/build-push-action` aspect and reproduces it directly.
```yaml
jobs:
repro:
runs-on: ubuntu-latest
steps:
- id: big
run: |
{
echo 'value<<EOF'
head -c 70000 /dev/urandom | base64 -w0
echo
echo 'EOF'
} >> "$GITHUB_OUTPUT"
```
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/974
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Jacob Alberty <jacob.alberty@gmail.com>
Co-committed-by: Jacob Alberty <jacob.alberty@gmail.com>
Removes code that whole-program reachability analysis (`deadcode` from `golang.org/x/tools`) confirmed unreachable, plus the `act/workflowpattern` package which no file outside its own directory imports.
- `act/common/draw.go` — CLI box-drawing helpers left over from nektos/act's dropped CLI
- `act/common/file.go` — `CopyFile`/`CopyDir` package-level helpers (container types have their own `CopyDir` methods, kept)
- `act/common/executor.go` — `Warning` type and `Warningf`. The `case Warning:` arm in `(Executor).Then`'s type switch was dead too (no code ever constructed a `Warning`); the switch is replaced with `if err != nil { return err }`
- `act/lookpath/env.go` — `LookPath` no-arg wrapper and `defaultEnv` struct. Only `LookPath2(file, env)` was used externally; the `Env` interface is kept
- `act/runner/action_cache_offline_mode.go` — `GoGitActionCacheOfflineMode` wrapper, never instantiated
- `act/workflowpattern/` — entire package, never imported
Net `-943` lines.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/971
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
It displayed an unused log and start an unused go routine. We should check the executors number before continue.
```
INFO[2026-05-12T21:01:04-07:00] Running job with maxParallel=1 for 1 matrix combinations
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 1 executors
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 0 executors
INFO[2026-05-12T21:01:04-07:00] NewParallelExecutor: Creating 1 workers for 0 executors
```
Reviewed-on: https://gitea.com/gitea/runner/pulls/960
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
Fixes#956.
Pseudo-TTY allocation is now an explicit, runner-wide opt-in via `runner.allocate_pty`, applied to both host and docker backends. Default is off, matching GitHub `actions/runner`.
```yaml
runner:
allocate_pty: false # default
```
**Before:** the host backend hardcoded `if true /* allocate Terminal */` and the docker backend used `term.IsTerminal(os.Stdout.Fd())`. As a result, `docker build` (and other TTY-aware tools) saw a TTY and emitted cursor-control redraw frames that flooded captured logs with thousands of duplicate-looking progress lines — only on host-mode runners in production, and on docker-mode runners when the daemon happened to be launched from a shell rather than a service.
**After:** both backends consult `Config.AllocatePTY`. The `term.IsTerminal` heuristic is gone, so behavior no longer depends on whether the daemon has a controlling terminal.
**Reproduction:** running `docker build` through `HostEnvironment.Exec` with output captured to a buffer:
| | Before (`if true`) | After (`AllocatePTY=false`) |
|---|---:|---:|
| bytes captured | 18,167 | 1,048 |
| ANSI CSI sequences | 556 | 0 |
| cursor-up `\e[1A` | 181 | 0 |
**Side fix:** `ptyWriter.AutoStop` is now `atomic.Bool`. The field is written from the exec goroutine after `cmd.Wait()` and read from the `copyPtyOutput` goroutine via `ptyWriter.Write`; existing tests never tripped the race detector because their commands produced no output before exit. The new host-mode test does.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/961
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-committed-by: silverwind <2021+silverwind@noreply.gitea.com>
Bumps `github.com/avast/retry-go` v4.7.0 -> v5.0.0, `golangci-lint` v2.11.4 -> v2.12.2 (aligns with gitea/gitea), and pins `govulncheck` to v1.3.0.
- `retry-go` v5 replaces the package-level `retry.Do(fn, opts...)` with a builder API `retry.New(opts...).Do(fn)`. The single call site in `internal/pkg/report/reporter.go` was migrated.
- `golangci-lint` v2.12.2 surfaces three new findings in `act/` (modernize/slicesbackward, govet/inline): one backward loop now uses `slices.Backward`, and the deprecated `reflect.Ptr` alias is replaced with `reflect.Pointer`.
- `go.mod`: the two direct-`require` blocks are merged into one, and a stray `gopkg.in/yaml.v3 // indirect` is moved into the indirect block. Purely cosmetic; `go.sum` is unchanged.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/965
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Fixes: https://gitea.com/gitea/runner/issues/859
Migration approach mirrors [actions-oss/act-cli#154](https://github.com/actions-oss/act-cli/pull/154).
### Dependency changes
- `github.com/docker/docker` v25.0.15 → **removed** (v29 doesn't exist as docker/docker; the project moved to moby/moby)
- `github.com/docker/cli` v25.0.7 → v29.4.3
- `github.com/docker/go-connections` v0.6.0 → v0.7.0
- `github.com/docker/docker-credential-helpers` v0.9.5 → v0.9.6
- `github.com/moby/go-archive` added at v0.2.0
- `github.com/moby/moby/api` added at v1.54.2
- `github.com/moby/moby/client` added at v0.4.1
- `github.com/moby/buildkit` removed (only used `dockerignore.ReadAll`, swapped for `moby/patternmatcher/ignorefile.ReadAll` directly)
- `github.com/containerd/errdefs` v0.3.0 → v1.0.0
### Migration
- v28: type aliases moved to their subpackages (`types.{Container,Image,Network,Exec}*` → `container/image/network/...`); deprecated APIs replaced (`ImageInspectWithRaw`, `client.IsErrNotFound`, `archive.CanonicalTarNameForPath`, `opts.ValidateMACAddress`, `ListOpts.GetAll`)
- v29: structural client redesign — every `cli.X(ctx, ...)` call switched to options-everywhere/Result-typed signatures, `ContainerExec*` → `Exec*`, `ContainerWait` returns a struct with `Result`/`Error` channels, `Tty`→`TTY`, `Copy*Container` takes options struct, `client.NewClientWithOpts` → `client.New`. `pkg/stdcopy` moved to `moby/moby/api/pkg/stdcopy`. The vendored copy of `cli/command/container/opts.go` was refreshed from cli v29 (now uses `netip.Addr` for IPs, port-set conversion helpers). A small local `parsePlatform` helper centralises the `os/arch[/variant]` parsing previously inlined into multiple call sites.
### Behaviour preservation
The migration introduced several behavioural shifts vs the v25 client; all were caught in review and reverted/fixed in follow-up commits:
- `GetDockerClient`: cli v29's `Ping(NegotiateAPIVersion: true)` returns errors that the old `NegotiateAPIVersion` silently swallowed. Restored best-effort behaviour (warn-log + continue) so daemons with blocked `_ping` or API < 1.40 keep working. The SSH-helper `client.New` call no longer inherits `client.FromEnv`, matching the old `NewClientWithOpts(WithHost, WithDialContext)` so `DOCKER_API_VERSION`/`DOCKER_TLS_VERIFY` don't leak into the SSH-tunneled client
- `parsePlatform`: malformed input now returns an explicit error instead of silently dropping to "no platform constraint" and pulling the host-default architecture. Single-segment (`"linux"`), 4+-segment (`"linux/arm/v7/extra"`), and trailing-slash (`"linux/arm/"`) inputs are all rejected
- `LoadDockerAuthConfig`/`LoadDockerAuthConfigs`: `config.LoadDefaultConfigFile(nil)` panics on a malformed config file (it does `fmt.Fprintln` on the nil `io.Writer`). Switched to `config.Load(config.Dir())` so load errors reach the logger and the panic path is gone. Restored the old behaviour of returning `config.Load` and `GetAuthConfig` errors to the caller (the v29 refactor had silently downgraded them to warn-only). A `reference.ParseNormalizedNamed` failure on the image string falls through to the `docker.io` default rather than aborting, since the old string-based hostname extraction was infallible
Test assertions also updated for two upstream error-message string shifts (`go-connections` port-range parser; `cli/opts` envfile BOM check). Added unit-test coverage for the new `parsePlatform` helper, locking in the intentional limits (single-segment, 4+-segment, and trailing-slash platforms rejected).
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/943
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Fixes#957.
## Why
The runner builds a custom `http.Transport` for its RPC client. From first principles, once we stop using the default transport we also stop inheriting its default proxy resolution behavior, so `HTTP_PROXY`/`HTTPS_PROXY` are ignored unless we wire that behavior back explicitly.
## What
- set `Proxy: http.ProxyFromEnvironment` on the custom transport
- add a regression test that verifies `getHTTPClient` honors proxy environment variables
Reviewed-on: https://gitea.com/gitea/runner/pulls/962
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
## Summary
- clone the runner environment map for each task before injecting runtime and OIDC tokens
- keep the shared base environment immutable so concurrent jobs cannot hit `concurrent map writes`
- add a unit test covering task-local env cloning
Fixes#958
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/959
Reviewed-by: Nicolas <bircni@icloud.com>
At v1.0.0 the `github.com/imdario/mergo` module was relocated to `dario.cat/mergo`, so a plain version bump (as in https://gitea.com/gitea/runner/pulls/951) leaves the import path pointing at the old, unmaintained location. This PR updates the import in `act/container/docker_run.go` and adjusts `go.mod` accordingly. The public API (`mergo.Merge`, `mergo.WithOverride`, `mergo.WithAppendSlice`) is unchanged.
Supersedes https://gitea.com/gitea/runner/pulls/951.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/954
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Fixes#950.
After #819, the daemon flushes logs eagerly on the job-result entry (via the `stateNotify` path), so `Close()` typically runs `ReportLog(true)` with an empty buffer. Gitea's `UpdateLog` handler short-circuits on `len(Rows)==0` before honoring `NoMore`, so the final request never runs `TransferLogs` and `dbfs_data` rows leak. The server-side short-circuit is latent since the original Actions implementation in 2023; #819 made it deterministically reachable.
Workaround: inject a sentinel row in `Close()` after the daemon has exited so the final `UpdateLog` always carries at least one row. Done after the daemon waits so the sentinel can't be flushed before `ReportLog(true)` reads it.
https://github.com/go-gitea/gitea/pull/37631 drops the empty-rows short-circuit when `NoMore=true`; that would work with or without this PR.
Reviewed-on: https://gitea.com/gitea/runner/pulls/952
Reviewed-by: Nicolas <bircni@icloud.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
## Summary
`artifactcache.Handler` was lowercasing cache keys before storing and returning them. This caused actions like `actions/setup-go` to treat every restore as a partial hit and re-upload the cache on every job run.
Similar issue: [act#2497](https://github.com/nektos/act/issues/2497)
## Root Cause
These actions build cache keys that include `RUNNER_OS` (e.g. `setup-go-Linux-x64-...` See [setup-go/cache-restore.ts](78961f6f84/src/cache-restore.ts (L11-L51)) ). In `gitea/runner`, `RUNNER_OS` is always `Linux` by default (See https://gitea.com/gitea/runner/search?q=RUNNER_OS).
These actions decide whether to save the cache data in their post step using **strict** `===` comparison between the primary key and the key returned from the runner. See [setup-go cache-save.ts](78961f6f84/src/cache-save.ts (L44-L86)) .
|State | Value|
|--- | ---|
|CachePrimaryKey | `setup-go-Linux-x64-ubuntu-22.04-go-1.24.9-abc123` |
|CacheMatchedKey | `setup-go-linux-x64-ubuntu-22.04-go-1.24.9-abc123` |
Because the runner's cache server lowercased the stored key, the response carried `setup-go-linux-...` while the action's primary key was `setup-go-Linux-...`. Strict equality failed, then the actions updated same data again. This repeated on every run, wasting disk and bandwidth. The duplicate blobs accumulate until GC .
https://gitea.com/gitea/runner/actions/runs/462560/jobs/737401#jobstep-2-15

https://gitea.com/gitea/runner/actions/runs/462560/jobs/737401#jobstep-6-22

## Fix
Drop the `strings.ToLower` calls in `find` and `reserve` so the original key case is preserved end-to-end. This fix will invalidate existing "case insensitive" keys.
## Notes
The [original act review](https://github.com/nektos/act/pull/1770/changes/BASE..d44b8d15649d9d09d1d891130b8f3962097a81f3#r1177624608) suggested making cache keys case-insensitive because `isExactKeyMatch` compares cache key ignoring case. However, the actions (`setup-go` / `setup-node` / `setup-ruby`) compare with strict `===` rather than `isExactKeyMatch`.
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/947
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
## Summary
- `CopyCollector.WriteFile` now removes any existing destination file
before writing, handling read-only modes (e.g. git pack files at
`0444`) that cause `EACCES`/`ERROR_ACCESS_DENIED` on macOS and Windows.
- Added `O_TRUNC` to the `OpenFile` flags as a safety net.
## Root cause
When a composite action with a post step runs on a host runner,
`runPostStep` calls `maybeCopyToActionDir`, which re-copies the action
into `miscpath/act/actions/<name>/`. The first copy (main step) writes
`.git/objects/pack/*.idx` at the destination with mode `0444` (as set
by go-git). The second copy (post step) calls
`os.OpenFile(dest, O_CREATE|O_WRONLY, …)` on that existing `0444` file,
which fails immediately:
- macOS: `open <path>: permission denied`
- Windows: `open <path>: Access is denied`
Fixes: https://gitea.com/gitea/runner/issues/941
Fixes: https://gitea.com/gitea/runner/issues/876
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/942
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-committed-by: Nicolas <bircni@icloud.com>
`NewGitCloneExecutor` holds a per-directory mutex while it `git checkout --force`s a remote action into the shared `<ActionCacheDir>/<UsesHash>`, but four read sites ran unlocked:
- `maybeCopyToActionDir`'s tar walk via `JobContainer.CopyDir`
- `prepareActionExecutor`'s `readAction` parse of `action.yml`
- `newReusableWorkflowExecutor`'s `model.NewWorkflowPlanner` after `cloneRemoteReusableWorkflow` released its lock
- `execAsDocker` when `ActionCache == nil`: `docker build` walks `contextDir` for the daemon-side build context
When two matrix jobs share a `uses:`, a read interleaved with a peer's checkout produces partial state — observed as `Cannot find module .../dist/index.js` and `setup-uv` failing on a half-written `action.yml`.
Exports `acquireCloneLock` as `AcquireCloneLock` and takes it at all four sites. `container.ImageExistsLocally` / `NewDockerBuildExecutor` and `model.NewWorkflowPlanner` are indirected through package-level vars so the docker-action build path and the reusable-workflow read site are testable without a real daemon, mirroring `ContainerNewContainer`. Three regression tests cover the higher-risk sites (`maybeCopyToActionDir`, `execAsDocker`, `newReusableWorkflowExecutor`); each fails if its `AcquireCloneLock` is removed.
Subsumed by https://gitea.com/gitea/runner/pulls/814 once that lands. Related: https://gitea.com/gitea/runner/pulls/930
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/938
Reviewed-by: Nicolas <bircni@icloud.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Aligns runner log output more closely with `actions/runner`:
- Strip the whale, rocket, cloud, construction, chequered-flag, and exclamation-mark glyphs from log lines and drop the now-unused `logPrefix` constant.
- Reword `no outputs used step '%s'` → `No outputs registered for step '%s'` (the original was ungrammatical and inaccurate — it fires when `set-output` references an unknown step ID).
- Wrap the docker pull/network/create/start phase of job container startup in a `::group::Starting job container` / `::endgroup::` collapsible section, mirroring `actions/runner`. Since act drives Docker through the SDK rather than the CLI, we can't echo `##[command]/usr/bin/docker create ...` lines verbatim — instead the helper emits a summary inside the group:
```
::group::Starting job container
image: <image>
name: <container-name>
network: <network-name>
::endgroup::
```
- Extracted the emit into a `printStartJobContainerGroup` helper (parallel to `printRunActionHeader` in `step_run.go`) and added a golden-style test `TestPrintStartJobContainerGroupGolden`.
- Drive-by: replace two remaining literal `"raw_output"` strings in `run_context.go` with the existing `rawOutputField` constant.
Closes#935
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/940
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Audit-driven cleanup of `act/` test fixtures. Three commits:
**1. Remove dead fixtures** — 12 fixture directories that no Go test references: `dir with spaces`, `environment-variables`, `issue-104`, `issue-122`, `issue-141`, `localdockerimagetest_`, `node`, `parallel`, `python`, `uses-composite-with-inputs`, `uses-composite-with-pre-and-post-steps`, `shells/custom` (under `act/runner/testdata/`), plus `act/artifactcache/testdata/example`.
**2. Collapse `actions/node{12,16,20}` to a single `actions/node24` fixture** — the trio dispatched through identical `IsNode()` code paths and exercised the container's node binary, not the `using:` string. Bumps bundled deps to current (`@actions/core@^3`, `@actions/github@^9`, `@vercel/ncc@^0.38.4`) — both runtime packages are now ESM-only, so `index.js` is rewritten to ESM and `"type": "module"` added. Drops committed `node_modules/` and `package-lock.json` (now gitignored locally; `dist/` continues to be ignored by the repo-root `.gitignore` as before). Reduces `local-action-js/push.yml` to a single `test-node24` job and bumps four other stale `using: node12/16` references in fixtures.
**3. Bump test container base images** to `node:24-bookworm-slim` / `node:24-bookworm` / `ubuntu:24.04`. Replaces `node:16-buster-slim`, `node:16-buster`, `node:12.20.1-buster-slim`, and the EOL `node:12-buster-slim` / `node:16-buster-slim` / `ubuntu:18.04` base images in `actions/{docker-local,docker-local-noargs,action1}/Dockerfile`.
The runner's model still accepts `using: node12/16/20` for third-party actions in the wild — those constants are untouched.
Fixes: https://gitea.com/gitea/runner/issues/931
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/932
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
`cloneIfRequired` only ran the underlying clone executor when the target directory was missing, so a reusable workflow referenced by a moving ref (`uses: org/repo/.gitea/workflows/wf.yml@master`) was cached forever after the first invocation — edits to the source file never propagated.
Always invoke `git.NewGitCloneExecutor`. It handles existing repositories via fetch + pull + hard-reset, so branch and tag refs are brought up to date on each run, matching GitHub Actions semantics.
Drops the global `executorLock` too: `NewGitCloneExecutor` already takes a per-directory lock via `acquireCloneLock`, so the outer mutex only added unnecessary serialization across unrelated reusable-workflow clones — worse now that every invocation runs the full fetch.
Includes a regression test that drives the wrapper against a local bare repo, pushes a new commit on `master` between two invocations, and asserts the cached workflow file reflects the new tip.
Fixes: https://github.com/go-gitea/gitea/issues/37483
Fixes: https://gitea.com/gitea/runner/issues/726
Related: https://github.com/go-gitea/gitea/issues/30543
Would be subsumed by https://gitea.com/gitea/runner/pulls/814 ("WIP: Introduce new action cache") once that lands.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/930
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| docker | stage | major | `28-dind-rootless` → `29-dind-rootless` |
| docker | stage | major | `28-dind` → `29-dind` |
---
> ⚠️ **Warning**
>
> Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/856) for more information.
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNiIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC42IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/924
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
Fixes#926.
Before:
<img src="/attachments/a5ae9221-eee2-410a-964e-6103ce126df4" alt="image.png" width="400">
After:
<img width="400" alt="image.png" src="attachments/2f2d67c4-6080-4ec3-9ae5-df33e6479920">
Also gets rid of a bunch of emojis in the logging and the obsolete link to `nektos/act` and align some other error messages.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/927
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
- Add idle-time cleanup for stale bind-workdir task directories instead of cleaning them on the task execution path.
- Make cleanup behavior configurable with `runner.startup_cleanup_age` as the stale-age threshold (default: `24h`) and `runner.idle_cleanup_interval` as the idle cleanup cadence (default: `10m`).
- Restrict cleanup scope to numeric task directory names only, to avoid touching operator-managed folders.
- Document the cleanup settings in `config.example.yaml` and `README.md`.
- Add tests for stale-directory cleanup, idle cleanup throttling, and config default/override parsing.
## Why
When a runner or host crashes, normal per-task cleanup may not run, leaving stale task directories under the bind-workdir root. Running this cleanup only while the runner is idle recovers that disk space without adding overhead to active job execution.
If you want, I can also tighten the wording around `startup_cleanup_age`, since the key name now reads a bit misleadingly relative to the actual behavior.
---------
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/870
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
## Summary
- Fix host-mode cleanup to remove the job **workspace** directory after a run (instead of leaving checkouts behind).
- On Windows, track step process PIDs and terminate remaining process trees during teardown before attempting workspace deletion (prevents file-lock failures).
- Skip workspace deletion when `bind_workdir` is enabled to avoid conflicting with runner-level task directory cleanup.
## Implementation details
- `HostEnvironment` now records PIDs for started commands and best-effort terminates them on Windows during `Remove()`.
- Workspace removal uses a small retry loop on Windows to handle transient locks.
- `BindWorkdir` is propagated into `HostEnvironment` so cleanup behavior matches runner configuration.
---------
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/883
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [actions/hello-world-docker-action](https://github.com/actions/hello-world-docker-action) | action | major | `v1` → `v2` |
---
### Release Notes
<details>
<summary>actions/hello-world-docker-action (actions/hello-world-docker-action)</summary>
### [`v2`](https://github.com/actions/hello-world-docker-action/releases/tag/v2): Version v2
[Compare Source](https://github.com/actions/hello-world-docker-action/compare/v1...v2)
Update action to use the new environment file method for setting outputs.
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
---
This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Reviewed-on: https://gitea.com/gitea/runner/pulls/921
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Renovate Bot <renovate-bot@gitea.com>
Co-committed-by: Renovate Bot <renovate-bot@gitea.com>
## Summary
- Lower the default `fetch_interval_max` from 60s to 5s to reduce job pickup latency for common single-runner deployments
- PR #819 optimized defaults for 200-runner fleets, regressing single-runner pickup time from ~2s to ~65s
- Most deployments use few runners; large fleets can still tune this value higher in their config
## Impact
| `fetch_interval_max` | 1 runner pickup | 200 runners idle |
| -------------------- | --------------- | ---------------- |
| 60s (previous) | up to **65s** | 3.3 req/s |
| **5s (new default)** | up to **5s** | 40 req/s |
Closes https://gitea.com/gitea/act_runner/issues/869
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/875
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Removes 88 `nolint` directives (386 → 298) via mechanical, zero-regression cleanups:
- **38 `bodyclose`** in `act/artifactcache/handler_test.go`: replaced by `defer resp.Body.Close()` after each HTTP call.
- **21 dead directives** (`gocyclo`, `dogsled`, `contextcheck`): none of these linters are enabled in `.golangci.yml`, so the directives were doing nothing.
- **29 `testifylint`** directives whose underlying issues were addressed by mechanical rewrites:
- `assert.Nil(t, err)` → `assert.NoError(t, err)`
- `assert.NotNil(t, err)` → `assert.Error(t, err)`
- `assert.Equal(t, true/false, x)` → `assert.True/False(t, x)`
- `assert.Equal(t, 0, len(x))` → `assert.Empty(t, x)`
- `assert.Equal(t, N, len(x))` → `assert.Len(t, x, N)`
- `assert.Len(t, x, 0)` → `assert.Empty(t, x)`
Many `testifylint` directives still apply because they flag `require-error` (i.e. testifylint wants `require.NoError` instead of `assert.NoError` for early bail-out). That's a behavior change (fail-fast vs continue) and out of scope for this purely mechanical cleanup — those can be addressed in a follow-up. Same for `expected-actual`, `equal-values`, `error-is-as`, and the remaining `nilnil` / `unparam` / `forbidigo` / `staticcheck` / `goheader` / `dupl` directives.
`golangci-lint run` is clean. Tests pass for all touched packages.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/864
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-committed-by: silverwind <2021+silverwind@noreply.gitea.com>
Old `cloneLock` is a package-level `sync.Mutex` that serialized every action clone across all goroutines, regardless of target directory.
This PR replaces it with a `sync.Map` of per-directory mutexes keyed by `input.Dir`. Same-directory operations still serialize; different directories now clone in parallel.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/866
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
## Bug fixes
- Fixed "file name too long" errors when running matrix jobs with many or
long-valued matrix parameters. Docker volume and container names are now
bounded to a safe length by hashing the full name with SHA-256, keeping
the result well within the filesystem `NAME_MAX` limit (255 bytes) even
after Docker appends its own suffixes (`-env`, `-network`, etc.).
## Upgrade notes
This change renames the Docker containers, volumes and networks created by
the runner. Resources created by a previous version will **not** be cleaned
up automatically after upgrade and will become orphans.
After upgrading, you can remove the legacy resources with:
```sh
# volumes
docker volume ls -q | grep -E '^GITEA-ACTIONS-TASK-[0-9]+_' | xargs -r docker volume rm
# networks
docker network ls --format '{{.Name}}' | grep -E '^GITEA-ACTIONS-TASK-[0-9]+_.*-network$' | xargs -r docker network rm
```
> **Note:** If multiple act_runner instances share the same Docker daemon,
> make sure no runner using the old version is running before executing the
> cleanup commands above.
Fixes#686
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/853
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Morgan Peyre <195218+peyremorgan@noreply.gitea.com>
Co-committed-by: Morgan Peyre <195218+peyremorgan@noreply.gitea.com>
Closes#848. Addresses [GHSA-82g9-637c-2fx2](https://github.com/go-gitea/gitea/security/advisories/GHSA-82g9-637c-2fx2) and the follow-up points raised by @ChristopherHX and @haroutp in that thread.
The change is breaking only for `cache.external_server` which uses auth via a pre-shared secret.
## How auth works now
1. **Runner starts** → opens the embedded cache server on `:port`. Loads / creates a 32-byte HMAC signing key in `<cache-dir>/.secret`.
2. **Runner receives a task** → calls `handler.RegisterJob(ACTIONS_RUNTIME_TOKEN, repository)` before the job runs, defers a revoker that removes the credential on completion. Registrations are reference-counted so a stray re-register cannot revoke a live job.
3. **Job container runs `actions/cache`** → the toolkit sends `Authorization: Bearer $ACTIONS_RUNTIME_TOKEN` on every management call (`reserve`, `upload`, `commit`, `find`, `clean`). The cache server's middleware looks the token up in the registered-jobs map: miss → 401; hit → the job's repository is injected into the request context.
4. **Repository scoping** — every cache entry is stamped with `Repo` on reserve; `find`, `upload`, `commit` all verify the caller's repo matches. A job in repo A cannot see or poison a cache entry owned by repo B, even when both reach the server over the same docker bridge. GC dedup also groups by `(Repo, Key, Version)` so one repo can't age out another.
5. **Archive downloads** — `@actions/cache` does not attach Authorization when downloading `archiveLocation`, so the `find` response is a short-lived HMAC-signed URL: `…/artifacts/:id?exp=<unix>&sig=<hmac>`, 10-minute TTL, signature binds `cacheID:exp`. Tampered, expired, or foreign-secret URLs get 401.
6. **Defence-in-depth** — `ACTIONS_RUNTIME_TOKEN` is added to `task.Secrets` so the runner's log masker scrubs it from step output.
## `cache.external_server` (standalone `act_runner cache-server`)
Operators set `cache.external_secret` to the same value on the runner config and the `act_runner cache-server` config. The `cache-server` then runs with bearer auth on the cache API and exposes a control-plane at `POST /_internal/{register,revoke}` (gated by the shared secret). The runner pre-registers each task's `ACTIONS_RUNTIME_TOKEN` with the remote server before the job runs and revokes it on completion. Same per-job auth + repo scoping as the embedded handler, just over the network.
`cache-server` refuses to start without `cache.external_secret`; runner config load also fails when `cache.external_server` is set without `cache.external_secret`.
## User-facing changes
- **One-time cache miss after upgrade.** Pre-existing entries in `bolt.db` have no `Repo` stamp and won't match any job — they'll be evicted by the normal GC. First job per cache key rebuilds its cache.
- **`cache.external_server` deployments must add `cache.external_secret`.** Breaking change for anyone running a standalone `act_runner cache-server`: set the same `cache.external_secret` in both the runner config and the cache-server config. Without it neither side starts.
- **No config changes required for the default setup.** Runners using the embedded cache server (the common case) keep working without any yaml edits; the auth mechanism is invisible to workflows.
---
This PR was written with the help of Claude Opus 4.7
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/849
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
## Summary
Mirrors the GitHub Actions runner behaviour where each `run:` step shows a collapsible **"Run \<command\>"** section containing the script, shell command, and environment variables before the actual step output.
### What changes
- **`pkg/runner/step_run.go`**: In `stepRun.main()`, two new executors are added to the pipeline:
1. `logRunGroupHeader()` — runs after `setupShellCommandExecutor()` (so `sr.cmdline` is already resolved). Emits a `::group::Run <step>` log entry followed by the interpolated script, the full shell command line, and the step's env vars (sorted, internal vars filtered out).
2. The existing execution function now has `defer rawLogger.Infof("::endgroup::")` so the group is closed after the step finishes, regardless of success or failure.
### Env var filtering
Internal runner vars are hidden (`GITHUB_*`, `GITEA_*`, `RUNNER_*`, `INPUT_*`, `PATH`, `HOME`) — only user-relevant vars are shown, matching what GitHub Actions displays.
### Example output
```
▼ Run cargo build
cargo build
shell: bash --noprofile --norc -e -o pipefail {0}
env:
CARGO_HOME: /home/runner/.cargo
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
<actual build output>
```
---------
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/847
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
## Problem
In host executor mode, uses: docker://<image> step actions fail because
act/runner/step_docker.go always attaches the step container to the job
container's network namespace, which doesn't exist in host mode.
### Example
Run following job in host runner
```yaml
jobs:
test:
runs-on: ubuntu-latest-host
steps:
- uses: docker://alpine:3.20
with:
args: echo hello
```
```
Error:
failed to start container: Error response from daemon:
joining network namespace of container:
No such container: xxxxxx
```
This pr allows the docker step in the host mode
## Testing
I tested following steps on host runner and it worked
```yaml
- name: Test azure cli action in host mode
uses: azure/cli@v2
env:
RUNNER_OS: Linux
with:
inlineScript: echo "hello from azure cli"
- uses: docker://alpine:3.20
with:
args: echo hello
```
---------
Co-authored-by: Nicolas <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/857
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: Mirko Sekulic <misha.sekulic@gmail.com>
Co-committed-by: Mirko Sekulic <misha.sekulic@gmail.com>
The `replace github.com/distribution/reference v0.6.0 => v0.5.0` was added defensively in [c4b57fbc](c4b57fbcb2) (#775) against `github.com/docker/distribution v2.8.3`, whose `reference_deprecated.go` calls the now-removed `reference.SplitHostname`. However, nothing in act_runner's build graph imports `github.com/docker/distribution/reference`, so the file is never compiled and the replace has no effect.
Verified locally: removing the directive followed by `go mod tidy` keeps `distribution/reference` at v0.6.0, drops `docker/distribution` from the graph entirely, and `go build ./...` / `go vet ./...` both pass.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/858
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
`s6-svscan` starts services in parallel, so `act_runner/run` could invoke `s6-svwait` before s6 had created the docker service's `supervise/` directory, failing with `s6-svwait: fatal: unable to s6_svstatus_read: No such file or directory`. Poll for the directory before waiting.
Fixes#760
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/851
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
Fixes#826.
Regressed in f2d54556 (#819, "perf: reduce runner-to-server connection load with adaptive reporting and polling"). That change added an early-return in `ReportState` whenever there was no state change and no pending outputs, so jobs that produce no log output and no step transitions for many minutes (e.g. a Linux kernel build) stop heartbeating. The server eventually marks the task as orphaned and cancels it while the runner is still executing.
The fix tracks the last successful `UpdateTask` time in an atomic and keeps the no-op skip only while the previous report is younger than `stateReportInterval`. The periodic state ticker fires at exactly `stateReportInterval`, so silent jobs now heartbeat each tick; redundant sends from a `stateNotify` firing right after a tick are still suppressed, preserving the perf intent of #819.
Test added: `TestReporter_StateHeartbeat` asserts the skip path within the interval and the heartbeat path after the interval elapses.
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/runner/pulls/852
Reviewed-by: Nicolas <bircni@icloud.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
- Fix `GXZ_PAGAGE` typo to `GXZ_PACKAGE`.
- Move `gxz`/`xgo` tool installs from `deps-backend` to `deps-tools`
and add `golangci-lint` there; `deps-backend` is just `go mod download`.
- Use `--color=always` + `printf` in `tidy-check` to match `fmt-check`.
- Use STATIC-gated `EXTLDFLAGS` instead of a uname-based toggle, and
move `-s -w` out of `EXTLDFLAGS` to match gitea's `-ldflags` layout.
- Pass `-s -w -linkmode external -extldflags "-static"` explicitly for
release-linux / release-windows; add `-s -w` to release-darwin.
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Merges the `gitea.com/gitea/act` fork into this repository as the `act/`
directory and consumes it as a local package. The `replace github.com/nektos/act
=> gitea.com/gitea/act` directive is removed; act's dependencies are merged
into the root `go.mod`.
- Imports rewritten: `github.com/nektos/act/pkg/...` → `gitea.com/gitea/act_runner/act/...`
(flattened — `pkg/` boundary dropped to match the layout forgejo-runner adopted).
- Dropped act's CLI (`cmd/`, `main.go`) and all upstream project files; kept
the library tree + `LICENSE`.
- Added `// Copyright <year> The Gitea Authors ...` / `// Copyright <year> nektos`
headers to 104 `.go` files.
- Pre-existing act lint violations annotated inline with
`//nolint:<linter> // pre-existing issue from nektos/act`.
`.golangci.yml` is unchanged vs `main`.
- Makefile test target: `-race -short` (matches forgejo-runner).
- Pre-existing integration test failures fixed: race in parallel executor
(atomic counters); TestSetupEnv / command_test / expression_test /
run_context_test updated to match gitea fork runtime; TestJobExecutor and
TestActionCache gated on `testing.Short()`.
Full `gitea/act` commit history is reachable via the second parent.
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
# Matrix Strategy: Support for Scalar Values and Template Expressions
## 🎯 Overview
This Pull Request implements support for **scalar values and template expressions** in matrix strategies. Previously, every matrix value had to be declared as a YAML array — a bare scalar like `os: ubuntu-latest` or `version: ${{ fromJSON(...) }}` caused silent failures. Now, scalars are automatically wrapped into single-element arrays, enabling the `${{ fromJSON(...) }}` pattern that is commonly used in GitHub Actions workflows for dynamic matrix generation.
## ⚠️ Semantic Changes
Two subtle behavior changes are introduced that are worth understanding:
### 1. Scalar matrix values now produce a 1-element matrix expansion
**Before this PR:** A scalar value like
```yaml
strategy:
matrix:
os: ubuntu-latest # no brackets
```
failed to decode as `map[string][]interface{}`, so `Matrix()` returned `nil`. The job ran **once, without any matrix context** (`matrix.os` was undefined).
**After this PR:** The same input is wrapped to `["ubuntu-latest"]`. The job runs **one matrix iteration** with `matrix.os = "ubuntu-latest"` set.
> **Impact on existing workflows:** Workflows that accidentally used bare scalars instead of arrays will now run with a matrix context where they previously did not. The functional result is usually the same, but `matrix.*` variables are now populated. This is considered an improvement, but is a semantic change.
---
### 2. Unevaluated template expressions fall back to a literal string value
The actual resolution of `${{ fromJSON(needs.job1.outputs.versions) }}` into an array happens **before** `Matrix()` is called — in `EvaluateYamlNode` inside `pkg/runner/runner.go`. This PR does not change that evaluation path.
If evaluation succeeds → the YAML node already contains a proper array → `Matrix()` decodes it normally.
If evaluation **fails or is skipped** (e.g., expression not yet resolved) → the scalar string is now wrapped as `["${{ fromJSON(...) }}"]` → the job runs **one iteration with the literal unexpanded string** as the matrix value.
> **Note:** The test `TestJobMatrix/matrix_with_template_expression` covers this fallback path, not the successful resolution path. The literal-wrap behavior is the fallback, not the primary mechanism for template expression support.
---
## 🔑 Key Changes
### 1. Flexible Matrix Decoding (`pkg/model/workflow.go`)
#### New Helper Function: `normalizeMatrixValue`
```go
func normalizeMatrixValue(key string, val interface{}) ([]interface{}, error)
```
- Converts a single matrix value to the expected array format
- Handles three cases:
1. **Arrays**: Pass through unchanged
2. **Scalars**: Wrap in single-element array (including template expressions)
3. **Invalid types** (maps, etc.): Return error to detect misconfigurations early
**Supported scalar types:**
- `string` — including unevaluated template expressions
- `int`, `float64` — numeric values
- `bool` — boolean values
- `nil` — null values
#### Enhanced `Matrix()` Method
**Before:**
```go
func (j *Job) Matrix() map[string][]interface{} {
if j.Strategy.RawMatrix.Kind == yaml.MappingNode {
var val map[string][]interface{}
if !decodeNode(j.Strategy.RawMatrix, &val) {
return nil
}
return val
}
return nil
}
```
**After:**
```go
func (j *Job) Matrix() map[string][]interface{} {
if j.Strategy == nil || j.Strategy.RawMatrix.Kind != yaml.MappingNode {
return nil
}
// First decode to flexible map[string]interface{} to handle scalars and arrays
var flexVal map[string]interface{}
err := j.Strategy.RawMatrix.Decode(&flexVal)
if err != nil {
// If that fails, try the strict array format
var val map[string][]interface{}
if !decodeNode(j.Strategy.RawMatrix, &val) {
return nil
}
return val
}
// Convert flexible format to expected format with validation
val := make(map[string][]interface{})
for k, v := range flexVal {
normalized, err := normalizeMatrixValue(k, v)
if err != nil {
log.Errorf("matrix validation error: %v", err)
return nil
}
val[k] = normalized
}
return val
}
```
**Key improvements:**
- Handles unevaluated template expressions as scalar strings
- Automatic conversion to array format for consistent processing
- Validation to catch nested maps and invalid configurations
- Fallback to strict array format decoding for backward compatibility
- Early nil check for strategy to prevent nil pointer panics
---
## 🎁 Use Cases
### Example 1: Template Expression in Matrix (primary motivation)
```yaml
jobs:
setup:
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.version-map.outputs.versions }}
steps:
- id: version-map
run: echo "versions=[\"1.17\",\"1.18\",\"1.19\"]" >> $GITHUB_OUTPUT
test:
needs: setup
strategy:
matrix:
# EvaluateYamlNode resolves this to an array before Matrix() is called
version: ${{ fromJSON(needs.setup.outputs.versions) }}
runs-on: ubuntu-latest
steps:
- run: echo "Testing version ${{ matrix.version }}"
```
### Example 2: Mixed Scalar and Array Values
```yaml
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest] # Array — unchanged
go-version: 1.19 # Scalar — wrapped to [1.19]
node: [14, 16, 18] # Array — unchanged
runs-on: ${{ matrix.os }}
```
### Example 3: Single Value Matrix
```yaml
jobs:
deploy:
strategy:
matrix:
environment: production # Scalar — wrapped to ["production"]
runs-on: ubuntu-latest
```
---
## ✅ Benefits
- ✨ **Feature Parity with GitHub Actions**: Supports template expressions in matrix strategies
- 🔒 **Robust Validation**: Detects misconfigured matrices (nested maps) early
- 🔄 **Backward Compatible**: Existing array-based workflows continue to work
- 📝 **Clear Error Messages**: Helpful validation messages for debugging
- 🛡️ **Type Safety**: Handles all YAML scalar types correctly
---
## 🚀 Breaking Changes
**Technically none in a strictly backward-incompatible sense**, but two semantic changes exist (see [⚠️ Semantic Changes](#️-semantic-changes-read-before-merging) above):
1. Scalar matrix values now iterate (previously they caused a `nil` matrix / no iteration)
2. Unevaluated template strings now produce a literal iteration instead of skipping
Both changes are improvements in practice, but downstream users should be aware.
---
## 📚 Related Work
- Related Gitea Server PR: [go-gitea/gitea#36564](https://github.com/go-gitea/gitea/pull/36564)
- Enables dynamic matrix generation from job outputs
- Supports advanced CI/CD patterns used in GitHub Actions
## 🔗 References
- [GitHub Actions: Matrix strategies](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs)
- [GitHub Actions: Expression syntax](https://docs.github.com/en/actions/learn-github-actions/expressions)
- [GitHub Actions: `fromJSON` in matrix context](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix)
Reviewed-on: https://gitea.com/gitea/act/pulls/149
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
Co-committed-by: Pascal Zimmermann <pascal.zimmermann@theiotstudio.com>
## Background
#819 replaced the shared `rate.Limiter` with per-worker exponential backoff counters to add jitter and adaptive polling. Before #819, the poller used:
```go
limiter := rate.NewLimiter(rate.Every(p.cfg.Runner.FetchInterval), 1)
```
This limiter was **shared across all N polling goroutines with burst=1**, effectively serializing their `FetchTask` calls — so even with `capacity=60`, the runner issued roughly one `FetchTask` per `FetchInterval` total.
#819 replaced this with independent per-worker `consecutiveEmpty` / `consecutiveErrors` counters. Each goroutine now backs off **independently**, which inadvertently removed the cross-worker serialization. With `capacity=N`, the runner now has N goroutines each polling on their own schedule — a regression from the pre-#819 baseline for any runner with `capacity > 1`.
(Thanks to @ChristopherHX for catching this in review.)
## Problem
With the post-#819 code:
- `capacity=N` maintains **N persistent polling goroutines**, each calling `FetchTask` independently
- At idle, N goroutines each wake up and send a `FetchTask` RPC per `FetchInterval`
- At full load, N goroutines **continue polling** even though no slot is available to run a new task — every one of those RPCs is wasted
- The `Shutdown()` timeout branch has a pre-existing bug: the "non-blocking check" is actually a blocking receive, so `shutdownJobs()` is never reached on timeout
## Real-World Impact: 3 Runners × capacity=60
Current production environment: 3 runners each with `capacity=60`.
| Metric | Post-#819 (current) | This PR | Reduction |
|--------|---------------------|---------|-----------|
| Polling goroutines (total) | 3 × 60 = **180** | 3 × 1 = **3** | **98.3%** (177 fewer) |
| FetchTask RPCs per poll cycle (idle) | **180** | **3** | **98.3%** |
| FetchTask RPCs per poll cycle (full load) | **180** (all wasted) | **0** (blocked on semaphore) | **100%** |
| Concurrent connections to Gitea | **180** | **3** | **98.3%** |
| Backoff state objects | 180 (per-worker) | 3 (one per runner) | Simplified |
### Idle scenario
All 180 goroutines wake up every `FetchInterval`, each sending a `FetchTask` RPC that returns empty. Server handles 180 RPCs per cycle for zero useful work. After this PR: **3 RPCs per cycle** — one per runner.
> Note: pre-#819 idle behavior was already ~3 RPCs/cycle due to the shared `rate.Limiter`. This PR restores that property while also addressing the full-load case below.
### Full-load scenario (all 180 slots occupied)
All 180 goroutines **continue polling** even though no slot is available. Every RPC is wasted. After this PR: all 3 pollers are **blocked on the semaphore** — **zero RPCs** until a task completes.
> This is a scenario neither the pre-#819 shared limiter nor the post-#819 per-worker backoff handles — both still issue `FetchTask` RPCs when no slot is free. The semaphore is the only approach of the three that ties polling to available capacity.
## Why Not Just Revert to `rate.Limiter`?
Reverting would restore the serialized behavior but is not the right long-term fix:
- **`rate.Limiter` has no concept of available capacity.** At full load it still hands out tokens and issues `FetchTask` RPCs that can't be acted on. The semaphore blocks polling entirely in that case — zero wasted RPCs.
- **It composes poorly with adaptive backoff from #819.** A shared limiter and per-worker backoff pull in different directions.
- **N goroutines serializing on a shared limiter means N-1 of them exist only to wait in line.** A single poller expresses the same behavior more directly.
The semaphore approach ties polling to capacity explicitly: `acquire slot → fetch → dispatch → release`. That invariant becomes structural rather than emergent from a rate limiter.
## Solution
Replace N polling goroutines with a **single polling loop** that uses a buffered channel as a semaphore to control concurrent task execution:
```go
// New: poller.go Poll()
sem := make(chan struct{}, p.cfg.Runner.Capacity)
for {
select {
case sem <- struct{}{}: // Acquire slot (blocks at capacity)
case <-p.pollingCtx.Done():
return
}
task, ok := p.fetchTask(...) // Single FetchTask RPC
if !ok {
<-sem // Release slot on empty response
// backoff...
continue
}
go func(t *runnerv1.Task) { // Dispatch task
defer func() { <-sem }() // Release slot when done
p.runTaskWithRecover(p.jobsCtx, t)
}(task)
}
```
The exponential backoff and jitter from #819 are preserved — just driven by a single `workerState` instead of N per-worker states.
## Shutdown Bug Fix
Fixed a pre-existing bug in `Shutdown()` where the timeout branch could never force-cancel running jobs:
```go
// Before (BROKEN): blocking receive, shutdownJobs() never reached
_, ok := <-p.done // blocks until p.done is closed
if !ok { return nil }
p.shutdownJobs() // dead code when jobs are still running
// After (FIXED): proper non-blocking check
select {
case <-p.done:
return nil
default:
}
p.shutdownJobs() // now correctly reached on timeout
```
## Code Changes
| Area | Detail |
|------|--------|
| `Poller.runner` | `*run.Runner` → `TaskRunner` interface (enables mock-based testing) |
| `Poll()` | N goroutines → single loop with buffered-channel semaphore |
| `PollOnce()` | Inlined from removed `pollOnce()` |
| `waitBackoff()` | New helper, eliminates duplicated backoff logic |
| `resetBackoff()` | New method on `workerState`, also resets stale `lastBackoff` metric |
| `Shutdown()` | Fixed blocking receive → proper non-blocking select |
| Removed | `poll()`, `pollOnce()` private methods (-2 methods, -42 lines) |
## Test Coverage
Added `TestPoller_ConcurrencyLimitedByCapacity` which verifies:
- With `capacity=3`, at most 3 tasks execute concurrently (`maxConcurrent <= 3`)
- Tasks actually overlap in execution (`maxConcurrent >= 2`)
- `FetchTask` is never called concurrently — confirms single poller (`maxFetchConcur == 1`)
- All 6 tasks complete successfully (`totalCompleted == 6`)
- Mock runner respects context cancellation, enabling shutdown path verification
```
=== RUN TestPoller_ConcurrencyLimitedByCapacity
--- PASS: TestPoller_ConcurrencyLimitedByCapacity (0.10s)
PASS
ok gitea.com/gitea/act_runner/internal/app/poll 0.59s
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/822
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Use `golangci-lint fmt` to format code, upgrading `.golangci.yml` to v2 and mirroring the linter configuration used by https://github.com/go-gitea/gitea. `gci` now handles import ordering into standard, project-local, blank, and default groups.
Mirrors https://github.com/go-gitea/gitea/pull/37194.
Changes:
- Upgrade `.golangci.yml` to v2 format with the same linter set as gitea (minus `prealloc`, `unparam`, `testifylint`, `nilnil` which produced too many pre-existing issues)
- Add path-based exclusions (`bodyclose`, `gosec` in tests; `gosec:G115`/`G117` globally)
- Run lint via `make lint-go` in CI instead of `golangci/golangci-lint-action`, matching the pattern used by other Gitea repos
- Apply safe auto-fixes (`modernize`, `perfsprint`, `usetesting`, etc.)
- Add explanations to existing `//nolint` directives
- Remove dead code (unused `newRemoteReusableWorkflow` and `networkName`), duplicate imports, and shadowed `max` builtins
- Replace deprecated `docker/distribution/reference` with `distribution/reference`
- Fix `Deprecated:` comment casing and simplify nil/len checks
---
This PR was written with the help of Claude Opus 4.7
Reviewed-on: https://gitea.com/gitea/act/pulls/163
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
## What
Add an optional Prometheus `/metrics` HTTP endpoint to `act_runner` so operators can observe runner health, polling behavior, job outcomes, and RPC latency without scraping logs.
New surface:
- `internal/pkg/metrics/metrics.go` — metric definitions, custom `Registry`, static Go/process collectors, label constants, `ResultToStatusLabel` helper.
- `internal/pkg/metrics/server.go` — hardened `http.Server` serving `/metrics` and `/healthz` with Slowloris-safe timeouts (`ReadHeaderTimeout` 5s, `ReadTimeout`/`WriteTimeout` 10s, `IdleTimeout` 60s) and a 5s graceful shutdown.
- `daemon.go` wires it up behind `cfg.Metrics.Enabled` (disabled by default).
- `poller.go` / `reporter.go` / `runner.go` instrument their existing hot paths with counters/histograms/gauges — no behavior change.
Metrics exported (namespace `act_runner_`):
| Subsystem | Metric | Type | Labels |
|---|---|---|---|
| — | `info` | Gauge | `version`, `name` |
| — | `capacity`, `uptime_seconds` | Gauge | — |
| `poll` | `fetch_total`, `client_errors_total` | Counter | `result` / `method` |
| `poll` | `fetch_duration_seconds`, `backoff_seconds` | Histogram / Gauge | — |
| `job` | `total` | Counter | `status` |
| `job` | `duration_seconds`, `running`, `capacity_utilization_ratio` | Histogram / GaugeFunc | — |
| `report` | `log_total`, `state_total` | Counter | `result` |
| `report` | `log_duration_seconds`, `state_duration_seconds` | Histogram | — |
| `report` | `log_buffer_rows` | Gauge | — |
| — | `go_*`, `process_*` | standard collectors | — |
All label values are predefined constants — **no high-cardinality labels** (no task IDs, repo URLs, branches, tokens, or secrets) so scraping is safe and bounded.
## Why
Teams self-hosting Gitea + `act_runner` at scale need to answer basic SRE questions that are currently invisible:
- How often are RPCs failing? Which RPC? (`act_runner_client_errors_total`)
- Are runners saturated? (`act_runner_job_capacity_utilization_ratio`, `act_runner_job_running`)
- How long do jobs take? (`act_runner_job_duration_seconds`)
- Is polling backing off? (`act_runner_poll_backoff_seconds`, `act_runner_poll_fetch_total{result=\"error\"}`)
- Are log/state reports slow? (`act_runner_report_{log,state}_duration_seconds`)
- Is the log buffer draining? (`act_runner_report_log_buffer_rows`)
Today operators have to grep logs. This PR makes all of the above first-class metrics so they can feed dashboards and alerts (`rate(act_runner_client_errors_total[5m]) > 0.1`, capacity saturation alerts, etc.).
The endpoint is **disabled by default** and binds to `127.0.0.1:9101` when enabled, so it's opt-in and safe for existing deployments.
## How
### Config
```yaml
metrics:
enabled: false # opt-in
addr: 127.0.0.1:9101 # change to 0.0.0.0:9101 only behind a reverse proxy
```
`config.example.yaml` documents both fields plus a security note about binding externally without auth.
### Wiring
1. `daemon.go` calls `metrics.Init()` (guarded by `sync.Once`), sets `act_runner_info`, `act_runner_capacity`, registers uptime + running-jobs GaugeFuncs, then starts the server goroutine with the daemon context — it shuts down cleanly on `ctx.Done()`.
2. `poller.fetchTask` observes RPC latency / result / error counters. `DeadlineExceeded` (long-poll idle) is treated as an empty result and **not** observed into the histogram so the 5s timeout doesn't swamp the buckets.
3. `poller.pollOnce` reports `poll_backoff_seconds` using the pre-jitter base interval (the true backoff level), and only when it changes — prevents noisy no-op gauge updates at the `FetchIntervalMax` plateau.
4. `reporter.ReportLog` / `ReportState` record duration histograms and success/error counters; `log_buffer_rows` is updated only when the value changes, guarded by the already-held `clientM`.
5. `runner.Run` observes `job_duration_seconds` and increments `job_total` by outcome via `metrics.ResultToStatusLabel`.
### Safety / security review
- All timeouts set; Slowloris-safe.
- Custom `prometheus.NewRegistry()` — no global registration side-effects.
- No sensitive data in labels (reviewed every instrumentation site).
- Single new dependency: `github.com/prometheus/client_golang v1.23.2`.
- Endpoint is unauthenticated by design and documented as such; default localhost bind mitigates exposure. Operators exposing externally should front it with a reverse proxy.
## Verification
### Unit tests
\`\`\`bash
go build ./...
go vet ./...
go test ./...
\`\`\`
### Manual smoke test
1. Enable metrics in `config.yaml`:
\`\`\`yaml
metrics:
enabled: true
addr: 127.0.0.1:9101
\`\`\`
2. Start the runner against a Gitea instance: \`./act_runner daemon\`.
3. Scrape the endpoint:
\`\`\`bash
curl -s http://127.0.0.1:9101/metrics | grep '^act_runner_'
curl -s http://127.0.0.1:9101/healthz # → ok
\`\`\`
4. Confirm the static series appear immediately: \`act_runner_info\`, \`act_runner_capacity\`, \`act_runner_uptime_seconds\`, \`act_runner_job_running\`, \`act_runner_job_capacity_utilization_ratio\`.
5. Trigger a workflow and confirm counters increment: \`act_runner_poll_fetch_total{result=\"task\"}\`, \`act_runner_job_total{status=\"success\"}\`, \`act_runner_report_log_total{result=\"success\"}\`.
6. Leave the runner idle and confirm \`act_runner_poll_backoff_seconds\` settles (and does **not** churn on every poll).
7. Ctrl-C and confirm a clean \"metrics server shutdown\" log line (no port-in-use error on restart within 5s).
### Prometheus integration
Add to \`prometheus.yml\`:
\`\`\`yaml
scrape_configs:
- job_name: act_runner
static_configs:
- targets: ['127.0.0.1:9101']
\`\`\`
Sample alert to try:
\`\`\`
sum(rate(act_runner_client_errors_total[5m])) by (method) > 0.1
\`\`\`
## Out of scope (follow-ups)
- TLS and auth on the metrics endpoint (mitigated today by localhost default; add when operators need external scraping).
- Per-task labels (intentionally avoided for cardinality safety).
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/820
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
## Summary
Many teams self-host Gitea + Act Runner at scale. The current runner design causes excessive HTTP requests to the Gitea server, leading to high server load. This PR addresses three root causes: aggressive fixed-interval polling, per-task status reporting every 1 second regardless of activity, and unoptimized HTTP client configuration.
## Problem
The original architecture has these issues:
**1. Fixed 1-second reporting interval (RunDaemon)**
- Every running task calls ReportLog + ReportState every 1 second (2 HTTP requests/sec/task)
- These requests are sent even when there are no new log rows or state changes
- With 200 runners × 3 tasks each = **1,200 req/sec just for status reporting**
**2. Fixed 2-second polling interval (no backoff)**
- Idle runners poll FetchTask every 2 seconds forever, even when no jobs are queued
- No exponential backoff or jitter — all runners can synchronize after network recovery (thundering herd)
- 200 idle runners = **100 req/sec doing nothing useful**
**3. HTTP client not tuned**
- Uses http.DefaultClient with MaxIdleConnsPerHost=2, causing frequent TCP/TLS reconnects
- Creates two separate http.Client instances (one for Ping, one for Runner service) instead of sharing
**Total: ~1,300 req/sec for 200 runners with 3 tasks each**
## Solution
### Adaptive Event-Driven Log Reporting
Replace the recursive `time.AfterFunc(1s)` pattern in RunDaemon with a goroutine-based select event loop using three trigger mechanisms:
| Trigger | Default | Purpose |
|---------|---------|---------|
| `log_report_max_latency` | 3s | Guarantee even a single log line is delivered within this time |
| `log_report_interval` | 5s | Periodic sweep — steady-state cadence |
| `log_report_batch_size` | 100 rows | Immediate flush during bursty output (e.g., npm install) |
**Key design**: `log_report_max_latency` (3s) must be less than `log_report_interval` (5s) so the max-latency timer fires before the periodic ticker for single-line scenarios.
State reporting is decoupled to its own `state_report_interval` (default 5s), with immediate flush on step transitions (start/stop) via a stateNotify channel for responsive frontend UX.
Additionally:
- Skip ReportLog when `len(rows) == 0` (no pending log rows)
- Skip ReportState when `stateChanged == false && len(outputs) == 0` (nothing changed)
- Move expensive `proto.Clone` after the early-return check to avoid deep copies on no-op paths
### Polling Backoff with Jitter
Replace fixed `rate.Limiter` with adaptive exponential backoff:
- Track `consecutiveEmpty` and `consecutiveErrors` counters
- Interval doubles with each empty/error response: `base × 2^(n-1)`, capped at `fetch_interval_max` (default 60s)
- Add ±20% random jitter to prevent thundering herd
- Fetch first, sleep after ��� preserves burst=1 behavior for immediate first fetch on startup and after task completion
### HTTP Client Tuning
- Configure custom `http.Transport` with `MaxIdleConnsPerHost=10` (was 2)
- Share a single `http.Client` between PingService and RunnerService
- Add `IdleConnTimeout=90s` for clean connection lifecycle
## Load Reduction
For 200 runners × 3 tasks (70% with active log output):
| Component | Before | After | Reduction |
|-----------|--------|-------|-----------|
| Polling (idle) | 100 req/s | ~3.4 req/s | 97% |
| Log reporting | 420 req/s | ~84 req/s | 80% |
| State reporting | 126 req/s | ~25 req/s | 80% |
| **Total** | **~1,300 req/s** | **~113 req/s** | **~91%** |
## Frontend UX Impact
| Scenario | Before | After | Notes |
|----------|--------|-------|-------|
| Continuous output (npm install) | ~1s | ~5s | Periodic ticker sweep |
| Single line then silence | ~1s | ≤3s | maxLatencyTimer guarantee |
| Bursty output (100+ lines) | ~1s | <1s | Batch size immediate flush |
| Step start/stop | ~1s | <1s | stateNotify immediate flush |
| Job completion | ~1s | ~1s | Close() retry unchanged |
## New Configuration Options
All have safe defaults — existing config files need no changes:
```yaml
runner:
fetch_interval_max: 60s # Max backoff interval when idle
log_report_interval: 5s # Periodic log flush interval
log_report_max_latency: 3s # Max time a log row waits (must be < log_report_interval)
log_report_batch_size: 100 # Immediate flush threshold
state_report_interval: 5s # State flush interval (step transitions are always immediate)
```
Config validation warns on invalid combinations:
- `fetch_interval_max < fetch_interval` → auto-corrected
- `log_report_max_latency >= log_report_interval` → warning (timer would be redundant)
## Test Plan
- [x] `go build ./...` passes
- [x] `go test ./...` passes (all existing + 3 new tests)
- [x] `golangci-lint run` — 0 issues
- [x] TestReporter_MaxLatencyTimer — verifies single log line flushed by maxLatencyTimer before logTicker
- [x] TestReporter_BatchSizeFlush — verifies batch size threshold triggers immediate flush
- [x] TestReporter_StateNotifyFlush — verifies step transition triggers immediate state flush
- [x] TestReporter_EphemeralRunnerDeletion — verifies Close/RunDaemon race safety
- [x] TestReporter_RunDaemonClose_Race — verifies concurrent Close safety
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/819
Reviewed-by: Nicolas <173651+bircni@noreply.gitea.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
## Summary
Adds a `container.bind_workdir` config option that exposes the nektos/act `BindWorkdir` setting. When enabled, workspaces are bind-mounted from the host filesystem instead of Docker volumes, which is required for DinD setups where jobs use `docker compose` with bind mounts (e.g. `.:/app`).
Each job gets an isolated workspace at `/workspace/<task_id>/<owner>/<repo>` to prevent concurrent jobs from the same repo interfering with each other. The task directory is cleaned up after job execution.
### Configuration
```yaml
container:
bind_workdir: true
```
When using this with DinD, also mount the workspace parent into the runner container and add it to `valid_volumes`:
```yaml
container:
valid_volumes:
- /workspace/**
```
*This PR was authored by Claude (AI assistant)*
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/810
Reviewed-by: ChristopherHX <38043+christopherhx@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
The main Gitea docker image is already distributed with proper semver tags, such that users can pin to e.g. the minor version and still pull in patch releases. This is something that has been lacking on the act runner images.
This PR expands the docker image tag versioning strategy such that when `v0.2.13` is released the following image tags are produced:
Basic:
- `0`
- `0.2`
- `0.2.13`
- `latest`
DinD:
- `0-dind`
- `0.2-dind`
- `0.2.13-dind`
- `latest-dind`
DinD-Rootless:
- `0-dind-rootless`
- `0.2-dind-rootless`
- `0.2.13-dind-rootless`
- `latest-dind-rootless`
To verify the `docker/metadata-action` produces the expected results in a Gitea workflow environment I've executed a release workflow. Results can be seen in [this run](https://gitea.com/RoboMagus/gitea_act_runner/actions/runs/14). (Note though that as the repository name of my fork changed from `act_runner` to `gitea_act_runner` this is reflected in the produced docker tags in this test run!)
---------
Co-authored-by: RoboMagus <68224306+RoboMagus@users.noreply.github.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/720
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: RoboMagus <robomagus@noreply.gitea.com>
Co-committed-by: RoboMagus <robomagus@noreply.gitea.com>
## Summary
- Fix data race on `r.closed` between `RunDaemon()` and `Close()` by protecting it with the existing `stateMu` — `closed` is part of the reporter state. `RunDaemon()` reads it under `stateMu.RLock()`, `Close()` sets it inside the existing `stateMu.Lock()` block
- `ReportState` now has a parameter to not report results from runDaemon even if set, from now on `Close` reports the result
- `Close` waits for `RunDaemon()` to signal exit via a closed channel `daemon` before reporting the final logs and state with result, unless something really wrong happens it does not time out
- Add `TestReporter_EphemeralRunnerDeletion` which reproduces the exact scenario from #793: RunDaemon's `ReportState` racing with `Close`, causing the ephemeral runner to be deleted before final logs are sent
- Add `TestReporter_RunDaemonClose_Race` which exercises `RunDaemon()` and `Close()` concurrently to verify no data race on `r.closed` under `go test -race`
- Enable `-race` flag in `make test` so CI catches data races going forward
Based on #794, with fixes for the remaining unprotected `r.closed` reads that the race detector catches.
Fixes#793
---------
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: rmawatson <rmawatson@hotmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/796
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
## Summary
- `actions/checkout`: v4/v5 → v6
- `actions/setup-go`: v5 → v6
- `docker/build-push-action`: v5 → v6
All other actions (`goreleaser/goreleaser-action@v6`, `docker/setup-qemu-action@v3`, `docker/setup-buildx-action@v3`, `docker/login-action@v3`, `crazy-max/ghaction-import-gpg@v6`) were already at their latest major versions.
No breaking changes affect the current workflow configurations. The main changes in the updated actions are Node.js 24 runtime upgrades and minor feature additions.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/792
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
## Summary
- Add `DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns` and `DOCKERD_ROOTLESS_ROOTLESSKIT_MTU=65520` to the DIND docker-compose example
- The `docker:dind-rootless` base image defaults to vpnkit as the network driver, which has substantially lower throughput than slirp4netns
## The problem
I noticed that pulling containers as well as downloading data within the container when running act_runner as DIND was very slow (see Ookla speedtest results in the following). While analysing the issue, I found that this was caused by the usage of vpnkit.
The `docker:dind-rootless` base image defaults to vpnkit as the network driver. slirp4netns was [added as an opt-in option](https://github.com/docker-library/docker/pull/543) and must be explicitly enabled via `DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns`.
This means anyone following the current DIND example gets vpnkit, which has significantly lower network throughput. This affects **all** network operations in the container — image pulls, package installs, and CI tasks.
Per the [rootlesskit iperf3 benchmarks](https://github.com/rootless-containers/rootlesskit/blob/master/docs/network.md):
| Driver | MTU 1500 | MTU 65520 |
|--------|----------|-----------|
| **vpnkit** | 0.60 Gbps | not supported |
| **slirp4netns** | 1.06 Gbps | 7.55 Gbps |
## Real-world benchmark results (Ookla speedtest, same server)
| | Download | Upload |
|---|---|---|
| **Default (vpnkit)** | ~130 Mbps | ~126 Mbps |
| **slirp4netns + MTU 65520** | ~958 Mbps | ~462 Mbps |
## References
- [docker-library/docker#543](https://github.com/docker-library/docker/pull/543) — added slirp4netns to dind-rootless as opt-in (vpnkit remains default)
- [rootlesskit network docs](https://github.com/rootless-containers/rootlesskit/blob/master/docs/network.md) — iperf3 benchmarks
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/786
Reviewed-by: silverwind <silverwind@noreply.gitea.com>
Co-authored-by: stecklars <stecklars@noreply.gitea.com>
Co-committed-by: stecklars <stecklars@noreply.gitea.com>
## Summary
- Bumps Go version from 1.24.0 (toolchain go1.24.11) to 1.26.0
- Fixes CI `govulncheck` failures caused by three standard library vulnerabilities in go1.24.11:
- GO-2026-4341: Memory exhaustion in `net/url` query parameter parsing
- GO-2026-4340: Handshake messages at incorrect encryption level in `crypto/tls`
- GO-2026-4337: Unexpected session resumption in `crypto/tls`
## Test plan
- [x] `make vet` passes
- [x] `make build` passes
- [x] `make test` passes (includes `govulncheck` and all unit tests)
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/787
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
The effect probably does not **yet** occur in the published versions because the last publication took place before the release of Docker v29.
Therefore, no one should expect version 29 of Docker to be used, so there are basically no side effects.
---
fix#768
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/769
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Max P. <mail@0xMax42.io>
Co-committed-by: Max P. <mail@0xMax42.io>
Relevant: https://gitea.com/gitea/act_runner/issues/735
See my example below, `edit` is my PR, `non-edit` is the origin/main.
```
dselen@N-DESKTOP1:~/development/act_runner$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
runner edit b12322f8c3f0 26 seconds ago 43.5MB
runner non-edit e5593ad32c16 34 minutes ago 43.1MB
dselen@N-DESKTOP1:~/development/act_runner$ docker run -d -e TZ=Europe/Amsterdam runner:non-edit
5f26979515f461a2a7e342aa586d7b91224d2d3c3dcf1ed0c1e7293ff00645a4
dselen@N-DESKTOP1:~/development/act_runner$ docker run -d -e TZ=Europe/Amsterdam runner:edit
9cc5fc6b364cf07776d97c6c60c03f23372eb2c93c7da8d3d80f4f6dc2a6b10e
dselen@N-DESKTOP1:~/development/act_runner$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9cc5fc6b364c runner:edit "/sbin/tini -- run.sh" 2 seconds ago Up 2 seconds serene_bardeen
5f26979515f4 runner:non-edit "/sbin/tini -- run.sh" 5 seconds ago Up 5 seconds jovial_euler
dselen@N-DESKTOP1:~/development/act_runner$ docker exec -it jovial_euler bash
5f26979515f4:/# date
Thu Aug 21 16:40:35 UTC 2025
dselen@N-DESKTOP1:~/development/act_runner$ docker exec -it serene_bardeen bash
9cc5fc6b364c:/# date
Thu Aug 21 18:40:42 CEST 2025
```
I do not see why this would not be acceptable, its only 400KB
Regards.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/738
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: ChristopherHX <christopherhx@noreply.gitea.com>
Co-authored-by: DaanSelen <daanselen@noreply.gitea.com>
Co-committed-by: DaanSelen <daanselen@noreply.gitea.com>
Before this commit, when running locally `act_runner exec` to test workflows, we could only fill env and secrets but not vars
This commit add a new exec option `--var` based on what is done for env and secret
Example:
`act_runner exec --env MY_ENV=testenv -s MY_SECRET=testsecret --var MY_VAR=testvariable`
workflow
```
name: Gitea Actions test
on: [push]
jobs:
TestAction:
runs-on: ubuntu-latest
steps:
- run: echo "VAR -> ${{ vars.MY_VAR }}"
- run: echo "ENV -> ${{ env.MY_ENV }}"
- run: echo "SECRET -> ${{ secrets.MY_SECRET }}"
```
Will echo var, env and secret values sent in the command line
Fixesgitea/act_runner#692
Co-authored-by: Lautriva <gitlactr@dbn.re>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/704
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: lautriva <lautriva@noreply.gitea.com>
Co-committed-by: lautriva <lautriva@noreply.gitea.com>
I used to be able to do something like `./act_runner register --instance https://gitea.com --token testdcff --name test` on GitHub Actions Runners, but act_runner always asked me to enter instance, token etc. again and requiring me to use `--no-interactive` including passing everything per cli.
My idea was to extract the preset input of some stages to skip the prompt for this value if it is a non empty string. Labels is the only question that has been asked more than once if validation failed, in this case the error path have to unset the values of the input structure to not end in a non-interactive loop.
_I have written this initially for my own gitea runner, might be useful to everyone using the official runner as well_
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/682
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Co-committed-by: Christopher Homberger <christopher.homberger@web.de>
- `basic`: Only the runner process in the container; users need to mount the Docker socket to it.
- `dind`: A Docker daemon will be started in the container with the root user.
- `dind-rootless`: A Docker daemon will be started in the container with a rootless user.
Use s6 instead of supervisord to start processes.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/619
Reviewed-by: Zettat123 <zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
```
[command]/opt/hostedtoolcache/goreleaser-action/2.3.2-pro/x64/goreleaser release --nightly
• by using this software you agree with its EULA, available at https://goreleaser.com/eula
• running goreleaser v2.3.2-pro
• only configurations files on version: 2 are supported, yours is version: 0 , please update your configuration
⨯ release failed after 0s error=only configurations files on version: 2 are supported, yours is version: 0 , please update your configuration
::error::The process '/opt/hostedtoolcache/goreleaser-action/2.3.2-pro/x64/goreleaser' failed with exit code 1
```
```
#20 [linux/arm64 builder 2/5] RUN apk add --no-cache make git
#20 CANCELED
------
> [linux/amd64 builder 5/5] RUN make clean && make build:
0.058 go clean -x -i ./...
0.061 go: go.mod requires go >= 1.22 (running go 1.21.10; GOTOOLCHAIN=local)
0.061 make: *** [Makefile:176: clean] Error 1
------
1 warning found (use docker --debug to expand):
- FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)
Dockerfile:8
--------------------
6 | WORKDIR /opt/src/act_runner
7 |
8 | >>> RUN make clean && make build
9 |
10 | FROM alpine:3.18
--------------------
ERROR: failed to solve: process "/bin/sh -c make clean && make build" did not complete successfully: exit code: 2
::error::buildx failed with: ERROR: failed to solve: process "/bin/sh -c make clean && make build" did not complete successfully: exit code: 2
```
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/603
Co-authored-by: Jason Song <i@wolfogre.com>
Co-committed-by: Jason Song <i@wolfogre.com>
We wanted the ability to disable outputting the logs from the individual job to the console. This changes the logging so that job logs are only output to the console whenever debug logging is enabled in `act_runner`, while still allowing the `Reporter` to receive these logs and forward them to Gitea when debug logging is not enabled.
Co-authored-by: Rowan Bohde <rowan.bohde@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/543
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
Co-committed-by: rowan-allspice <rowan-allspice@noreply.gitea.com>
Or the latest nightly version could be "v0.2.8-4-gbe2df36" even though there's "v0.2.9", although they are for the same commit.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/536
## Description
Issue described in #460
## Changes
- Edited `supervisord.conf` to exit if it detects any of the supervisored processes exiting.
- minor text fix
## Notes
Without this change (or something similar), if act_runner fails, then the container will stay up as a zombie container - it does nothing and does not restart. After this change, if act_runner fails (e.g. due to Gitea instance being down), then supervisord will exit and the container will be restarted.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/462
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: davidfrickert <david.frickert@protonmail.com>
Co-committed-by: davidfrickert <david.frickert@protonmail.com>
Added environment variable `RUNNER_STATE_FILE` to let users specify
where `run.sh` looks for the runner JSON file. Defaults to ``.runner``
to preserve the original behavior.
Addresses issue #368. It's not my preferred solution but it's the least invasive one I can think of.
I'm happy to make any changes you want.
I didn't see an appropriate place to reference the change in the documentation. I will add documentation wherever you think is appropriate.
To test this I did the following:
* Built an image with the `make docker` command and pushed it to my private registry.
* Added that private image as the image in my existing docker stack that was exhibiting the behavior described in #368.
* Added the RUNNER_STATE_FILE environment variable pointing to the runner JSON file set in my `config.yml` (``/data/runner.json`` in this case).
* Configured a new runner in gitea and added the token as an environment variable in the stack config.
* Deployed the stack and verified the new runner was recognized (in the idle state).
* Force updated the runner service to restart the container.
* Verified the same runner was still recognized in gitea (in the idle state) once it was back up and running.
Here is the relevant config. It's stored as a template so I've left the things that would normally be redacted as they are in the template.
```
runner:
image: hub.hax.in.net/haxwithaxe/act_runner:dev1
networks:
- swarm-net
environment:
RUNNER_STATE_FILE: /data/runner.json
CONFIG_FILE: /data/config.yml
GITEA_INSTANCE_URL: "https://git_gitea"
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ git_runner_reg_token }}"
GITEA_RUNNER_NAME: "git_runner"
volumes:
- runner_data:/data
- /var/run/docker.sock:/var/run/docker.sock
```
`runner_data` is a glusterfs subvolume
Thanks for creating this gitea specific fork. Apart from the issue this pull request addresses it works so well I don't have to think about it once it's set up.
Co-authored-by: haxwithaxe <spam@haxwithaxe.net>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/377
Co-authored-by: haxwithaxe <haxwithaxe@noreply.gitea.com>
Co-committed-by: haxwithaxe <haxwithaxe@noreply.gitea.com>
Without this, actions fail when attempting to clone the repo:
```
2023-07-18 17:11:02 [Smoke Test/Run basic test suite] failed to attach to exec: http: invalid Host header
```
This appears to be the same as https://github.com/nektos/act/issues/1908, and only shows up with golang 1.20.6. Staying with golang 1.20.5 is a temporary solution to avoid the bug until it is fixed upstream.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/295
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
- Removed `deadcode`, `structcheck`, and `varcheck` linters from `.golangci.yml`
- Fixed a typo in a comment in `daemon.go`
- Renamed `defaultActionsUrl` to `defaultActionsURL` in `exec.go`
- Removed unnecessary else clause in `exec.go` and `runner.go`
- Simplified variable initialization in `exec.go`
- Changed function name from `getHttpClient` to `getHTTPClient` in `http.go`
- Removed unnecessary else clause in `labels_test.go`
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/289
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This PR
- adds the `cache-server` command so act_runner can run as a cache server. When running as a cache server, act_runner only processes the requests related to cache and does not run jobs.
- adds the `external_server` configuration for cache. If specified, act_runner will use this URL as the ACTIONS_CACHE_URL instead of starting a cache server itself.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/275
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
Follow #242, #244Fixes#258
Users could use `docker_host` configuration to specify which docker daemon will be used by act_runner.
- If `docker_host` is **empty**, act_runner will find an available docker host automatically.
- If `docker_host` is **"-"**, act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
- If `docker_host` is **not empty or "-"**, the specified docker host will be used. An error will be returned if it doesn't work.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/260
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
Follow #242.
Move `docker.host` to `container.docker_host`.
There are already some options for docker/container in `container`, so developers could get confused about where to add options.
It's breaking, but I think it's OK since `docker.host` was added just two days ago.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/244
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
- Import "path", "runtime", "strconv", and "strings" packages in daemon.go
- Move "Starting runner daemon" log message to a different location
- Refactor log formatter initialization and add debug level caller information
- Split Config struct into separate Log, Runner, Cache, and Container structs with comments in config.go
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/225
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-committed-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Close https://gitea.com/gitea/act_runner/issues/177
Related https://gitea.com/gitea/act/pulls/56
### ⚠️ Breaking
The `container.network_mode` is a deprecated configuration item. It may be removed after Gitea 1.20 released.
Previously, if the value of `container.network_mode` is `bridge`, it means that `act_runner` will create a new network for job.But `bridge` is easily confused with the bridge network created by Docker by default.
We recommand that using `container.network` to specify the network to which containers created by `act_runner` connect.
### 🆕 container.network
The configuration file of `act_runner` add a new item of `contianer.network`.
In `config.example.yaml`:
```yaml
container:
# Specifies the network to which the container will connect.
# Could be host, bridge or the name of a custom network.
# If it's empty, act_runner will create a network automatically.
network: ""
```
As the comment in the example above says, the purpose of the `container.network` is specifying the network to which containers created by `act_runner` will connect.
`container.network` accepts the following valid values:
- `host`: All of containers (including job containers and service contianers) created by `act_runner` will be connected to the network named `host` which is created automatically by Docker. Containers will share the host’s network stack and all interfaces from the host will be available to these containers.
- `bridge`: It is similar to `host`. All of containers created by `act_runner` will be connected to the network named `bridge` which is created automatically by Docker. All containers connected to the `bridge` (Perhaps there are containers that are not created by `act_runner`) are allowed to communicate with each other, while providing isolation from containers which are not connected to that `bridge` network.
- `<custom_network>`: Please make sure that the `<custom_network>` network already exists firstly (`act_runner` does not detect whether the specified network exists currently. If not exists yet, will return error in the stage of `docker create`). All of containers created by `act_runner` will be connected to `<custom_network>`. After the job is executed, containers are removed and automatically disconnected from the `<custom_network>`.
- empty: `act_runner` will create a new network for each job container and their service containers (if defined in workflow). So each job container and their service containers share a network environment, but are isolated from others container and the Docker host. Of course, these networks created by `act_runner` will be removed at last.
### Others
- If you do not have special needs, we highly recommend that setting `container.network` to empty string (and do not use `container.network_mode` any more). Because the containers created by `act_runner` will connect to the networks that are created by itself. This point will provide better isolation.
- If you set `contianer.network` to empty string or `<custom_network>`, we can be access to service containers by `<service-id>:<port>` in the steps of job. Because we added an alias to the service container when connecting to the network.
Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/184
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <gedong_1994@163.com>
Co-committed-by: sillyguodong <gedong_1994@163.com>
Fixes#145
At present, the working directory of a work flow is a path like `/<owner>/<repo>`, so the directory may conflict with system directory like `/usr/bin`. We need to add a parent directory for the working directory.
In this PR, the parent directory is `/workspace` by default and users could configure it by the `workdir_parent` option.
This change doesn't affect the host mode because in host mode the working directory will always be in `$HOME/.cache/act/` directory.
Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/154
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-committed-by: Zettat123 <zettat123@gmail.com>
### Add secret
```
DOCKERHUB_TOKEN=xxx
```
### Tag
when tag like `v1.0.0`, it will build multi platform docker image `gitea/act_runner:1.0.0` and `gitea/act_runner:latest`, then push to docker hub
### Use
> volume `/data` save `.runner` config file
> volume `/root/.cache` save actcache and actions cache
```sh
docker run -e GITEA_INSTANCE_URL=*** \
-e GITEA_RUNNER_REGISTRATION_TOKEN=*** \
-e GITEA_RUNNER_NAME=*** \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /root/act_runner/data:/data \
-v /root/act_runner/cache:/root/.cache \
gitea/act_runner
```
Test join runners success

Test run action success

Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/118
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: seepine <seepine@noreply.gitea.io>
Co-committed-by: seepine <seepine@noreply.gitea.io>
This adds a very simple Dockerfile and run script for running `act_runner` as a container.
It also allows setting `Privileged` and `ContainerOptions` flags via the new config file when spawning task containers. The combination makes it possible to use Docker-in-Docker (which requires `privileged` mode) as well as pass any other options child Docker containers may require.
For example, if Gitea is running in Docker on the same machine, for the `checkout` action to behave as expected from a task container launched by `act_runner`, it might be necessary to map the hostname via something like:
```
container:
network_mode: bridge
privileged: true
options: --add-host=my.gitea.hostname:host-gateway
```
> NOTE: Description updated to reflect latest code.
> NOTE: Description updated to reflect latest code (again).
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/84
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Thomas E Lackey <telackey@bozemanpass.com>
Co-committed-by: Thomas E Lackey <telackey@bozemanpass.com>
Close#21.
Refactor environment variables to configuration file (config.yaml) and registration file (.runner).
The old environment variables are still supported, but warning logs will be printed.
Like:
```text
$ GITEA_DEBUG=true ./act_runner -c config.yaml daemon
INFO[0000] Starting runner daemon
WARN[0000] env GITEA_DEBUG has been ignored because config file is used
$ GITEA_DEBUG=true ./act_runner daemon
INFO[0000] Starting runner daemon
WARN[0000] env GITEA_DEBUG will be deprecated, please use config file instead
```
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/90
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Fix#64 (incompletely).
It's still not ideal. It makes more sense to use the gateway IP address of container network as outbound IP of cache server. However, this requires act to cooperate, some think like:
- act creates the network for new container, and returns the network to runner.
- runner extracts the gateway IP in the network.
- runner uses the gateway IP as outbound IP, and pass it to act as cache server endpoint.
- act It continues to create the container with the created network.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/74
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
The label will follow the format `label[:schema[:args]]`, and the schema will be `host` if it's omitted. So
- `ubuntu:docker://node:18`: Run jobs with label `ubuntu` via docker with image `node:18`
- `ubuntu:host`: Run jobs with label `ubuntu` on the host directly.
- `ubuntu`: Same as `ubuntu:host`.
- `ubuntu:vm:ubuntu-latest`: (Just a example, not Implemented) Run jobs with label `ubuntu` via virtual machine with iso `ubuntu-latest`.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/69
Reviewed-by: Zettat123 <zettat123@noreply.gitea.io>
Reviewed-by: wxiaoguang <wxiaoguang@noreply.gitea.io>
close#42
1. Inject runner version when `make build`
After building, executing command line: `./act_runner -v` or `./act_runner --version`, the version of runner is printed.

2. In `Actions` UI:

3. Set request header in http client interceptor.
Co-authored-by: sillyguodong <gedong_1994@163.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/43
Reviewed-by: delvh <dev.lh@web.de>
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: sillyguodong <sillyguodong@noreply.gitea.io>
Co-committed-by: sillyguodong <sillyguodong@noreply.gitea.io>
This PR is to fix the wrong last step duration when job failed like shown in the screenshot.
The reason is because when job failed, `Fire` function did not pass in Time, and `r.state.StoppedAt` is by default set to `0001-01-01 08:05:43 +0805 LMT`, which is later on reported to gitea by `UpdateTask`, which calls `UpdateTaskByState` to update the `task.Stopped`, and `task.Stopped` is used in `FullSteps`, resulting in wrong calcaulation of last step duration.
Co-authored-by: nickname <test@123.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/41
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: HesterG <hesterg@noreply.gitea.io>
Co-committed-by: HesterG <hesterg@noreply.gitea.io>
If the Workdir field doesn't ends with the filepath seperator,
bad things happen
Fixes#33
Sample for host mode on windows, needs be adjusted for linux e.g. replace pwsh with bash
Also fixes
```yaml
on: push
jobs:
_:
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
with:
path: subdir/action
- uses: ./subdir/action
```
with an action.yml in the same repo
```yaml
runs:
using: composite
steps:
- run: |
echo "Hello World"
shell: pwsh
```
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/34
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: ChristopherHX <christopherhx@noreply.gitea.io>
Co-committed-by: ChristopherHX <christopherhx@noreply.gitea.io>
When deploy a Gitea server with a self-signed HTTPS certification. Runner will be failed when connect to Gitea server. This PR will fix that to allow ignore the HTTPS certification verification.
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/18
Reviewed-by: Jason Song <i@wolfogre.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-committed-by: Lunny Xiao <xiaolunwen@gmail.com>
The only reason docker is really required by now, is that act_runner ping docker.
This change only pings docker if a label with `docker://` is added to the runner.
Plain labels without `:` like `self-hosted` are run directly on the system. Previously the pseudo non docker label `-self-hosted` have been required like this `self-hosted:docker://-self-hosted`, but due to docker ping this still required a dockerd to be pingable.
Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/16
Reviewed-by: Jason Song <i@wolfogre.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: ChristopherHX <christopherhx@noreply.gitea.io>
Co-committed-by: ChristopherHX <christopherhx@noreply.gitea.io>
We use [connect-go](https://github.com/bufbuild/connect-go) instead of [grpc-go](https://github.com/grpc/grpc-go) because connect-go support HTTP/1.1, that means we can mount the gRPC api on the Gitea server without change the protocol.
So it doesn't make sense that make the runner support both HTTP/1.1 and HTTP/2, and [upgrade the protocol used on Gitea](
ae018b6b48/modules/graceful/server_http.go (L23)) to support HTTP/2 and h2c. Although it works right now, I believe there'll be lots of problems when the Gitea server is behind a reverse proxy.
So let's KISS, we don't touch the http protocol of Gitea, and disable HTTP/2 for runner. And we would support HTTP/2 in the future if we really need it.
Co-authored-by: Jason Song <i@wolfogre.com>
Reviewed-on: https://gitea.com/gitea/act_runner/pulls/4
description:Use this template for reporting bugs/issues.
labels:
- 'kind/bug'
body:
- type:markdown
attributes:
value:|
Thanks for taking the time to fill out this bug report!
- type:textarea
id:act-debug
attributes:
label:Bug report info
render:plain text
description:|
Output of `act --bug-report`
placeholder:|
act --bug-report
validations:
required:true
- type:textarea
id:act-command
attributes:
label:Command used with act
description:|
Please paste your whole command
placeholder:|
act -P ubuntu-latest=node:12 -v -d ...
render:sh
validations:
required:true
- type:textarea
id:what-happened
attributes:
label:Describe issue
description:|
Also tell us what did you expect to happen?
placeholder:|
Describe issue
validations:
required:true
- type:input
id:repo
attributes:
label:Link to GitHub repository
description:|
Provide link to GitHub repository, you can skip it if the repository is private or you don't have it on GitHub, otherwise please provide it as it might help us troubleshoot problem
placeholder:|
https://github.com/nektos/act
validations:
required:false
- type:textarea
id:workflow
attributes:
label:Workflow content
description:|
Please paste your **whole** workflow here
placeholder:|
name: My workflow
on: ['push', 'schedule']
jobs:
test:
runs-on: ubuntu-latest
env:
KEY: VAL
[...]
render:yml
validations:
required:true
- type:textarea
id:logs
attributes:
label:Relevant log output
description:|
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Please verify that the log output doesn't contain any sensitive data.
- Use `make help` to find available development targets
- PR descriptions: minimal, only what and why, no task lists or file listings
- Reference issues and PRs by full URL, not by number
- Use Conventional Commits for commit messages and PR titles, plus the `enhance` type for user-facing enhancements
- Add an `Assisted-by: AGENT_NAME:MODEL_VERSION` trailer to commit messages, never `Co-Authored-By` or `Signed-off-by`
- Attribute agent authorship on one trailing line in issue and pull request comments, never as a PR description section
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
- Comments: write almost none, short and preferably same-line, explaining why for a future reader. Never narrate code, the change or the prompt. Preserve existing ones that still apply
- Add the current year into the copyright header of new `.go` files
- Read `DEVELOPMENT.md` for internals and conventions
- Ensure no trailing whitespace in edited files
- Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, and `make checks` for the non-lint source checks
- Lint what changed with `make lint-go`, and `make lint-go-windows` for Windows and platform-split files
- Fix the cause rather than disabling a linter or weakening a test. Where unavoidable, use the narrowest scope with a trailing comment giving the reason
- Run single go tests with `go test -run '^TestName$' ./modulepath/`. `make test` self-skips the integration tests without docker or network, `make test-dind` runs the daemon-facing tests against the built dind image
- Write the fewest, fastest tests covering the behavior, extending an existing one where possible. Prefer unit tests where logic is testable in isolation
- Wait on a deterministic condition rather than `sleep`
- Update the files under `docs/` when behavior documented there changes
Help wanted! We'd love your contributions to Act. Please review the following guidelines before contributing. Also, feel free to propose changes to these guidelines by updating this file and submitting a pull request.
- [I have a question...](#questions)
- [I found a bug...](#bugs)
- [I have a feature request...](#features)
- [I have a contribution to share...](#process)
## <a id="questions"></a> Have a Question?
Please don't open a GitHub issue for questions about how to use `act`, as the goal is to use issues for managing bugs and feature requests. Issues that are related to general support will be closed and redirected to our gitter room.
For all support related questions, please ask the question in our gitter room: [nektos/act](https://gitter.im/nektos/act).
## <a id="bugs"></a> Found a Bug?
If you've identified a bug in `act`, please [submit an issue](#issue) to our GitHub repo: [nektos/act](https://github.com/nektos/act/issues/new). Please also feel free to submit a [Pull Request](#pr) with a fix for the bug!
## <a id="features"></a> Have a Feature Request?
All feature requests should start with [submitting an issue](#issue) documenting the user story and acceptance criteria. Again, feel free to submit a [Pull Request](#pr) with a proposed implementation of the feature.
## <a id="process"></a> Ready to Contribute
### <a id="issue"></a> Create an issue
Before submitting a new issue, please search the issues to make sure there isn't a similar issue doesn't already exist.
Assuming no existing issues exist, please ensure you include required information when submitting the issue to ensure we can quickly reproduce your issue.
We may have additional questions and will communicate through the GitHub issue, so please respond back to our questions to help reproduce and resolve the issue as quickly as possible.
New issues can be created with in our [GitHub repo](https://github.com/nektos/act/issues/new).
### <a id="pr"></a>Pull Requests
Pull requests should target the `master` branch. Please also reference the issue from the description of the pull request using [special keyword syntax](https://help.github.com/articles/closing-issues-via-commit-messages/) to auto close the issue when the PR is merged. For example, include the phrase `fixes #14` in the PR description to have issue #14 auto close. Please send documentation updates for the [act user guide](https://nektosact.com) to [nektos/act-docs](https://github.com/nektos/act-docs).
### <a id="style"></a> Styleguide
When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Here are a few points to keep in mind:
- Please run `go fmt ./...` before committing to ensure code aligns with go standards.
- We use [`golangci-lint`](https://golangci-lint.run/) for linting Go code, run `golangci-lint run --fix` before submitting PR. Editors such as Visual Studio Code or JetBrains IntelliJ; with Go support plugin will offer `golangci-lint` automatically.
- There are additional linters and formatters for files such as Markdown documents or YAML/JSON:
- Please refer to the [Makefile](Makefile) or [`lint` job in our workflow](.github/workflows/checks.yml) to see how to those linters/formatters work.
- You can lint codebase by running `go run main.go -j lint --env RUN_LOCAL=true` or `act -j lint --env RUN_LOCAL=true`
- In `Makefile`, there are tools that require `npx` which is shipped with `nodejs`.
- Our `Makefile` exports `GITHUB_TOKEN` from `~/.config/github/token`, you have been warned.
- You can run `make pr` to cleanup dependencies, format/lint code and run tests.
- All dependencies must be defined in the `go.mod` file.
- Advanced IDEs and code editors (like VSCode) will take care of that, but to be sure, run `go mod tidy` to validate dependencies.
- For details on the approved style, check out [Effective Go](https://golang.org/doc/effective_go.html).
- Before running tests, please be aware that they are multi-architecture so for them to not fail, you need to run `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64` before ([more info available in #765](https://github.com/nektos/act/issues/765)).
Also, consider the original design principles:
- **Polyglot** - There will be no prescribed language or framework for developing the microservices. The only requirement will be that the service will be run inside a container and exposed via an HTTP endpoint.
- **Cloud Provider** - At this point, the tool will assume AWS for the cloud provider and will not be written in a cloud agnostic manner. However, this does not preclude refactoring to add support for other providers at a later time.
- **Declarative** - All resource administration will be handled in a declarative vs. imperative manner. A file will be used to declared the desired state of the resources and the tool will simply assert the actual state matches the desired state. The tool will accomplish this by generating CloudFormation templates.
- **Stateless** - The tool will not maintain its own state. Rather, it will rely on the CloudFormation stacks to determine the state of the platform.
- **Secure** - All security will be managed by AWS IAM credentials. No additional authentication or authorization mechanisms will be introduced.
### License
By contributing your code, you agree to license your contribution under the terms of the [MIT License](LICENSE).
**Warning:** Below badges with size for each image are displaying size of **compressed image size in registry. After pulling the image, size can be drastically different due to Docker uncompressing the image layers.**
## Images based on [`buildpack-deps`][hub/_/buildpack-deps]
**Note 1: `node` images are based on Debian root filesystem, while it is extremely similar to Ubuntu, there might be some differences**
**Note 2: `node` `-slim` images don't have `python` installed, if you want to use actions or software that is depending on `python`, you need to specify image manually**
## Images based on [`actions/virtual-environments`][gh/actions/virtual-environments]
**Note: `nektos/act-environments-ubuntu` have been last updated in February, 2020. It's recommended to update the image manually after `docker pull` if you decide to use it.**
This is a custom fork of [nektos/act](https://github.com/nektos/act/), for the purpose of serving [act_runner](https://gitea.com/gitea/act_runner).
Gitea Runner runs the jobs of Gitea Actions workflows. Register it with a Gitea instance to run jobs in Docker containers, Kubernetes pods or directly on the host.
It cannot be used as command line tool anymore, but only as a library.
## Installation
It's a soft fork, which means that it will track the latest release of nektos/act.
### Prerequisites
Branches:
- Linux, macOS or Windows, or FreeBSD.
-`git` 2.34.1 or newer.
-`docker`, or `podman` with `podman.socket` enabled, for `docker` labels and Docker actions. See Docker's [install instructions](https://docs.docker.com/engine/install/).
-`main`: default branch, contains custom changes, based on the latest release(not the latest of the master branch) of nektos/act.
-`nektos/master`: mirror for the master branch of nektos/act.
In the job image, or on the host for host jobs:
Tags:
-`bash` or `sh`, or `pwsh` or `powershell` on Windows.
-`node` for JavaScript actions and `hashFiles()`.
-`git` 2.34.1 or newer for [`builtin:checkout`](docs/builtin-actions.md).
-`git-lfs` for `builtin:checkout` with `lfs: true`.
-`ssh` for `builtin:checkout` with `ssh-key`.
-`nektos/vX.Y.Z`: mirror for `vX.Y.Z` of [nektos/act](https://github.com/nektos/act/).
-`vX.YZ.*`: based on `nektos/vX.Y.Z`, contains custom changes.
- Examples:
-`nektos/v0.2.23` -> `v0.223.*`
-`nektos/v0.3.1` -> `v0.301.*`, not ~~`v0.31.*`~~
-`nektos/v0.10.1` -> `v0.1001.*`, not ~~`v0.101.*`~~
-`nektos/v0.3.100` -> not ~~`v0.3100.*`~~, I don't think it's really going to happen, if it does, we can find a way to handle it.
Kubernetes mode needs [a few more tools](docs/kubernetes.md) in job images.
Download the binary for your platform from [dl.gitea.com](https://dl.gitea.com/gitea-runner/).
# Overview [](https://github.com/nektos/act/actions) [](https://gitter.im/nektos/act?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://goreportcard.com/report/github.com/nektos/act) [](https://github.com/jonico/awesome-runners)
### Build from source
> "Think globally, `act` locally"
```bash
make build
```
Run your [GitHub Actions](https://developer.github.com/actions/) locally! Why would you want to do this? Two reasons:
### Build a docker image
- **Fast Feedback** - Rather than having to commit/push every time you want to test out the changes you are making to your `.github/workflows/` files (or for any changes to embedded GitHub actions), you can use `act` to run the actions locally. The [environment variables](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables) and [filesystem](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#filesystems-on-github-hosted-runners) are all configured to match what GitHub provides.
- **Local Task Runner** - I love [make](<https://en.wikipedia.org/wiki/Make_(software)>). However, I also hate repeating myself. With `act`, you can use the GitHub Actions defined in your `.github/workflows/` to replace your `Makefile`!
```bash
make docker
```
# How Does It Work?
## Quickstart
When you run `act` it reads in your GitHub Actions from `.github/workflows/` and determines the set of actions that need to be run. It uses the Docker API to either pull or build the necessary images, as defined in your workflow files and finally determines the execution path based on the dependencies that were defined. Once it has the execution path, it then uses the Docker API to run containers for each action based on the images prepared earlier. The [environment variables](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables) and [filesystem](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#file-systems) are all configured to match what GitHub provides.
Enable Actions in the configuration file of your Gitea instance:
Let's see it in action with a [sample repo](https://github.com/cplee/github-actions-demo)!
Want to contribute to act? Awesome! Check out the [contributing guidelines](CONTRIBUTING.md) to get involved.
### Run
## Manually building from source
```bash
./gitea-runner daemon
```
- Install Go tools 1.20+ - (<https://golang.org/doc/install>)
- Clone this repo `git clone git@github.com:nektos/act.git`
- Run unit tests with `make test`
- Build and install: `make install`
## Deployment
### Windows service
Register the runner first, keeping `gitea-runner.exe`, `config.yaml` and the `.runner` file in one directory. Then install and start the service from an elevated Command Prompt:
```bat
sc.exe create gitea-runner binPath= "\"C:\gitea-runner\gitea-runner.exe\" daemon --config \"C:\gitea-runner\config.yaml\"" start= auto
sc.exe start gitea-runner
```
The service logs to `gitea-runner.log` next to the executable. Its account needs write access to that directory and access to the tools jobs use.
`sc.exe stop gitea-runner` lets running jobs finish within `runner.shutdown_timeout`. To allow the same on shutdown, raise the service's preshutdown timeout above it, here to six minutes. This takes effect after the next reboot:
The recommended Docker-in-Docker image runs its own Docker daemon for jobs. Jobs never get the host's Docker socket, which is equivalent to root access on the host.
```bash
docker run --privileged -e GITEA_INSTANCE_URL=https://your_gitea.com -e GITEA_RUNNER_REGISTRATION_TOKEN=<your_token> \
The volumes keep the registration and the image cache across container recreation. The image reads more variables, such as `GITEA_RUNNER_LABELS` and `CONFIG_FILE`, see [scripts/run.sh](scripts/run.sh) and the [Docker example](examples/docker/README.md).
| `latest-dind-rootless` | Bundled, runs as UID 1000 for stronger isolation. Needs `--privileged` and [rootless Docker](https://docs.docker.com/engine/security/rootless/) support on the host. Its image cache is in `/home/rootless/.local/share/docker`. |
| `latest` | None. Mount the host's `/var/run/docker.sock`, or set `DOCKER_HOST`. |
Versioned and `nightly` tags take the same suffixes, like `nightly-dind`. The images target Docker, not Podman.
### Example deployments
See the [examples](examples) directory for more deployments, among them systemd, Kubernetes and VMs.
## Configuration
The runner reads an optional YAML file:
```bash
./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
```
`config generate` prints [config.example.yaml](internal/pkg/config/config.example.yaml), which documents every option. Copy the lines you need into your file and uncomment them.
### Editing a config file
`config` edits a file in place and keeps its comments, which suits provisioning scripts:
```bash
./gitea-runner config set runner.capacity 4
./gitea-runner config set runner.envs.MY_VAR value
Keys are dotted YAML paths. Unknown keys and invalid values are refused without touching the file. Without `-c`, it uses `config.yaml` from the working directory, then from the binary's directory.
### Environment variables
Config values can read the runner's environment as `${NAME}`, for example `capacity: ${RUNNER_CAPACITY}`. An unset variable fails the load, and `$${NAME}` stays a literal `${NAME}`.
The override variables of earlier releases, such as `GITEA_RUNNER_CAPACITY`, are gone. Set those options in the file instead.
### Labels
Labels decide which jobs a runner accepts and how it runs them. A job's `runs-on` is matched against the label names, and the first match picks where it runs.
A label is written as `<name>[:<schema>[:<args>]]`, with one of these schemas:
-`docker://<image>` runs the job in a container of `<image>`.
-`kubernetes://<image>` runs it as a Kubernetes pod, see [docs/kubernetes.md](docs/kubernetes.md).
-`host`, the default, runs its steps directly on the runner's machine.
With these labels, `runs-on: ubuntu-latest` runs in a container and `runs-on: macos` on the host:
[gitea/runner-images](https://gitea.com/gitea/runner-images) has suitable images. Jobs matching no label run in `runner.default_image`, or on the host when docker is unavailable.
Host jobs still need a Docker daemon for Docker actions and `docker://` steps. Windows hosts do not support those, as on GitHub. On Windows, a step's background processes end with the job unless it opts out, see [host mode](docs/host-mode.md).
#### Registration vs config labels
Labels are set at registration, with `--labels` or the prompt. To change them later, use one of these, the leftmost set one wins:
```
daemon --labels / GITEA_RUNNER_LABELS > runner.labels in the config file > labels in the .runner file
```
Changed labels are saved to the `.runner` file and sent to Gitea on startup. When `runner.labels` is set, `register` uses it and ignores `--labels`.
### Tool cache
Setup actions like `setup-go` install tools into `RUNNER_TOOL_CACHE`. `runner.tool_cache_mode` selects what backs it:
| Mode | Tool cache |
| --- | --- |
| `none` (default) | Provided by the job image. Versions it lacks are downloaded in every job. |
| `shared` | One volume for all jobs. Only safe with `runner.capacity: 1`. |
To ship tools in a job image, install them to `/opt/hostedtoolcache/<tool>/<version>/<arch>` with an empty `<arch>.complete` file next to it, as the `-full` [runner images](https://gitea.com/gitea/runner-images) do:
```dockerfile
RUNGO=$(curl -fsSL 'https://go.dev/dl/?mode=json'| grep -oP '"version": "\Kgo1\.26\.[0-9]*'| head -1);\
DIR="/opt/hostedtoolcache/go/${GO#go}/x64"&&\
mkdir -p "$(dirname "$DIR")"&&\
curl -fsSL "https://dl.google.com/go/${GO}.linux-amd64.tar.gz"| tar -xz -C /tmp &&\
mv /tmp/go "$DIR"&&\
touch "${DIR}.complete"
```
### Proxy
Set the proxy in the runner's environment, for example with systemd `Environment=` or `docker run -e`:
```sh
http_proxy=http://proxy.example:3128
https_proxy=http://proxy.example:3128
no_proxy=gitea.internal,.example.local
```
The runner uses it and passes it on to jobs. Jobs always reach the cache server, `localhost`, their service containers and a `tcp://` Docker daemon directly. Add Gitea to `no_proxy` if it should be reached directly too.
To change the proxy for jobs, use `runner.envs`, a step's `env:` or `$GITHUB_ENV`. Workflow and job level `env:` have no effect on it.
The Docker daemon pulls images with its own proxy settings. The `dind` images pass the variables above to it, for other daemons see [the Docker documentation](https://docs.docker.com/engine/daemon/proxy/).
A password in the proxy URL is hidden in logs, but every step can read it.
## Jobs
### Service containers
When a service declares a healthcheck, in its image or `options`, the job waits for it to become healthy:
```yaml
services:
postgres:
image:postgres:18
options:>-
--health-cmd pg_isready
--health-interval 5s
--health-retries 10
```
A service that turns unhealthy, or is not healthy within `container.service_ready_timeout`, fails the job. Steps reach a service by its id and port, like `psql -h postgres -p 5432`.
Unlike GitHub, jobs running on the host start no services. Give them a `container:` to use services.
### Docker from a job (`GITEA_DOCKER_WORKSPACE`)
Containers a job starts can bind-mount the workspace by its path, like `docker run -v "$PWD:/src"`. Where the Docker daemon sees another path, for example a remote `DOCKER_HOST`, prefix the bind with `GITEA_DOCKER_WORKSPACE`:
```yaml
volumes:
- ${GITEA_DOCKER_WORKSPACE:-.}/data:/app/data
```
In most Linux container jobs, the containers, networks and volumes a job creates are removed when it ends, `docker compose` services included. Set `container.sweep: false` if jobs deploy services to the runner's Docker daemon.
### Built-in actions (`uses: builtin:<name>`)
`uses: builtin:checkout` checks out a repository without downloading `actions/checkout` or needing Node in the job image, see [docs/builtin-actions.md](docs/builtin-actions.md).
### Caching (`actions/cache`)
Each runner runs its own cache server. As on GitHub, a job restores caches of its own ref, its pull request's base branch and the default branch.
#### Sharing a cache between runners
Run one `gitea-runner cache-server` and point every runner at it with the same secret, for example from `openssl rand -hex 32`:
```yaml
# cache-server.yaml
cache:
dir:/data/actcache
port:8088
external_secret:"<secret>"# or external_secret_file: /path/to/secret
Eviction does not apply to the bucket. Add lifecycle rules that expire old caches and abort incomplete multipart uploads.
#### Eviction
Entries unused for `cache.retention` (default `168h`) are removed. A repository over `cache.repo_size_limit` (default `10GB`) loses its least recently used entries. See [config.example.yaml](internal/pkg/config/config.example.yaml) for more limits.
#### Cache service v2
The runner serves the cache service v2 API used by current `actions/cache` releases. It patches them to use it on Gitea, which also makes the stock `actions/upload-artifact` and `actions/download-artifact` v4 work. Artifact calls then go through the cache server, so jobs must be able to reach it.
Hook scripts run where the job's steps run, before the first step and after the last. They match GitHub's `ACTIONS_RUNNER_HOOK_JOB_STARTED` and `ACTIONS_RUNNER_HOOK_JOB_COMPLETED`.
Use them for per-job setup no workflow should carry, like registry logins or masking runner-wide secrets. They block the job, and a non-zero exit fails it. See [docs/job-hooks.md](docs/job-hooks.md).
### Secret masking
Secrets and `::add-mask::` values are hidden in job logs, [local job logs](#local-job-logs-logjobdir), job summaries and container names. As on GitHub, a job output containing one is dropped with a warning.
## Operation
### Idle cleanup
While idle, the runner removes leftovers of earlier jobs every `runner.idle_cleanup_interval` (default `10m`):
- Workspaces older than `runner.workdir_cleanup_age` (default `24h`), when `container.bind_workdir` is on. This assumes no other runner shares `container.workdir_parent`.
- Unused Docker networks and volumes of jobs the runner did not tear down.
- Kubernetes job pods older than `runner.workdir_cleanup_age`, with their Secrets.
Set either option to `0` to turn cleanup off.
### Post-task script (`runner.post_task_script`)
A host script that runs after each task's cleanup, for housekeeping like Docker pruning. The runner takes no new jobs until it exits or hits `runner.post_task_script_timeout` (default `5m`). Its output goes to the runner log.
On Windows, use an `.exe`, `.bat` or `.cmd` file, PowerShell scripts are not supported. See [docs/post-task-script.md](docs/post-task-script.md).
### Local job logs (`log.job.dir`)
Set `log.job.dir` to keep a copy of every task's log, masked as in Gitea, with the job's result on the last line. Copies are kept for `log.job.retention` (default `168h`), so keep it above `runner.timeout`. Only the runner's user can read them.
### OpenTelemetry (`OTEL_*`)
Set `OTEL_EXPORTER_OTLP_ENDPOINT` in the runner's environment to send job traces and runner metrics to an OpenTelemetry collector. See [docs/telemetry.md](docs/telemetry.md).
<!-- Do not remove this test for UTF-8: if “Ω” doesn’t appear as greek uppercase omega letter enclosed in quotation marks, you should use an editor that supports UTF-8, not this one. -->
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2023 The nektos/act Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package artifactcache provides a cache handler for the runner.
//
// Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server
//
// TODO: Authorization
// TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache
// TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
// insecureTransport is shared, because a transport per request would pool no connections.
varinsecureTransport=&http.Transport{Proxy:http.ProxyFromEnvironment,TLSClientConfig:&tls.Config{InsecureSkipVerify:true}}//nolint:gosec // the runner reaches its instance on the operator's say-so
for_,data:=range[]string{date[:8],s.opts.Region,"s3","aws4_request",fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%x",date,scope,sha256.Sum256([]byte(strings.Join(canonical,"\n"))))}{// key derivation, the last round signs
func(s*serverV4)createArtifact(r*http.Request,reqcreateArtifactRequest)(createArtifactResponse,error){// an exec invocation re-runs its workflows, so it replaces earlier invocations' artifacts but conflicts with its own
w.Header().Set("Content-Disposition","attachment; filename*=UTF-8''"+strings.ReplaceAll(url.QueryEscape(filename),"+","%20"))// RFC 5987 attr-chars only, which the toolkit and mime.ParseMediaType both accept
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
packageartifacts
import(
"crypto/rand"
"encoding/base64"
"encoding/json/v2"
"time"
)
funcLocalRuntimeToken(runstring,lifetimetime.Duration)string{// an unsecured JWT with the claims the toolkit and buildx read, its run hashed for scp and its jti an unguessable bearer
funcFetchAction(ctxcontext.Context,inputNewGitCloneExecutorInput)(string,string,error){// input.Dir is one clone URL and ref's, holding a directory per commit
iferr!=nil||source.Scheme!="http"&&source.Scheme!="https"||hexRefRegex.MatchString(input.Ref)&&!isFullObjectID(input.Ref){// only git tells a short commit from a branch
funcextractTree(ctxcontext.Context,archiveio.Reader,dirstring,stripbool)(string,error){// into dir/<commit of the pax header>, strip dropping the directory wrapping the files
t.Run("keeps an unprefixed archive whole, downloads where insteadOf points and exports a clone without an archive or from an unreachable host",func(t*testing.T){
{name:"directory in target path",links:[][2]string{{"index.js","bundle/nested/index.js"},{"bundle","dist"}},files:[]string{"index.js","bundle/nested/index.js"}},
{name:"parent after directory link",links:[][2]string{{"index.js","bundle/../index.js"},{"bundle","dist/nested"}},files:[]string{"index.js","bundle/index.js"}},
{name:"dangling child of directory link",links:[][2]string{{"lib","dist"},{"dist/optional","missing.js"}},files:[]string{"lib/index.js","lib/nested/index.js"}},
ErrShortRef=errors.New("short SHA references are not supported")
)
// AcquireCloneLock serializes access to dir across runner processes, or within this one where no lock file can be created. Readers of dir must hold it too.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.