Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1#!/usr/bin/env bash
Matt W2#
Matt W3# Dogfood — build, deploy, and inspect the running stack.
Matt W4#
Matt W5# ./run.sh rebuild the image and restart the containers
Matt W6# ./run.sh build build the image only
Matt W7# ./run.sh restart restart without rebuilding
Matt W8# ./run.sh up start (build if the image is missing)
Matt W9# ./run.sh down stop and remove the containers
Matt W10# ./run.sh logs [-f] show logs
Matt W11# ./run.sh status containers, health, and the public endpoint
Matt W12# ./run.sh test run the full Rust test suite in Docker
Matt W13# ./run.sh check cargo check
Matt W14# ./run.sh fixtures regenerate the jj/git fixture corpus
Matt W15# ./run.sh psql open a psql shell on the configured database
Matt W16# ./run.sh token show the outstanding admin setup token status
Matt W17# ./run.sh shell a shell in the web container
Matt W18# ./run.sh metrics scrape /metrics from inside the container
Matt W19# ./run.sh reindex [id] re-run indexing (all repositories, or one)
Matt W20# ./run.sh backup [dir] pg_dump + a snapshot of the repository volume
Matt W21# ./run.sh rehearse prove a backup restores (spec §10)
Matt W22# ./run.sh loadtest drive a large repository over HTTP (spec §11)
Matt W23#
Matt W24# The deploy path is `docker compose`, so this script is a convenience wrapper
Matt W25# and never the only way to operate the stack.
Matt W26
Matt W27set -euo pipefail
Matt W28
Matt W29ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Matt W30COMPOSE=(docker compose -f "$ROOT/docker/compose.yaml")
Matt W31SERVICE=web
Matt W32CONTAINER=dogfood-web
Matt W33# Every service built and restarted by the default `deploy` path.
Matt W34SERVICES=(web worker ssh)
Matt W35
Matt W36# Pinned so a toolchain bump is a deliberate edit. 1.83 and older fail to parse
Matt W37# a transitive `time-core` manifest.
Matt W38RUST_IMAGE="rust:slim"
Matt W39
Matt W40CARGO_ARGS=(
Matt W41 --rm
Matt W42 -v "$ROOT":/w -w /w
Matt W43 -v dogfood-cargo-registry:/usr/local/cargo/registry
Matt W44 -v dogfood-target:/w/target
Matt W45)
Matt W46
Matt W47c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
Matt W48c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
Matt W49c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }
Matt W50
Matt W51require_env() {
Matt W52 if [[ ! -f "$ROOT/.env" ]]; then
Matt W53 c_red "No .env at $ROOT/.env — the containers cannot start without it."
Matt W54 echo "Copy .env.example and fill in DATABASE_URL and the OIDC_* values."
Matt W55 exit 1
Matt W56 fi
Matt W57}
Matt W58
Matt W59# Read a value out of .env without sourcing it, so quoting and stray shell
Matt W60# metacharacters in secrets cannot execute.
Matt W61env_get() {
Matt W62 sed -n "s/^$1=//p" "$ROOT/.env" | head -1
Matt W63}
Matt W64
Matt W65cmd_build() {
Matt W66 require_env
Matt W67 c_dim "building $SERVICE…"
Matt W68 "${COMPOSE[@]}" build "${SERVICES[@]}"
Matt W69 c_green "built"
Matt W70}
Matt W71
Matt W72cmd_up() {
Matt W73 require_env
Matt W74 "${COMPOSE[@]}" up -d
Matt W75 wait_healthy
Matt W76}
Matt W77
Matt W78cmd_down() {
Matt W79 "${COMPOSE[@]}" down
Matt W80}
Matt W81
Matt W82cmd_restart() {
Matt W83 require_env
Matt W84 # --force-recreate so a changed .env is actually picked up; compose does not
Matt W85 # recreate a container just because its env_file changed on disk.
Matt W86 "${COMPOSE[@]}" up -d --force-recreate
Matt W87 wait_healthy
Matt W88}
Matt W89
Matt W90cmd_deploy() {
Matt W91 cmd_build
Matt W92 cmd_restart
Matt W93}
Matt W94
Matt W95# Poll until the app answers, so the script fails loudly rather than returning
Matt W96# success on a container that crash-looped.
Matt W97wait_healthy() {
Matt W98 c_dim "waiting for $CONTAINER to serve…"
Matt W99 for _ in $(seq 1 40); do
Matt W100 if docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1; then
Matt W101 c_green "healthy"
Matt W102 cmd_status
Matt W103 return 0
Matt W104 fi
Matt W105 if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
Matt W106 c_red "container exited — last 30 log lines:"
Matt W107 docker logs --tail 30 "$CONTAINER" 2>&1 || true
Matt W108 return 1
Matt W109 fi
Matt W110 sleep 1
Matt W111 done
Matt W112 c_red "timed out waiting for health — last 30 log lines:"
Matt W113 docker logs --tail 30 "$CONTAINER" 2>&1 || true
Matt W114 return 1
Matt W115}
Matt W116
Matt W117cmd_logs() {
Matt W118 "${COMPOSE[@]}" logs "${@:-}" "${SERVICES[@]}"
Matt W119}
Matt W120
Matt W121cmd_status() {
Matt W122 echo
Matt W123 "${COMPOSE[@]}" ps
Matt W124 local base
Matt W125 base="$(env_get BASE_URL)"
Matt W126 base="${base:-https://dogfood.sh}"
Matt W127 echo
Matt W128 printf '%-28s ' "$base/healthz"
Matt W129 curl -s -o /dev/null -w '%{http_code}\n' "$base/healthz" --max-time 15 || echo "unreachable"
Matt W130 printf '%-28s ' "$base/readyz"
Matt W131 curl -s -o /dev/null -w '%{http_code}\n' "$base/readyz" --max-time 15 || echo "unreachable"
Matt W132}
Matt W133
Matt W134cmd_test() {
Matt W135 # Fixtures are required so the corpus tests cannot silently skip.
Matt W136 [[ -d "$ROOT/fixtures/repos" ]] || cmd_fixtures
Matt W137 docker run "${CARGO_ARGS[@]}" -e DF_REQUIRE_FIXTURES=1 "$RUST_IMAGE" bash -c '
Matt W138 apt-get update -qq >/dev/null 2>&1
Matt W139 apt-get install -y -qq git >/dev/null 2>&1
Matt W140 cargo test --workspace'
Matt W141}
Matt W142
Matt W143cmd_check() {
Matt W144 docker run "${CARGO_ARGS[@]}" "$RUST_IMAGE" cargo check --workspace --all-targets
Matt W145}
Matt W146
Matt W147cmd_fixtures() {
Matt W148 "$ROOT/fixtures/gen.sh"
Matt W149}
Matt W150
Matt W151cmd_psql() {
Matt W152 require_env
Matt W153 local url
Matt W154 url="$(env_get DATABASE_URL)"
Matt W155 [[ -n "$url" ]] || { c_red "DATABASE_URL not set in .env"; exit 1; }
Matt W156 # Only request a TTY when we actually have one, so `./run.sh psql -tAc '…'`
Matt W157 # works from a script or a pipeline.
Matt W158 local tty=()
Matt W159 [[ -t 0 && -t 1 ]] && tty=(-it)
Matt W160 docker run --rm "${tty[@]}" postgres:17-alpine psql "$url" "$@"
Matt W161}
Matt W162
Matt W163cmd_token() {
Matt W164 require_env
Matt W165 local url
Matt W166 url="$(env_get DATABASE_URL)"
Matt W167 echo "Outstanding setup tokens (plaintext is only ever printed to the log at first boot):"
Matt W168 docker run --rm postgres:17-alpine psql "$url" -c \
Matt W169 "SELECT id, created_at, consumed_at, consumed_by FROM setup_tokens ORDER BY created_at DESC;"
Matt W170 echo "If none is outstanding and you still need admin, promote a user directly:"
Matt W171 c_dim " ./run.sh psql -c \"UPDATE users SET is_admin = true WHERE handle = 'your-handle';\""
Matt W172}
Matt W173
Matt W174cmd_shell() {
Matt W175 docker exec -it "$CONTAINER" bash
Matt W176}
Matt W177
Matt W178# /metrics is loopback-only by design (spec §7), so it is scraped from inside
Matt W179# the container rather than over the network.
Matt W180cmd_metrics() {
Matt W181 docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/metrics
Matt W182}
Matt W183
Matt W184# The reconciling reindex (spec §4, §10). Runs in the worker, which is where the
Matt W185# repository volume is mounted.
Matt W186cmd_reindex() {
Matt W187 if [[ -n "${1:-}" ]]; then
Matt W188 "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --repo "$1"
Matt W189 else
Matt W190 "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --all
Matt W191 fi
Matt W192}
Matt W193
Matt W194case "${1:-deploy}" in
Matt W195 ""|deploy) cmd_deploy ;;
Matt W196 build) cmd_build ;;
Matt W197 up) cmd_up ;;
Matt W198 down) cmd_down ;;
Matt W199 restart) cmd_restart ;;
Matt W200 logs) shift; cmd_logs "$@" ;;
Matt W201 status) cmd_status ;;
Matt W202 test) cmd_test ;;
Matt W203 check) cmd_check ;;
Matt W204 fixtures) cmd_fixtures ;;
Matt W205 psql) shift; cmd_psql "$@" ;;
Matt W206 token) cmd_token ;;
Matt W207 shell) cmd_shell ;;
Matt W208 metrics) cmd_metrics ;;
Matt W209 reindex) shift; cmd_reindex "${1:-}" ;;
Matt W210 backup) shift; "$ROOT/scripts/backup.sh" backup "$@" ;;
Matt W211 rehearse) shift; "$ROOT/scripts/backup.sh" rehearse "$@" ;;
Matt W212 loadtest) shift; "$ROOT/scripts/loadtest.sh" "$@" ;;
Matt W213 -h|--help|help)
Matt W214 sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
Matt W215 ;;
Matt W216 *)
Matt W217 c_red "unknown command: $1"
Matt W218 sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
Matt W219 exit 1
Matt W220 ;;
Matt W221esac

221 lines · Shell