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.
forge/instance.go +25−6
| @@ -172,16 +172,35 @@ func Init(dir string) (string, error) { | |||
| 172 | 172 | if err := os.MkdirAll(abs, 0o755); err != nil { | |
| 173 | 173 | return "", err | |
| 174 | 174 | } | |
| 175 | path := filepath.Join(abs, "vole.toml") | ||
| 176 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { | ||
| 177 | // Write an absolute data_dir so `serve` works from any working directory. | ||
| 175 | path, err := InitAt(filepath.Join(abs, "vole.toml")) | ||
| 176 | if err != nil { | ||
| 177 | return "", err | ||
| 178 | } | ||
| 179 | return filepath.Dir(path), nil | ||
| 180 | } | ||
| 181 | |||
| 182 | // InitAt prepares the instance a config file describes and returns that file's | ||
| 183 | // absolute path. A missing config is created beside its own data directory, so | ||
| 184 | // `init --dir` keeps its one-directory shape. An existing config is used as | ||
| 185 | // written, which is how a package ships /etc/vole/vole.toml and still has init | ||
| 186 | // create the repositories and roster database it names. | ||
| 187 | func InitAt(cfgPath string) (string, error) { | ||
| 188 | abs, err := filepath.Abs(cfgPath) | ||
| 189 | if err != nil { | ||
| 190 | return "", err | ||
| 191 | } | ||
| 192 | // An absolute data_dir lets `serve` run from any working directory. | ||
| 193 | if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { | ||
| 194 | return "", err | ||
| 195 | } | ||
| 196 | if _, err := os.Stat(abs); errors.Is(err, os.ErrNotExist) { | ||
| 178 | 197 | body := strings.Replace(config.Sample(), | |
| 179 | `data_dir = "./data"`, fmt.Sprintf("data_dir = %q", filepath.Join(abs, "data")), 1) | ||
| 180 | if err := os.WriteFile(path, []byte(body), 0o644); err != nil { | ||
| 198 | `data_dir = "./data"`, fmt.Sprintf("data_dir = %q", filepath.Join(filepath.Dir(abs), "data")), 1) | ||
| 199 | if err := os.WriteFile(abs, []byte(body), 0o644); err != nil { | ||
| 181 | 200 | return "", err | |
| 182 | 201 | } | |
| 183 | 202 | } | |
| 184 | cfg, err := config.Load(path) | ||
| 203 | cfg, err := config.Load(abs) | ||
| 185 | 204 | if err != nil { | |
| 186 | 205 | return "", err | |
| 187 | 206 | } | |