anvilsign in

collin/anvil

RenderedSource

1# anvil
2
3A minimal, self-hosted git forge in Rust, built on gix (gitoxide).
4
5## Design rules
6
7- **Pure gitoxide — never the git binary.** The product (server, CI broker,
8 mirroring, deploy image) must not invoke or depend on a `git` executable.
9 When gix lacks a capability, implement the protocol on gix plumbing instead
10 of shelling out — e.g. gix can't push yet, so `anvil-git/src/push.rs` speaks
11 the send-pack wire format itself (with `gitserver-core` covering the server
12 side). Tests may use the git CLI, but only as a fixture/interop check, never
13 on the code path under test.
14
15Dev setup: `git config core.hooksPath .githooks` — the pre-commit hook runs
16`cargo +nightly fmt` (nightly, for the unstable options in rustfmt.toml),
17`cargo sort-derives` (`cargo install cargo-sort-derives`), and
18`cargo clippy --workspace --all-targets -- -D warnings`.
19
20## Viewing attachments referenced in tasks
21
22TODO items and tickets may embed an uploaded image as
23`![...](/{owner}/{repo}/-/attachments/{hash})`. These bytes live outside git
24history, so to actually *see* one, get the bytes and `Read` the file. Two ways:
25
26**Preferred — over git, no credentials.** anvil mirrors every attachment into
27`refs/anvil/attachments` (a flat tree of `<hash> → blob`, off the branch
28namespace so a default pull never drags it down). From a clone, opt in once:
29
30```
31git fetch origin '+refs/anvil/attachments:refs/anvil/attachments'
32git cat-file -p "refs/anvil/attachments:<hash>" > /tmp/att && # then Read /tmp/att
33```
34
35This uses the clone's existing git auth — no PAT — and works offline afterward.
36
37**Fallback — HTTP with a PAT** (no clone, or the ref isn't fetched). Credentials
38live in the git-ignored `.anvil-credentials` at the repo root (`ANVIL_BASE_URL` +
39a read-only `ANVIL_TOKEN`); `source` it, then:
40
41```
42curl -fsS -H "Authorization: Bearer $ANVIL_TOKEN" \
43 "$ANVIL_BASE_URL/{owner}/{repo}/-/attachments/{hash}" -o /tmp/att && # Read it
44```
45
46The PAT is read-only (GET/HEAD only), safe to hold. If neither the ref nor
47`.anvil-credentials` is available, ask the user rather than guessing.
48
49Attachments are often phone screenshots larger than the `Read` tool's 256KB
50cap. Downscale before reading (macOS `sips`):
51
52```
53sips -Z 1100 -s formatOptions 70 /tmp/att --out /tmp/att-small.jpg # then Read that
54```
55
56note that we can't use portless for this app because of a websocets bug with http/2
57
58## Remote runners: where this stands
59
60anvil does **not** execute CI. Runners dial out, claim jobs, and run them on
61their own Docker daemon. Design and rationale: `docs/remote-runners.md`.
62
63M1 is done: `anvil-job` (wire format), `anvil-docker` (connect/ensure_image),
64`anvil-worker` (the runner binary), five endpoints under `/-/runner/`,
65in-memory leases, and no Docker socket on the deployed container.
66
67**Invariants to not break.** These are the reasons the split is shaped the way
68it is, and each is easy to undo by accident:
69
70- The runner never parses `.anvil/ci.yml`. Image resolution, the allowlist
71 check and script assembly happen in `build_job`, so a runner cannot widen
72 what it is permitted to run.
73- `store_artifact` stays server-side. The runner uploads a raw tar; `browse`
74 decides whether that becomes a servable directory tree, which is anvil's
75 call, not a runner's.
76- A job's secrets live on its lease, not the vault, once dispatched.
77 `Vault::take` fails after the repo's unlock TTL lapses, so re-reading at
78 finish time would silently skip log masking on exactly the long runs whose
79 logs most need it.
80- `anvil-job` depends on serde and nothing else. It exists so the runner does
81 not link toasty, SQLite and gix.
82
83**Next, in order:**
84
851. **M2, platform.** The plumbing is live end to end (`JobSpec.platform` reaches
86 both bollard option structs; runners advertise their native platform on
87 claim). What is missing is a source: a `platform:` key in `.anvil/ci.yml`,
88 probably a `[ci] platform` default, and routing a job to a runner that has
89 that architecture natively. This is what stops arm64 runners testing code
90 that ships as amd64.
912. **M3, the deploy agent.** Image builds cannot be CI jobs, because job
92 containers get no Docker socket by design. `docker build --platform
93 linux/amd64` plus `docker push` belongs to a separate process on the build
94 host at a different trust level, triggered by the existing `deploy_webhook`.
95 Folding it into the dial-out channel as a privileged "publish" job kind,
96 authorized by the `is_deploy_target` check that already scopes CD to one
97 repo, would remove the last inbound path to the build host.
983. **CI concurrency.** "One job at a time" was a property of the old in-process
99 loop and is gone. Nothing bounds in-flight jobs now beyond how many runners
100 exist. Needs a `max_concurrent` equivalent.
1014. **Per-runner credentials.** One shared `[ci] runner_token` means one
102 revocation for every runner, no `last_used_at`, and any holder can claim any
103 job and receive its secrets. Wants the API-token write scope first (see
104 TODO.md).
1055. **Live logs.** The result POST is a single write, matching the old
106 behaviour. Streaming needs chunked append with offsets and a UI that
107 tolerates gaps. Newly worth doing now that a producer exists.
108
109**Operationally:** nothing runs until a runner is started and `[ci]
110runner_token` is set; queued runs just sit, with a warning logged at startup.
111Agent sessions still drive Docker locally and are the one thing the dropped
112socket mount gives up. They are off by default.
113
114Current status, resume notes, the agreed next steps (a/b/c), and the roadmap live
115in the TODO. Read it first:
116
117@TODO.md