Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ target
**/node_modules
web/dist
.env*
deploy/.env
**/.env
**/.env.*
*.senv
.senv
.dev
Expand Down
22 changes: 16 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,31 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: useblacksmith/setup-docker-builder@v1
- name: Validate the WAL Compose stack
run: |
cp deploy/walgit.env.example deploy/.env
sed -i 's/^WALGIT_TOKEN=$/WALGIT_TOKEN=ci/' deploy/.env
sed -i 's/^WALGIT__STORE__BUCKET=$/WALGIT__STORE__BUCKET=ci/' deploy/.env
docker compose -f deploy/compose.yaml config -q
rm -f deploy/.env
- name: Build release image without publishing
uses: useblacksmith/build-push-action@v2
with:
context: .
file: Containerfile
file: Dockerfile
platforms: linux/amd64
push: false
load: true
provenance: false
build-args: WALGIT_BUILD_SHA=${{ github.sha }}
build-args: |
WALGIT_BUILD_SHA=${{ github.sha }}
WALGIT_CONFIG_FILE=deploy/walgithub.toml
WALGIT_HTTP_PORT=8097
tags: walgithub:smoke
- name: Start S3-compatible test store
run: |
docker compose up -d --wait rustfs
docker compose run --rm create-bucket
docker compose -f deploy/compose.rustfs.yaml up -d --wait rustfs
docker compose -f deploy/compose.rustfs.yaml run --rm create-bucket
- name: Start packaged facade
run: |
docker run -d --name walgithub-smoke --network host \
Expand All @@ -156,7 +166,7 @@ jobs:
run: |
mkdir -p smoke-logs
docker logs walgithub-smoke > smoke-logs/facade.log 2>&1 || true
docker compose logs rustfs > smoke-logs/store.log 2>&1 || true
docker compose -f deploy/compose.rustfs.yaml logs rustfs > smoke-logs/store.log 2>&1 || true
- uses: actions/upload-artifact@v4
if: always()
with:
Expand All @@ -167,4 +177,4 @@ jobs:
if: always()
run: |
docker rm -fv walgithub-smoke || true
docker compose down -v
docker compose -f deploy/compose.rustfs.yaml down -v
9 changes: 8 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ machines whose "disk" is 20 GiB of tmpfs, next to a long tail of small repositor
| `web/README.md` | Frontend engineers changing the React SPA, Vite build, SDK adapter, static assets, loading states. |
| `walgit.example.toml` | Every config key with its default and a comment. Change it with the code. |
| `walgit.standalone.toml` | The one-machine shape: `walgit-server --config walgit.standalone.toml` → `https://walgit.localhost:8080/`. |
| `docs/HOSTING.md` | Anyone running walgit as a service: the image, `deploy/compose.yaml` (WAL-Git + WAL-Github), auth/TLS, roles and placement, upgrades, monitoring. |
| `deploy/nginx.conf.example` | An optional nginx in front; documents the `X-Accel-Redirect` byte-offload contract. |
| `Containerfile`, `flake.nix` | An OCI image; a Nix package, image and devshell. |
| `Dockerfile`, `deploy/compose.yaml`, `deploy/walgit.env.example`, `deploy/compose.rustfs.yaml` | The image (both services via build args); the WAL stack; its shared environment; the optional, separate rustfs dev store. `flake.nix` is the Nix package, image and devshell. |

---

Expand Down Expand Up @@ -451,6 +452,12 @@ long-lived access keys. Explicit pairs still support local object stores; partia
**D45 — Facade runtime configuration (2026-09-09).** The walgithub image packages
`deploy/walgithub.toml`; facade behavior changes ship with the image. The deployment supplies only
the store bindings (bucket, region, public origin) and secrets through `WALGIT__` overrides.
**(2026-10-31)** One `Dockerfile` builds both shapes: the default bakes `deploy/host.toml` for
WAL-Git, while WAL-Github selects its config with
`--build-arg WALGIT_CONFIG_FILE=deploy/walgithub.toml --build-arg WALGIT_HTTP_PORT=8097`. The WAL
deployment (`deploy/compose.yaml`, `docs/HOSTING.md`) runs both services and deliberately starts no object
store; `deploy/compose.rustfs.yaml` is a separate, optional dev store. The `Containerfile` name is
gone; the build file is `Dockerfile`.

