Files
act_runner/docs/post-task-script.md
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

7.2 KiB

Post-task script

The post-task script is an optional host hook that runs once after every task, after the runner has already finished its normal per-task cleanup. Typical uses include pruning Docker images, vacuuming ephemeral disks, or resetting VM state between jobs.

It is configured under runner.post_task_script in the runner YAML config (see config.example.yaml).

When it runs

For each task, execution order is:

  1. Workflow runs (steps, actions, containers).
  2. In-job cleanup (action post: steps, container stop/remove).
  3. Job outputs are reported to Gitea.
  4. Bind-workdir workspace removal, when container.bind_workdir is enabled.
  5. Post-task script (this hook).
  6. Final task acknowledgement to Gitea (reporter.Close()).

The script is additive: it does not replace any built-in cleanup. When container.bind_workdir is enabled, the task workspace directory has usually already been deleted before the script starts. GITEA_WORKSPACE is still set to the path the job used, for reference.

Runner stays offline until the script finishes

This is the most important operational detail.

When the post-task script starts, the runner stops sending task heartbeats to Gitea (the same mechanism used during cancel/cleanup). From Gitea's perspective, the runner is not available for new work until:

  1. The script exits (success or failure), and
  2. The runner sends the final task flush to Gitea.

While the script runs:

  • Gitea will not assign another task to this runner for the current job slot (heartbeats are stopped).
  • The runner capacity slot stays occupied locally — with capacity: 1, the poller will not start another task until the script completes.
  • Runner shutdown (shutdown_timeout) counts this phase as part of the in-flight task; a long or stuck script delays graceful shutdown.

If the script never exits, the runner remains in this state until runner.post_task_script_timeout elapses (default 5 minutes when a script is configured). The runner then kills the script process and proceeds to the final acknowledgement. Until that timeout fires, the runner effectively stays offline.

Set post_task_script_timeout to a value that matches how long your housekeeping is allowed to take — not how long you wish it could take. Prefer short, bounded scripts.

Recommendations

  • Keep scripts fast and bounded (seconds, not minutes).
  • Avoid interactive prompts, blocking network calls without timeouts, or waiting on user input.
  • Use idempotent operations (the script may run after success, failure, or cancellation).
  • Test failure modes: hung script, non-zero exit, missing executable.
  • Watch the runner process log for script output (it is not written to the Gitea job log).
  • On shutdown, ensure scripts respond to process termination within post_task_script_timeout.

Configuration

runner:
  # Path to an executable on the host. Empty or omitted disables the hook.
  post_task_script: /usr/local/bin/gitea-post-task.sh

  # Hard limit on script runtime. Default when post_task_script is set: 5m.
  # If the script exceeds this, it is killed and the runner continues.
  post_task_script_timeout: 2m
Option Default Description
runner.post_task_script (disabled) Host path to the script or binary. Relative paths are resolved from the runner process working directory.
runner.post_task_script_timeout 5m (only when script is set) Maximum time the script may run before the runner kills it and moves on.

The script must be executable on the host (shebang on Linux/macOS, or a native .exe / .bat / .cmd on Windows). PowerShell (.ps1) is not supported yet as the value of post_task_script; the runner executes the configured path directly and does not invoke powershell.exe for you.

gitea-runner exec does not load runner YAML and will not run this hook.

Environment variables

The script receives runner.envs / runner.env_file values plus:

Variable Description
GITEA_TASK_ID Numeric task ID.
GITEA_RUN_ID Workflow run ID, when provided by the server.
GITEA_REPOSITORY Repository slug (owner/name).
GITEA_WORKSPACE Workspace path used for the job (may already be deleted).
GITEA_JOB_RESULT success, failure, cancelled, skipped, or unknown.

The script environment is not a full copy of the job container environment. System variables such as PATH are only present if you define them in runner.envs or runner.env_file.

Output and errors

  • Stdout/stderr are written to the runner process log (logrus), prefixed with post-task script stdout: / post-task script stderr:.
  • Non-zero exit codes are logged as warnings only. They do not change the job result already reported to Gitea.
  • Timeouts and start failures are logged as warnings; the runner still completes the task acknowledgement.

Interaction with other timeouts

Timeout Effect on post-task script
runner.post_task_script_timeout Kills the script if it runs too long. This is the only timeout that bounds the script.
runner.timeout Caps the task up to the script. The script detaches from the task deadline, so a job near the runner timeout limit does not cut the script short — it still gets its full post_task_script_timeout.
runner.shutdown_timeout On SIGINT/SIGTERM, bounds how long the runner waits for the task to finish. The post-task script detaches from cancellation, so it is not interrupted by this window and may extend shutdown until its own post_task_script_timeout elapses.

Examples

Linux — prune dangling Docker resources

/usr/local/bin/gitea-post-task.sh:

#!/bin/sh
set -eu
docker image prune -f
docker builder prune -f --filter 'until=24h'

config.yaml:

runner:
  post_task_script: /usr/local/bin/gitea-post-task.sh
  post_task_script_timeout: 3m

Windows — batch file (.cmd)

Use a .cmd or .bat file. PowerShell scripts are not supported yet as post_task_script; call PowerShell from a batch wrapper if needed:

C:\gitea-runner\scripts\post-task.cmd:

@echo off
docker image prune -f
runner:
  post_task_script: C:\gitea-runner\scripts\post-task.cmd
  post_task_script_timeout: 3m

PowerShell workaround until native .ps1 support exists:

C:\gitea-runner\scripts\post-task.cmd:

@echo off
powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0post-task.ps1"

Windows notes

  • Supported as post_task_script: .exe, .bat, .cmd.
  • Not supported yet: .ps1 as the configured path (use a .cmd wrapper; see above).
  • .sh files require a Unix shell on the PATH unless you point post_task_script at the interpreter.
  • Use backslashes or forward slashes in YAML paths; both work in Go on Windows.

See also

  • Configuration — generating and loading config.yaml
  • config.example.yaml — all runner options
  • Bind-workdir idle cleanup (runner.workdir_cleanup_age) — separate from this hook; runs only when the runner is idle