Skip to content
Try Gitea Cloud ☁️ for 30 days → Accelerate your Development & Deploys!

Gitea Runner 3.0.0 is released

Banner for blog post with title "Gitea Runner 3.0.0 is released"

We are happy to announce the release of Gitea Runner 3.0.0.

This is a runner-side release. It remains wire-compatible with existing Gitea versions; the major version bump reflects three breaking changes rather than a new server dependency.

CVE-2026-73802: Container-escape vulnerability

When privileged mode is disabled, workflow-controlled container options are now sanitized to prevent container-escape attacks. The runner strips options that can be used to break out of container isolation (#1058):

PidMode, IpcMode, UTSMode, CgroupnsMode, UsernsMode, CapAdd, SecurityOpt, Devices, DeviceCgroupRules, DeviceRequests, VolumesFrom, Runtime, CgroupParent, and Sysctls.

These options are preserved when an administrator explicitly enables privileged mode, since that already signals acceptance of expanded host access. Workflows that relied on these options on a non-privileged runner must enable privileged mode on the runner where they are genuinely required.

The headline of this release is cache service v2: standard actions/upload-artifact, actions/download-artifact, and actions/cache now work against a Gitea Runner without the gitea-upload-artifact fork. Around that, 3.0.0 adds encoded-secret masking, job hooks, and proxy propagation, along with a safety guard against two daemons sharing one runner file.

Stock artifact and cache actions — no fork required

Section titled “Stock artifact and cache actions — no fork required”

Gitea Runner now serves the GitHub Actions github.actions.results.api.v1.CacheService (v2) next to the existing v1 cache API, sharing the same store, plus the subset of the Azure blob protocol that the [@actions](https://github.com/actions)/toolkit uploader uses (#1110).

In practice this means the unmodified upstream actions work:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.cache
key: deps-${{ hashFiles('**/lockfile') }}
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

actions/upload-artifact / actions/download-artifact (v4.4.0 and newer) and actions/cache no longer need the gitea-upload-artifact fork or hand-rolled workarounds. The runner patches action bundles at load time to open the GHES gate and read the cache endpoint from ACTIONS_CACHE_URL; this was validated against 118 real bundles spanning 16 actions.

Previously only the verbatim value of a secret was masked, so a secret that leaked through an action which serialized it stayed readable in the logs. Each secret and ::add-mask:: value is now also masked in its encoded forms, matching the value encoders in GitHub’s runner (#1108):

  • JSON — values escaped via toJSON(secrets)
  • Base64 — e.g. secrets carried in an Authorization header
  • URL percent-encoding — secrets embedded in URLs

Encodings that leave the value unchanged are skipped, so a plain token still costs a single replacement pair.

Operators can now run a host script inside the job environment at two lifecycle points, mirroring GitHub’s ACTIONS_RUNNER_HOOK_JOB_STARTED / ACTIONS_RUNNER_HOOK_JOB_COMPLETED (#1111):

runner:
hooks:
job_started: /hooks/started.sh
job_completed: /hooks/completed.sh

job_started runs before the job’s first step and job_completed after its last. Both scan their output for workflow commands, read back $GITHUB_ENV and $GITHUB_PATH, and fail the job on a non-zero exit code. This is useful for warming caches, provisioning credentials, or emitting telemetry around each job.

The runner now sets http_proxy, https_proxy, and no_proxy (lowercase and uppercase) so that everything it controls uses them — jobs, service containers, and Dockerfile action builds (#1112).

Sensible hosts are added to no_proxy for jobs automatically: the cache server, loopback addresses, job service containers, and the Docker daemon (so docker-in-docker keeps working). When you use a dind image, the daemon reads the same proxy variables, and the runner warns at startup if it detects a proxy mismatch.

Setting RUNNER_TOOL_CACHE now actually relocates the tool cache instead of only changing the variable, so ${{ runner.tool_cache }} and the environment variable agree (#1122). The same change lets job and service volumes displace a conflicting mount at the same target, fixes name:/target:ro being mounted read-write at the literal path /target:ro, and makes unknown keys in config.yaml produce a warning instead of silently disappearing.

1. Cache service v2 is enabled by default (#1110). The runner now serves the v2 CacheService out of the box. If you previously deployed the gitea-upload-artifact fork or a custom artifact/cache shim, remove it and switch your workflows to the stock actions/upload-artifact@v4, actions/download-artifact@v4, and actions/cache@v4. If you must keep the previous behavior, disable it with cache.v2: false.

2. Host-escape container options are stripped without privileged mode (#1058). Workflows that set options such as PidMode, CapAdd, SecurityOpt, Devices, or Sysctls in container.options will no longer have them applied on a non-privileged runner. If a workflow genuinely needs these, run it on a runner where the administrator has explicitly enabled privileged mode — the boundary is now enforced instead of quietly bypassed.

3. One runner process per .runner file (#1099). The daemon and register now take a non-blocking advisory lock on a sibling <runner-file>.lock at startup. Two processes that share the same .runner file previously presented identical UUID+token credentials, so Gitea treated them as one runner and they cancelled each other’s jobs. A second process on the same host now fails fast with a clear error. Give each daemon its own .runner file. The OS releases the lock automatically on exit, even on a hard kill, so stale locks do not accumulate. Cross-host setups that share a runner file over NFS still rely on server-side detection in Gitea.

  • Replace gitea/runner:2.x with gitea/runner:3.0.0 in Docker Compose, Kubernetes manifests, and service units.
  • If you use the gitea-upload-artifact fork or artifact/cache workarounds, remove them and move to the stock upstream actions (breaking change 1).
  • Audit workflows that set container.options host-namespace or device flags; enable privileged mode on the runner where they are genuinely required (breaking change 2).
  • Ensure every runner daemon has its own .runner file before rolling out (breaking change 3).
  • If you run behind a proxy, review the automatic no_proxy entries and, for dind, confirm the daemon and runner agree on proxy settings.
  • Test artifact upload/download, cache restore, and cancellation in a staging environment before rolling out broadly.

Pre-built binaries are available from the Gitea Runner downloads page.

The release is also available from the Gitea Runner release page.

Thank you to everyone who contributed code, testing, bug reports, documentation, and feedback since the 2.0.0 release.


  • BREAKING

    • Add cache service v2 next to the v1 cache API so stock actions/upload-artifact/download-artifact (v4.4.0+) and actions/cache work without the fork (#1110)
    • Strip host-escape container options (PidMode, CapAdd, SecurityOpt, Devices, Sysctls, …) when privileged mode is disabled (#1058)
    • Guard against two runner processes sharing one .runner file with an advisory lock (#1099)
  • FEATURES

    • Add job hooks runner.hooks.job_started / runner.hooks.job_completed (#1111)
    • Propagate http_proxy/https_proxy/no_proxy to jobs, services, and Dockerfile action builds (#1112)
    • Mask secrets that reach the log in JSON, base64, and URL-encoded forms (#1108)
  • BUGFIXES

    • Relocate the tool cache with RUNNER_TOOL_CACHE, allow mounting over runner paths, and warn on unknown config keys (#1122)
    • Stop leaking per-job docker networks (#1124)
    • Copy action directories into job containers when the target path is symlinked (e.g. /var/run/run), including on Docker 29.7 (#1130, #1129)
    • Fix panics and enable the forcetypeassert lint (#1123)
    • Update github.com/ulikunitz/xz to v0.5.15 (security) (#1127)
  • FEATURES

    • Run pre-entrypoint and post-entrypoint of docker actions (#1106)
    • Report runner name, environment, workspace, and debug info to jobs (#1105)
    • Support --platform and --pull in container.options (#1104)
    • Read cache.external_secret from a file (#1100)
  • BUGFIXES

    • Escape command data the runner writes itself (#1120)
    • Clean up service containers after failed job setup (#1066)
  • FEATURES

    • Add runner health admission checks (#1090)
    • Report a GitHub-style “Set up job” section (#1089)
  • BUGFIXES

    • Repair the free-disk-space build on FreeBSD (#1098)
    • Stop host-mode jobs from leaking processes on Windows (#1080)
    • Classify a cancelled step as an interruption, not a failure (#1095)
    • Stop racing the daemon when removing containers (#1093)
    • Guard status-check functions against a nil job context (#1092)
  • FEATURES

    • Honor GITEA_RUNNER_LABELS on daemon start with colon-containing labels (#1085)
    • Add --token-file flag to the register command (#1076)
    • Add bug-report subcommand, exec --eventpath, and runner.set_act_env (#1075)
  • BUGFIXES

    • Ignore blank lines and decode UTF-16 in runner env files (#1084)
    • Prevent service containers from overwriting job container credentials (#1083)
    • Support natively typed boolean workflow inputs (#1087)
    • Remove action outputs exceeding size limits (#1070)
  • BUGFIXES
    • Skip service containers with an empty image (#1074)
    • Prevent exponential growth of RunContext masks in composite actions (#1059)
    • Attach task token when cloning actions from a self-hosted instance on a different host (#1056)
    • Install nftables in dind images to silence nft cleanup errors (#1064)