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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software: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 · 15d 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.

client/branch.go +18−8

@@ -97,7 +97,7 @@ func (b *Branch) CommitAndPush(noPush bool, reapply func() error) error {
97 97 return nil
98 98 }
99 99
100 const attempts = 8
100 const attempts = 12
101 101 announced := false
102 102 for attempt := 1; ; attempt++ {
103 103 err := b.pushOnce()
@@ -114,9 +114,15 @@ func (b *Branch) CommitAndPush(noPush bool, reapply func() error) error {
114 114 fmt.Fprintln(os.Stderr, "metadata moved underneath us; re-applying on the new tip")
115 115 announced = true
116 116 }
117 // Jitter matters: without it, concurrent writers back off in lockstep and
118 // collide again on every round.
119 time.Sleep(time.Duration(attempt*10+rand.IntN(30)) * time.Millisecond)
117 // Back off exponentially and spread the jitter across the whole interval.
118 // Concurrent writers that sleep the same amount collide again on the next
119 // round, and a linear delay gives the last of a crowd too little room to win
120 // before the budget runs out.
121 wait := 25 * time.Millisecond << min(attempt-1, 4)
122 if wait > 400*time.Millisecond {
123 wait = 400 * time.Millisecond
124 }
125 time.Sleep(wait/2 + time.Duration(rand.IntN(int(wait/2)+1)))
120 126 if ferr := b.Fetch(); ferr != nil {
121 127 return ferr
122 128 }
@@ -139,10 +145,13 @@ func (b *Branch) pushOnce() error {
139 145 }
140 146
141 147 // isLostRace distinguishes a lost race, which is retryable, from a policy
142 // rejection, which is not. A lost race has two shapes: our commit is no longer a
143 // fast-forward, or our push command arrives after someone else moved the ref and
144 // carries a stale old-oid ("incorrect old value provided"), or the branch is
145 // created by two writers at once ("reference already exists").
148 // rejection, which is not. A lost race has several shapes: our commit is no
149 // longer a fast-forward, our push arrives after someone else moved the ref and
150 // carries a stale old-oid ("incorrect old value provided"), the branch is created
151 // by two writers at once ("reference already exists"), or the server is holding
152 // the ref lock for another push while its value changes ("cannot lock ref"). The
153 // last one is why this has to look at more than the obvious rejections: it is a
154 // lock, not a policy, and it clears on its own.
146 155 func isLostRace(err error) bool {
147 156 msg := err.Error()
148 157 for _, marker := range []string{
@@ -150,6 +159,7 @@ func isLostRace(err error) bool {
150 159 "fetch first",
151 160 "incorrect old value provided",
152 161 "reference already exists",
162 "cannot lock ref",
153 163 "stale info",
154 164 "Updates were rejected",
155 165 } {