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.
Docs: rewrite the README and organize the guides
520470e19cd906bd6fff987e2d132d793ce56486 · Keenan Nemetz · 16d ago · parent c5902451
Docs: rewrite the README and organize the guides Replace the long README with a landing page, split feature details into focused guides, and remove the roadmap and spike notes. Update comments that referenced them.
docs/ci.md +211
| @@ -0,0 +1,211 @@ | |||
| 1 | # CI | ||
| 2 | |||
| 3 | CI should run where the work is, not only where a vendor says it can. Vole sends | ||
| 4 | jobs over SSH to runners you control: a server, a spare desktop, or a machine in | ||
| 5 | the next room. The runner executes your steps and reports the result, which is | ||
| 6 | stored with the repository. | ||
| 7 | |||
| 8 | ## Add a workflow | ||
| 9 | |||
| 10 | Workflows live in `.vole/workflows/*.yaml`. When you push a new workflow, Vole | ||
| 11 | checks it before accepting the push, so a broken workflow is rejected early. | ||
| 12 | |||
| 13 | ```yaml | ||
| 14 | version: 1 | ||
| 15 | on: | ||
| 16 | push: | ||
| 17 | branches: [main] | ||
| 18 | jobs: | ||
| 19 | build: | ||
| 20 | runs-on: [linux, arm64] # only a runner with both labels takes this job | ||
| 21 | steps: | ||
| 22 | - name: test | ||
| 23 | run: go test ./... | ||
| 24 | - name: build | ||
| 25 | run: make dist | ||
| 26 | artifacts: | ||
| 27 | - path: dist/* | ||
| 28 | pages: | ||
| 29 | path: site/ # publish this directory as the repository's site | ||
| 30 | ``` | ||
| 31 | |||
| 32 | Triggers work like this: | ||
| 33 | |||
| 34 | - `on.push.branches` limits a workflow to certain branches. | ||
| 35 | - `on.push.tags` limits it to tag pushes. | ||
| 36 | - A workflow with neither runs on every branch. | ||
| 37 | - A workflow that names `tags:` runs only on tags. | ||
| 38 | |||
| 39 | When a push starts a job, Vole remembers the exact commit and the exact workflow | ||
| 40 | file. Editing the workflow later does not change a job that is already queued. | ||
| 41 | |||
| 42 | You can check workflows locally before pushing: | ||
| 43 | |||
| 44 | ```sh | ||
| 45 | vole workflow check | ||
| 46 | vole workflow list | ||
| 47 | ``` | ||
| 48 | |||
| 49 | ## Start a runner | ||
| 50 | |||
| 51 | Start a runner anywhere it can reach the Vole server: | ||
| 52 | |||
| 53 | ```sh | ||
| 54 | vole runner --remote ssh://git@host:2222 | ||
| 55 | vole runner --remote ssh://git@host:2222 --once # run one job, then exit | ||
| 56 | ``` | ||
| 57 | |||
| 58 | You can describe the machine and limit which repositories it serves: | ||
| 59 | |||
| 60 | ```sh | ||
| 61 | vole runner --remote ssh://git@host:2222 --label linux --label arm64 | ||
| 62 | vole runner --remote ssh://git@host:2222 --repos demo --repos docs | ||
| 63 | ``` | ||
| 64 | |||
| 65 | - `--label` says what the machine is. A job is sent only to a runner that has | ||
| 66 | every label in its `runs-on`. A job with no `runs-on` can run anywhere. | ||
| 67 | - `--repos` narrows what the account is already allowed to run. It cannot grant | ||
| 68 | access to a repository. | ||
| 69 | - `--dir` chooses where checkouts are kept. | ||
| 70 | |||
| 71 | All runners share one pool. An idle runner waits for work instead of asking | ||
| 72 | repeatedly, and Vole gives the next job to the repository with the least work in | ||
| 73 | progress. Set `max_jobs_per_repo` when you need a hard limit per repository. | ||
| 74 | |||
| 75 | The `/runners` page shows who is connected, what each runner is building, and how | ||
| 76 | much work is queued. | ||
| 77 | |||
| 78 | ## Who can run jobs and report results | ||
| 79 | |||
| 80 | A job runs repository code, so Vole uses a runner allowlist for both taking a job | ||
| 81 | and reporting its result: | ||
| 82 | |||
| 83 | ```sh | ||
| 84 | vole acl runners demo ci-runner-1 # replace the list | ||
| 85 | vole acl runners demo --add ci-runner-2 # add one account | ||
| 86 | vole acl runners demo --remove ci-runner-1 # remove one account | ||
| 87 | vole acl runners demo --clear # back to admins only | ||
| 88 | ``` | ||
| 89 | |||
| 90 | Admins always count. For everyone else, a writer running CI on their laptop does | ||
| 91 | not turn a required check green. The result is still recorded, and the run page | ||
| 92 | says whether it counts. | ||
| 93 | |||
| 94 | When a job finishes, the runner uploads its log, writes a record under | ||
| 95 | `runs/<run id>/<attempt>/`, and pushes the record together with artifact pointers | ||
| 96 | and any release pointers. Vole checks that the account is allowed, that the record | ||
| 97 | names that account, and that it describes the job the account was given. The run | ||
| 98 | page names the account and links to the commit that holds the record. | ||
| 99 | |||
| 100 | The runner did the work, so the runner reports the result. The SSH key behind the | ||
| 101 | push identifies the runner, and `data/audit.log` records the decision. | ||
| 102 | |||
| 103 | ## Logs | ||
| 104 | |||
| 105 | The runner uploads the log before reporting the exit code. Vole does not stream | ||
| 106 | logs: a log assembled by the server would not be the runner's own record. | ||
| 107 | |||
| 108 | ```text | ||
| 109 | /r/<repo>/runs/<id>/log # latest attempt | ||
| 110 | /r/<repo>/runs/<id>/log?attempt=N # a particular retry | ||
| 111 | ``` | ||
| 112 | |||
| 113 | If a runner is killed, its attempt has no log. | ||
| 114 | |||
| 115 | ## Artifacts | ||
| 116 | |||
| 117 | Use `artifacts:` to list files that a build should publish. Vole expands the | ||
| 118 | patterns in the checkout after the steps pass, then uploads the files to the same | ||
| 119 | storage used by LFS and CI logs. | ||
| 120 | |||
| 121 | ```yaml | ||
| 122 | artifacts: | ||
| 123 | - path: dist/* | ||
| 124 | ``` | ||
| 125 | |||
| 126 | Artifacts appear on the run page with a download link: | ||
| 127 | |||
| 128 | ```sh | ||
| 129 | curl -OJ http://host/r/demo/runs/01M5.../artifacts/tool-linux | ||
| 130 | sha256sum tool-linux | ||
| 131 | ``` | ||
| 132 | |||
| 133 | A pattern that matches nothing fails the job. A build that stopped producing the | ||
| 134 | declared file is exactly what the declaration is meant to catch. If two matches | ||
| 135 | have the same base name, Vole refuses them so nothing is published under an | ||
| 136 | unexpected name. | ||
| 137 | |||
| 138 | A run artifact belongs to its run. It is not a release artifact. Moving one into a | ||
| 139 | release is a separate step (`vole release attach`). Run artifacts are kept for as | ||
| 140 | long as the run exists. | ||
| 141 | |||
| 142 | ## Build a release in CI | ||
| 143 | |||
| 144 | A tag push can build a release. Mark the artifacts `release: true`: | ||
| 145 | |||
| 146 | ```yaml | ||
| 147 | version: 1 | ||
| 148 | on: | ||
| 149 | push: | ||
| 150 | tags: ['v*'] | ||
| 151 | jobs: | ||
| 152 | release: | ||
| 153 | steps: | ||
| 154 | - name: build | ||
| 155 | run: make dist | ||
| 156 | artifacts: | ||
| 157 | - path: dist/* | ||
| 158 | release: true | ||
| 159 | ``` | ||
| 160 | |||
| 161 | ```sh | ||
| 162 | git tag -a v1.0.0 -m "first release" # needs notes and a tagger | ||
| 163 | git push origin refs/tags/v1.0.0 # the runner builds and publishes | ||
| 164 | ``` | ||
| 165 | |||
| 166 | The record on the `vole` branch names the build (`release: v1.0.0 artifacts from | ||
| 167 | run 01M5...`), so `git log vole` shows which run shipped the files. A lightweight | ||
| 168 | tag is refused: it has no release notes and no tagger. A tag run never counts as a | ||
| 169 | branch's required check, even if the tag points at that branch's tip. | ||
| 170 | |||
| 171 | See [Releases](releases.md) for the release page and artifact rules. | ||
| 172 | |||
| 173 | ## Publish a website in CI | ||
| 174 | |||
| 175 | `pages:` publishes a directory as the repository's website, using the same rules | ||
| 176 | as `vole pages deploy`: the branch contains exactly those files, so a page removed | ||
| 177 | from the build stops being served. | ||
| 178 | |||
| 179 | ```yaml | ||
| 180 | pages: | ||
| 181 | path: site/ | ||
| 182 | ``` | ||
| 183 | |||
| 184 | The commit names the build (`site: main from run 01M5... (6beaa53)`), so | ||
| 185 | `git log pages` shows what put that tree there. | ||
| 186 | |||
| 187 | Vole refuses the deploy if the run is no longer the newest commit on its branch. | ||
| 188 | An older build finishing late must not silently roll the site back. The run log | ||
| 189 | explains why: | ||
| 190 | |||
| 191 | ```text | ||
| 192 | this run built f1f415ec but main is at 6beaa530 now; the site was not deployed | ||
| 193 | ``` | ||
| 194 | |||
| 195 | Only one job per workflow can publish the site. The branch name is set by the | ||
| 196 | instance (`pages_ref`), not by the workflow. | ||
| 197 | |||
| 198 | ## How the queue works | ||
| 199 | |||
| 200 | - Finished runs are stored as records on the `vole` branch. `data/runs.db` only | ||
| 201 | stores work that has not happened yet. | ||
| 202 | - A push normally reaches the queue in under a second. Every 30 seconds Vole also | ||
| 203 | compares each branch and tag with the last tip it queued, so a missed | ||
| 204 | notification still produces a run. | ||
| 205 | - Pushing again cancels queued runs for the commit it replaced. It never cancels a | ||
| 206 | run that is already in progress. | ||
| 207 | - If a runner disappears, its job is requeued. After five attempts the job fails | ||
| 208 | instead of cycling forever. | ||
| 209 | - Deleting `data/runs.db` loses attempts that were in flight. Current branch and | ||
| 210 | tag tips are queued again from Git, and `vole index rebuild` restores finished | ||
| 211 | history from the records on each repository's `vole` branch. | ||