Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add packaging, a CI workflow, and containerised local runs

3b39a84c913cabe15bfa6a60dd176ff2dd28d241 · Keenan Nemetz · 14d ago · parent 30a72490

Add packaging, a CI workflow, and containerised local runs

A v* tag now builds a .deb and a .rpm for amd64 and arm64 with nfpm and attaches them, with checksums, to the release. The package ships the systemd unit, a config at /etc/vole/vole.toml, and a postinstall that creates the service account and prepares the instance that config names; `vole admin init --config` is what makes that possible, and the binary's version is stamped from the tag instead of the source tree.

scripts/ci.sh becomes .vole/workflows/ci.yaml, so `vole workflow run ci` executes exactly what a push runs, and the Markdown link check moves to scripts/check-links.py. Local runs now always execute in the job image and share ci/container with the runner; --no-container is gone, because a run in a different environment proves nothing about CI.

The runner mounts a per-repository namespace at /cache and sets no variable for it: a job opts in through its image's env or the new job-level env, which is pinned with the queue and delivered with the lease. Whole namespaces are evicted least-recently-written first so a module cache is never half-deleted, and contributor branches get their own namespace so an untrusted tip cannot seed the cache a protected branch builds into.

Running the suite in that container found four bugs worth naming: /tmp was mounted noexec, which broke `go test` and `go run`; the read-only root is gone, because images make toolchain directories world-writable deliberately and the mount set is what protects the host; OpenSSH needs a passwd entry for the job's uid; and a lost metadata push reported as "cannot lock ref" was not classified as a race, so concurrent CLI writes gave up on the first collision. That retry now backs off exponentially with jitter.

docs/workflows.md +51−10

@@ -105,6 +105,26 @@ packages the builds need, plus `/bin/sh` and a POSIX `sleep` (Vole runs shell st
105 105 and keeps the container alive between them). Pin the base image by digest; a
106 106 mutable tag changes what a build means.
107 107
108 ## Environment
109
110 `env` on a job applies to every step, after the image's own, so the job wins:
111
112 ```yaml
113 jobs:
114 test:
115 image: golang:1.27-bookworm
116 env:
117 GOCACHE: /cache/go-build
118 steps:
119 - run: go test ./...
120 ```
121
122 The runner sets `CI=true`, `VOLE_CI=1`, `HOME=/tmp`, `TMPDIR=/tmp`, and
123 `GIT_TERMINAL_PROMPT=0`; a workflow that tries to replace one of those is refused at
124 push time. Everything else is the workflow's. This is how a job opts in to the
125 cache the runner mounts at `/cache`, and how a job using a registry image gets it
126 without an image definition.
127
108 128 ## Contributor branches
109 129
110 130 A push to `refs/heads/contrib/<name>/...` queues no work by itself. Those
@@ -140,17 +160,38 @@ vole workflow run ci -j test # one job in one workflow
140 160 vole workflow run -j test # the job named test in every workflow
141 161 ```
142 162
143 A workflow can be named with or without its extension (`ci` or `ci.yaml`). Steps
144 run in the working tree, in the order the workflow declares them. The command
163 A workflow can be named with or without its extension (`ci` or `ci.yaml`). The
164 command validates the workflow first, with the same checker a push uses, then
145 165 stops at the first failure and exits with that step's status, so it can wrap a
146 script. It validates the workflow first, with the same checker a push uses.
147
148 A local run is a check of the commands, not a rehearsal of the push. It ignores
149 `on.push` filters, `runs-on`, and the job image, and it does not publish artifacts
150 or deploy a site. It sets `CI=true`, `VOLE_CI=1`, and `VOLE_CI_LOCAL=1`, gives
151 each step its declared timeout, and reads no standard input. Jobs share your
152 working tree, so a later job sees files an earlier one wrote; a pushed run starts
153 each job from a clean checkout in its image.
166 script.
167
168 The steps run in the image the job names, not on your machine. That is the point:
169 the answer here is the answer CI gives, so a toolchain difference between "works
170 for me" and a red run does not exist. An `image-file:` definition is built and
171 cached by digest exactly as a runner would build it, and the working tree is bound
172 into the container, so the steps see your uncommitted edits and leave their output
173 where you would expect.
174
175 The run needs a container runtime — Docker, rootless Podman, or nerdctl — and
176 `--container-runtime` selects one (Docker by default). There is no mode that skips
177 the container: a step that runs in a different environment proves nothing about
178 CI. The default network is `bridge`, because a job that cannot fetch a dependency
179 cannot run here either; `--container-network none` matches what a runner does by
180 default. The container runs as your uid and gid; its filesystem is writable but
181 thrown away when the run ends, and `/tmp` is a tmpfs.
182
183 A local run mounts the same job cache as a runner, under the user cache directory
184 (`$XDG_CACHE_HOME/vole/workflows`, usually `~/.cache/vole/workflows`), so a
185 repeated run is as fast here as it is on a runner. As on a runner, the run sets no
186 variables: the image's `env:` (or the step) decides whether `/cache` is used. See
187 [Runners § Caching](runner.md#caching). `--cache-size` caps it (5G by default) and
188 `--no-cache` empties it after every job.
189
190 A local run still ignores `on.push` filters and `runs-on`, publishes no artifacts,
191 and deploys no site. It sets `CI=true` and `VOLE_CI=1`, gives each step its
192 declared timeout, and reads no standard input. Jobs run in declaration order and
193 share the working tree, so a later job sees files an earlier one wrote; a pushed
194 run starts each job from a clean checkout.
154 195
155 196 ## Artifacts
156 197