**D46 — Dynamic GitHub App integrations (2026-09-09).** Replaces D42's global GitHub callback/secret/
installation config with `github/integrations.json`, a CAS'd bucket registry managed by the private
Expand Down
48 changes: 31 additions & 17 deletions Containerfile → Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# walgithub image — the GitHub Enterprise Server facade for local development and E2Es.
# walgit — one binary in front of an object store.
#
# podman build -t walgit -f Containerfile .
# podman run --rm -p 127.0.0.1:8097:8097 \
# -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY \
# -e WALGIT__STORE__BUCKET=your-bucket \
# -v ./walgit.toml:/etc/walgit/walgit.toml:ro \
# docker build -t walgit .
# docker run --rm --stop-timeout 60 -p 127.0.0.1:8080:8080 \
# -e WALGIT__STORE__BUCKET=my-bucket \
# -e WALGIT_TOKEN="$(openssl rand -hex 32)" \
# -v walgit-cache:/var/lib/walgit \
# walgit
#
# The image carries git (upload-pack, repack, bundle, index-pack run as subprocesses),
# git-lfs, CA certificates and tini. Config comes from /etc/walgit/walgit.toml or
# WALGIT__SECTION__KEY environment overrides; the local cache (materialized repositories,
# a self-signed TLS cert) lives under /var/lib/walgit and can be wiped at any time — the
# bucket is the only durable state. flake.nix provides separate standalone walgit packaging.
# The image bakes `deploy/host.toml` at /etc/walgit/walgit.toml and takes the bucket, token and
# every other setting from the environment (`WALGIT__SECTION__KEY`). docs/HOSTING.md is the host
# guide; the Compose stack is deploy/compose.yaml (WAL-Git + WAL-Github).
#
# The runtime image carries git (upload-pack, repack, bundle, index-pack run as subprocesses),
# git-lfs, CA certificates and tini. /var/lib/walgit is a disposable cache: materialized packs,
# a self-signed certificate, task logs. The bucket is the only durable state.
#
# The facade E2E image (docs/GITHUB.md) is this same build with a different baked config:
# docker build --build-arg WALGIT_CONFIG_FILE=deploy/walgithub.toml \
# --build-arg WALGIT_HTTP_PORT=8097 -t walgithub:smoke .

# ---- 1. web UI (embedded into the binary at compile time) ---------------------------
FROM docker.io/library/node:24-bookworm-slim AS web
Expand All @@ -23,7 +28,7 @@ RUN pnpm install --frozen-lockfile
COPY web/ ./
RUN pnpm run build && test -f dist/index.html && test -f dist/repos.js

