Run scripts when git fetch finds new commits to specified git branches.
Two users matter:
[general].user— the user defined in the general section of the jobs.toml file. This is the user that the fetch-runner process runs under (alsoUser=in the systemd unit). The fetch-runner script refuses to start if the running uid doesn't match this user.[[jobs]].run_as— defined for each job described in jobs.toml. This user owns the job's repo and runs its git ops and deploy script. If this value is not defined, it will default to[general].user. When run_as is defined, fetch-runner dispatches everything viasudo -n -u <run_as>and a sudoers rule must allow it. A sudoers rule can be generated for each run_as user by runningfetch-runner --print-sudoers <jobs.toml>.
Convention assumed throughout the docs is that each repo lives at
/srv/<run_as>/<repo_name>/, owned <run_as>:<run_as> mode 0755.
That gives every path under /srv/<run_as>/ a single owner and lets
[general].user traverse with just search permission.
As [general].user (we use fetch-runner below):
uv tool install git+https://github.com/BYU-ODH/fetch-runnerNote the installed executable path (typically
/home/[general].user/.local/bin/fetch-runner).
As a user with sudo capability:
Clone the fetch-runner repository
git clone https://github.com/BYU-ODH/fetch-runner.gitNow, as the run_as user, copy the deploy.sh script into the application directory.
For a job with run_as = "app1" deploying the api repo:
sudo -u app1 cp /path/to/fetch-runner/examples/deploy.sh /srv/app1/api/deploy.sh
sudo -u app1 chmod +x /srv/app1/api/deploy.shReplace every deploy-user in the script with the job's run_as user
(app1 here). The guard block at the top refuses to run as any other
user. Regenerate it for a different user with:
fetch-runner --print-guard app1Commit the script to the app's repo so deploys are version-controlled.
cp /path/to/fetch-runner/examples/jobs.toml /home/[general].user/jobs.tomlPer [[jobs]]:
name— label shown in logspath— absolute repo path, owned and writable byrun_asbranch— branch to watchscript— absolute script pathargs— optional list of argv strings passed to the script (each a single token; no whitespace or shell metacharacters). Included in the generated sudoers rule, so regenerate after any change.run_as— optional; defaults to[general].usertimeout_seconds— optional script timeout
Validate without starting:
fetch-runner --check /home/fetch-runner/jobs.tomlsudo cp /path/to/fetch-runner/examples/fetch-runner.service \
/etc/systemd/system/fetch-runner.serviceIn the CUSTOMIZE block of the new fetch-runner system unit service, set:
User/Groupto[general].userExecStartto the binary path from step 1 and the path to your jobs.toml config fileReadWritePathsto every directory any child process (such as git) writes to — including the root directory of each repo (sudo'd git is still inside the fetch-runner unit service's filesystem sandbox). If all of your repos are inside a single directory, (/srv, for example) you can setReadWritePathsto only this directory and all child directories will be readable/writeable as well.
The example unit omits NoNewPrivileges= and RestrictSUIDSGID=
because they block sudo's setuid. The sudoers fragment (step 5) is what
bounds the privilege. If every job uses run_as = [general].user, you
can re-enable both.
5. Install the sudoers fragment (only if any job sets a run_as user that is different from [general].user)
fetch-runner --print-sudoers /home/fetch-runner/jobs.toml \
| sudo tee /etc/sudoers.d/fetch-runner > /dev/null
sudo chmod 0440 /etc/sudoers.d/fetch-runner
sudo visudo -cf /etc/sudoers.d/fetch-runner # syntax checkNote: You may need to run these steps separately in order to create the /etc/sudoers.d/fetch-runner file. This can be done by copying the output of the fetch-runner --print-sudoers command and pasting the result into the /etc/sudoers.d/fetch-runner file. The other commands can be followed as written.
Re-run after any jobs.toml change. The git rule is intentionally not
arg-restricted: running git as run_as is no broader than what the
deploy-script rule already grants.
sudo systemctl daemon-reload
sudo systemctl enable --now fetch-runnerExisting configs without run_as keep working unchanged — sudo is
skipped entirely. To split, add run_as per job, update each script's
guard for the new user, regenerate the sudoers fragment, reload.
sudo systemctl status fetch-runner
sudo journalctl -u fetch-runner -f
sudo journalctl -u fetch-runner -bsudo: a password is required→ sudoers fragment is missing or stale; re-run step 5.fetch-runner-guard: refusing to run as <user>→ the script's guard names a user that doesn't match the job'srun_as; regenerate withfetch-runner --print-guard <run_as>.