Commit Graph

33 Commits

Author SHA1 Message Date
silverwind
b7a3bf98bc fix: stop leaking per-job docker networks (#1124)
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>
2026-07-30 16:25:47 +00:00
bircni
41c72216bf feat: propagate proxy variables to jobs, services and builds (#1112)
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>
2026-07-30 08:15:48 +00:00
silverwind
3f7fd16ea1 fix: fix panics, enable the forcetypeassert lint (#1123)
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>
2026-07-30 07:09:53 +00:00
silverwind
0192861155 fix: allow relocating the tool cache and mounting over runner paths (#1122)
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>
2026-07-29 21:32:28 +00:00
bircni
e6c7ba3a15 feat: add job hooks (#1111)
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>
2026-07-29 19:16:24 +00:00
silverwind
b4a64b97dd feat: support --platform and --pull in container.options (#1104)
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>
2026-07-24 16:42:30 +00:00
bircni
7bec310002 fix: stop host-mode jobs from leaking processes on Windows (#1080)
## 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>
2026-07-22 15:04:07 +00:00
silverwind
89467c9dd0 fix: stop racing the daemon when removing containers (#1093)
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>
2026-07-21 11:03:15 +00:00
bircni
60177008a5 fix: ignore blank lines and decode UTF-16 in the runner env files (#1084)
Fixes #496
Fixes #552

Reviewed-on: https://gitea.com/gitea/runner/pulls/1084
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
2026-07-15 19:45:50 +00:00
bircni
7e7e3ef1a6 fix: stop service containers from clobbering the job container's credentials (#1083)
Fixes #835
Fixes #643

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1083
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
2026-07-15 06:15:50 +00:00
bircni
3396021e0f test: Enhance Coverage + CI (#1055)
Reviewed-on: https://gitea.com/gitea/runner/pulls/1055
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-07-01 03:26:42 +00:00
Nicolas
007717956a feat: Add optional runner.post_task_script hook after task cleanup (#1026)
- 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>
2026-06-19 19:28:10 +00:00
StarAurryon
2963716953 feat: ipv6 options for network container creation (#1029)
Here is a final proposal for ipv6 enablement on temporary network created by gitea runner

---------

Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Nicolas Schwartz <9308314+StarAurryon@users.noreply.github.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1029
Reviewed-by: Nicolas <bircni@icloud.com>
Co-authored-by: StarAurryon <206206+staraurryon@noreply.gitea.com>
Co-committed-by: StarAurryon <206206+staraurryon@noreply.gitea.com>
2026-06-15 05:05:20 +00:00
Nicolas
3996d6d032 fix(cleanup): kill Unix step process group on cancel to avoid hang (#1025)
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>
2026-06-14 20:52:42 +00:00
Nicolas
205af7cd01 fix: prevent loss of step log output at end of step (#1028)
## 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>
2026-06-14 20:43:19 +00:00
Nicolas
33e6d1d8ff fix(host): bound host-environment cleanup and reclaim leaked scratch dirs (#1024)
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>
2026-06-14 14:14:43 +00:00
Nicolas
c749e52bb7 fix(cleanup): kill Windows step process tree on cancel to avoid hang (#1011)
## 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>
2026-06-02 16:53:27 +00:00
silverwind
f17b6b9fc3 fix(container): re-validate cached container id before reuse (#1003)
`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>
2026-05-29 22:33:44 +00:00
silverwind
abec931d98 fix: restore global docker config dir and socket env in tests (#1004)
`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>
2026-05-29 16:47:10 +00:00
silverwind
270ea41232 fix: matrix-job data races + outputs, leaner offline test suite (#994)
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>
2026-05-29 05:23:10 +00:00
Nicolas
0b9f251b6a fix: deliver cancel ack and reap leftover Windows job processes (#996)
## 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>
2026-05-24 10:01:01 +00:00
Jacob Alberty
cf7e29c10d fix(parse_env_file): support env-file lines larger than 64 KiB (#974)
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>
2026-05-17 13:00:17 +00:00
Nicolas
8a99506fed Fix host cleanup, volume allowlist, cache upload, and action host edge cases (#970)
## Summary
- prevent host-mode execution from deleting caller-owned workdirs
- harden `valid_volumes` checks against `..` and symlink escapes
- return immediately after artifact cache upload write failures
- default implicit remote action clone hosts to `GitHubInstance`/`github.com`

Authored with assistance from OpenAI Codex GPT-5.

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/970
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
2026-05-17 12:53:04 +00:00
silverwind
3c5f03ff8f feat: make pseudo-TTY allocation opt-in (#961)
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>
2026-05-15 18:11:39 +00:00
silverwind
32bed52686 fix(deps): bump docker deps, switch to moby/moby (#943)
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>
2026-05-14 02:29:05 +00:00
silverwind
1e3ab0c40a fix(deps): update mergo to v1.0.2 (now dario.cat/mergo) (#954)
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>
2026-05-10 22:05:05 +00:00
silverwind
75643645f0 feat: remove emojis from runner logging, add Starting job container group (#940)
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>
2026-05-07 19:32:53 +00:00
silverwind
d607f3b342 test: clean up dead/stale fixtures and bump test container images (#932)
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>
2026-05-07 00:11:56 +00:00
silverwind
594c9ade7c Align step failure log output with GitHub Actions (#927)
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>
2026-05-05 20:17:32 +00:00
Nicolas
a22119cf88 fix(host): correct host workspace cleanup on Windows (#883)
## 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>
2026-05-05 18:28:12 +00:00
Lunny Xiao
13dc9386fe Rename act_runner to runner (#850)
## Consumer-facing breaking changes

- **Go module path**: `gitea.com/gitea/act_runner` → `gitea.com/gitea/runner`. Anything importing `act/...` or `internal/...` packages (notably Gitea itself) must update imports.
- **Binary name**: `act_runner` → `gitea-runner`. Wrapper scripts, systemd units, init scripts, and documentation referencing the binary by `act_runner` will break.
- **Docker image**: `gitea/act_runner` → `gitea/runner` (incl. `*-dind-rootless` variants). Users pulling `gitea/act_runner:nightly` etc. will get stale images. Note: the image name is `gitea/runner`, not `gitea/gitea-runner`.
- **Release artifact paths**: S3 directory `act_runner/{{.Version}}` → `gitea-runner/{{.Version}}`, and artifact filenames change with the new project name. Existing download URLs break.
- **Metrics namespace**: changed from `act_runner` to `gitea_runner` (e.g. `act_runner_jobs_total` → `gitea_runner_jobs_total`); existing monitors/dashboards must be updated.
- **ldflags version path**: `gitea.com/gitea/act_runner/internal/pkg/ver.version` → `gitea.com/gitea/runner/internal/pkg/ver.version`. Affects anyone building with custom ldflags.
- **Kubernetes example resource names**: `act-runner` / `act-runner-vol` → `runner` / `runner-vol`. Users who copied the manifests verbatim will see resource churn on apply.
- **s6 service name**: `scripts/s6/act_runner/` → `scripts/s6/gitea-runner/` (image-internal; only matters for downstream image overrides).

Unchanged: YAML config field names, env vars (`GITEA_*`), CLI flags/subcommands, registration file format.
---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/850
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
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>
2026-04-30 20:12:51 +00:00
silverwind
2516573592 chore: clean up nolint directives in act package (#864)
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>
2026-04-29 18:32:55 +00:00
silverwind
fab2d6ae04 Merge gitea/act into act/
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>
2026-04-22 22:29:06 +02:00