# ---- 2. rust build ------------------------------------------------------------------------
# ---- 2. rust build ------------------------------------------------------------------
FROM docker.io/library/rust:1.97-bookworm AS build
RUN apt-get update && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev pkg-config cmake perl python3 \
&& rm -rf /var/lib/apt/lists/*
Expand All @@ -39,7 +44,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
&& install -D target/release/walgit /out/bin/walgit \
&& install -D target/release/walgit-server /out/bin/walgit-server

# ---- 3. runtime -----------------------------------------------------------------------------
# ---- 3. runtime ---------------------------------------------------------------------
# trixie ships git 2.47+: walgit wants >= 2.47 on the server (pack.writeReverseIndex, bundle-uri,
# `index-pack --rev-index`); clients need >= 2.46.
FROM docker.io/library/debian:trixie-slim
Expand All @@ -49,11 +54,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends git git-lfs ca-
RUN useradd --uid 1000 --create-home --shell /bin/sh walgit \
&& mkdir -p /etc/walgit /var/lib/walgit && chown walgit:walgit /var/lib/walgit
COPY --from=build /out/bin/walgit /out/bin/walgit-server /usr/local/bin/
COPY deploy/walgithub.toml /etc/walgit/walgit.toml
ENV WALGIT_CONFIG=/etc/walgit/walgit.toml
ARG WALGIT_CONFIG_FILE=deploy/host.toml
ARG WALGIT_HTTP_PORT=8080
COPY ${WALGIT_CONFIG_FILE} /etc/walgit/walgit.toml
LABEL org.opencontainers.image.title="walgit" \
org.opencontainers.image.description="git hosting on an object store: smart HTTP, bundle-uri, LFS, web UI" \
org.opencontainers.image.licenses="MIT"
ENV WALGIT_CONFIG=/etc/walgit/walgit.toml \
WALGIT_HTTP_PORT=${WALGIT_HTTP_PORT}
USER walgit
WORKDIR /home/walgit
EXPOSE 8097
EXPOSE ${WALGIT_HTTP_PORT}
VOLUME ["/var/lib/walgit"]
HEALTHCHECK --interval=30s --timeout=5s CMD curl -fsS http://127.0.0.1:8097/readyz || exit 1
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD port="${PORT:-${WALGIT__SERVER__LISTEN##*:}}"; port="${port:-${WALGIT_HTTP_PORT}}"; \
curl -fsS "http://127.0.0.1:${port}/readyz" || curl -fsSk "https://127.0.0.1:${port}/readyz" || exit 1
STOPSIGNAL SIGTERM
ENTRYPOINT ["tini", "--", "walgit-server"]
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,33 @@ with its reasoning, the invariants, and the cost model (round trips to the bucke
```sh
# build (needs rust per rust-toolchain.toml, protoc, node 24 + pnpm for the web UI)
just web-build && cargo build --release -p walgit-cli
# or: nix build .#walgit or: podman build -t walgit -f Containerfile .
# or: nix build .#walgit or: docker build -t walgit .

# one box, TLS by walgit itself, a local S3 store (rustfs in a container)
just dev-store
./target/release/walgit-server --config walgit.standalone.toml
open https://walgit.localhost:8080/
```

For a deployment, Compose runs **WAL-Git** and **WAL-Github** against the bucket you provide:

```sh
cp deploy/walgit.env.example deploy/.env && "${EDITOR:-vi}" deploy/.env # token, bucket, public origins
docker compose -f deploy/compose.yaml up -d --build
# WAL-Git http://127.0.0.1:8080 token auth
# WAL-Github http://127.0.0.1:8097 GHES facade, loopback only
```

The stack starts no S3 service: set `WALGIT__STORE__*` in `.env` to your bucket. If you want a
local S3-compatible endpoint for development, `deploy/compose.rustfs.yaml` is a separate Compose
project (`just dev-store`).

* `walgit.standalone.toml` — the one-machine shape (self-signed TLS, rustfs, every role). Start here.
* `walgit.example.toml` — every key with its default and a comment.
* `Containerfile`, `flake.nix` — an OCI image and a Nix package/devshell.
* `docs/HOSTING.md` — running it as a service: the image, `deploy/compose.yaml`, auth/TLS, roles and
placement, upgrades, monitoring.
* `Dockerfile`, `deploy/compose.yaml` (WAL-Git + WAL-Github), `deploy/compose.rustfs.yaml` (optional dev S3),
`flake.nix` — an OCI image, the WAL stack, a separate store, and a Nix package/devshell.
* `deploy/nginx.conf.example` — an optional nginx in front: public TLS, one `auth_request` per credential, and
**byte offload**: walgit answers bundle/LFS downloads with `X-Accel-Redirect` and nginx streams + caches the
object from the bucket itself (S3 presigned or GCS with walgit's bearer). The file documents the contract.
Expand Down Expand Up @@ -174,7 +190,7 @@ crates/
walgit-config walgit.toml (+ WALGIT__ env overrides), per-repo settings merge, fail-closed validation
walgit-cli `walgit serve|import|compact|bundle|wal|mirror|synth|config|repo`; `walgit-server` = `walgit serve`
web/ React SPA (Vite) + sdk/repos.ts, built into the binary; the wire contract is web/API.md
docs/ BUNDLE_URI_DESIGN, ROUNDTRIPS (the cost model), POLICY, LFS, INTEGRITY, EVENTS, CONTRACT, patches/
docs/ BUNDLE_URI_DESIGN, ROUNDTRIPS (the cost model), HOSTING, POLICY, LFS, INTEGRITY, EVENTS, CONTRACT, patches/
```

## Invariants worth memorising
Expand Down
19 changes: 18 additions & 1 deletion crates/walgit-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ pub enum AuthMode {
#[serde(deny_unknown_fields)]
pub struct StaticToken {
pub principal: String,
/// Read from env var if set, else literal.
/// Read from env var if set, else literal. Missing when `token_env` is the only source.
#[serde(default)]
pub token: String,
#[serde(default)]
pub token_env: Option<String>,
Expand Down Expand Up @@ -1985,6 +1986,22 @@ audiences = ["walgit-cli", "https://git.example.com"]
let err = Config::parse("[store]\nbucket = \"b\"\n[server.auth]\nmode = \"token\"\n")
.unwrap_err();
assert!(err.to_string().contains("tokens"), "{err}");
// `token_env` alone is a complete source: the literal `token` may be absent.
let env_only = Config::parse(
r#"
[store]
bucket = "b"
[server.auth]
mode = "token"
tokens = [{ principal = "admin", token_env = "WALGIT_TEST_TOKEN" }]
"#,
)
.unwrap();
assert_eq!(
env_only.server.auth.tokens[0].token_env.as_deref(),
Some("WALGIT_TEST_TOKEN")
);
assert!(env_only.server.auth.tokens[0].token.is_empty());
// oidc: anonymous_read off, an allowlist, and a way in.
let err = Config::parse("[store]\nbucket = \"b\"\n[server.auth]\nmode = \"oidc\"\nanonymous_read = false\nallowed_domains = [\"example.com\"]\n").unwrap_err();
assert!(err.to_string().contains("way in"), "{err}");
Expand Down
2 changes: 1 addition & 1 deletion crates/walgit-server/src/web/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::error::ApiError;
#[folder = "../../web/dist"]
// Debug builds read the folder at runtime; `allow_missing` lets the crate
// compile where the web build is absent (distributed sccache workers).
// Release images assert the optimized artefacts exist (Containerfile).
// Release images assert the optimized artefacts exist (Dockerfile).
#[allow_missing = true]
struct Assets;

Expand Down
15 changes: 11 additions & 4 deletions compose.yaml → deploy/compose.rustfs.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# compose file for walgit local dev (podman compose): rustfs (S3-compatible) + bucket creation.
# Optional S3-compatible store for local development — a SEPARATE Compose project.
#
# Usage:
# podman compose up -d rustfs # start rustfs
# podman compose run --rm create-bucket # create the walgit-test bucket
# The WAL deployment (deploy/compose.yaml) deliberately starts no store: point it at your own
# bucket through `.env`. This file is the one we recommend when you want a local S3 endpoint:
#
# podman compose -f deploy/compose.rustfs.yaml up -d rustfs
# podman compose -f deploy/compose.rustfs.yaml run --rm create-bucket
# just dev-store # convenience wrapper
#
# Then set WALGIT__STORE__S3__ENDPOINT=http://host.docker.internal:9000 and
# WALGIT__STORE__S3__FORCE_PATH_STYLE=true in the WAL deployment's `.env`.
#
# rustfs exposes the S3 API on :9000 and the console on :9001.
# Credentials are fixed for local dev — do not use in production.

name: walgit-rustfs

services:
rustfs:
image: rustfs/rustfs:latest
Expand Down
87 changes: 87 additions & 0 deletions deploy/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# WAL deployment: WAL-Git and WAL-Github in front of an object store you provide.
#
# cp deploy/walgit.env.example deploy/.env && "${EDITOR:-vi}" deploy/.env
# docker compose -f deploy/compose.yaml up -d --build
#
# There is deliberately no S3 backend here. Both services read WALGIT__STORE__* from `.env`,
# so the bucket is your decision. An optional local rustfs dev store lives in
# deploy/compose.rustfs.yaml and is a separate Compose project (docs/HOSTING.md).
#
# WAL-Git http://127.0.0.1:8080 token auth; serve + maintain
# WAL-Github http://127.0.0.1:8097 GHES facade; auth-free, loopback only; serve + events
#
# The bucket is the repository. Both cache volumes are disposable.

name: walgit

x-walgit-common: &walgit-common
restart: unless-stopped
# SIGTERM starts the serving drain (D31): phase 1 is bounded at 30s and phase 2 gets
# server.drain_timeout (20s), so Docker must not SIGKILL before both finish.
stop_grace_period: 60s
env_file:
- .env
# The image's root filesystem is only the OS, git and the binaries; the process never writes
# to it. /tmp is scratch; the cache volume is the one writable mount that holds state.
read_only: true
tmpfs:
- /tmp:rw,size=256m
# Reach an object store published on the host (optional; Docker Desktop resolves it anyway).
extra_hosts:
- "host.docker.internal:host-gateway"
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"

services:
walgit:
<<: *walgit-common
image: ${WALGIT_IMAGE:-walgit:latest}
build:
context: ..
dockerfile: Dockerfile
args:
WALGIT_CONFIG_FILE: deploy/host.toml
WALGIT_HTTP_PORT: "8080"
WALGIT_BUILD_SHA: ${WALGIT_BUILD_SHA:-dev}
environment:
# Fail fast instead of creating objects in a bucket nobody named.
WALGIT__STORE__BUCKET: "${WALGIT__STORE__BUCKET:?set WALGIT__STORE__BUCKET in .env}"
WALGIT_TOKEN: "${WALGIT_TOKEN:?set WALGIT_TOKEN in .env}"
WALGIT__SERVER__AUTH__MODE: "${WALGIT_AUTH_MODE:-token}"
WALGIT__SERVER__ROLES: '["serve","maintain"]'
WALGIT__SERVER__LISTEN: "${WALGIT_LISTEN:-0.0.0.0:8080}"
WALGIT__SERVER__PUBLIC_URL: "${WALGIT_PUBLIC_URL:-http://127.0.0.1:8080}"
ports:
- "${WALGIT_BIND:-127.0.0.1}:${WALGIT_PORT:-8080}:${WALGIT_CONTAINER_PORT:-8080}"
volumes:
- walgit-cache:/var/lib/walgit

walgithub:
<<: *walgit-common
image: ${WALGITHUB_IMAGE:-walgithub:latest}
build:
context: ..
dockerfile: Dockerfile
args:
WALGIT_CONFIG_FILE: deploy/walgithub.toml
WALGIT_HTTP_PORT: "8097"
WALGIT_BUILD_SHA: ${WALGIT_BUILD_SHA:-dev}
environment:
WALGIT__STORE__BUCKET: "${WALGIT__STORE__BUCKET:?set WALGIT__STORE__BUCKET in .env}"
# The facade is auth-free by construction (docs/GITHUB.md) and must stay that way.
WALGIT__SERVER__AUTH__MODE: "none"
WALGIT__SERVER__TLS__MODE: "off"
WALGIT__SERVER__ROLES: '["serve","events"]'
WALGIT__SERVER__LISTEN: "${WALGITHUB_LISTEN:-0.0.0.0:8097}"
WALGIT__SERVER__PUBLIC_URL: "${WALGITHUB_PUBLIC_URL:-http://127.0.0.1:8097}"
ports:
- "${WALGITHUB_BIND:-127.0.0.1}:${WALGITHUB_PORT:-8097}:${WALGITHUB_CONTAINER_PORT:-8097}"
volumes:
- walgithub-cache:/var/lib/walgit

volumes:
walgit-cache:
walgithub-cache:
Loading