Jump to…
snowinitial commitqoxwzsukwmkx1mo
1#!/usr/bin/env bash
2#
3# Dogfood — build, deploy, and inspect the running stack.
4#
5# ./run.sh rebuild the image and restart the containers
6# ./run.sh build build the image only
7# ./run.sh restart restart without rebuilding
8# ./run.sh up start (build if the image is missing)
9# ./run.sh down stop and remove the containers
10# ./run.sh logs [-f] show logs
11# ./run.sh status containers, health, and the public endpoint
12# ./run.sh test run the full Rust test suite in Docker
13# ./run.sh check cargo check
14# ./run.sh fixtures regenerate the jj/git fixture corpus
15# ./run.sh psql open a psql shell on the configured database
16# ./run.sh token show the outstanding admin setup token status
17# ./run.sh shell a shell in the web container
18# ./run.sh metrics scrape /metrics from inside the container
19# ./run.sh reindex [id] re-run indexing (all repositories, or one)
20# ./run.sh backup [dir] pg_dump + a snapshot of the repository volume
21# ./run.sh rehearse prove a backup restores (spec §10)
22# ./run.sh loadtest drive a large repository over HTTP (spec §11)
23#
24# The deploy path is `docker compose`, so this script is a convenience wrapper
25# and never the only way to operate the stack.
26
27set -euo pipefail
28
29ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30COMPOSE=(docker compose -f "$ROOT/docker/compose.yaml")
31SERVICE=web
32CONTAINER=dogfood-web
33# Every service built and restarted by the default `deploy` path.
34SERVICES=(web worker ssh)
35
36# Pinned so a toolchain bump is a deliberate edit. 1.83 and older fail to parse
37# a transitive `time-core` manifest.
38RUST_IMAGE="rust:slim"
39
40CARGO_ARGS=(
41 --rm
42 -v "$ROOT":/w -w /w
43 -v dogfood-cargo-registry:/usr/local/cargo/registry
44 -v dogfood-target:/w/target
45)
46
47c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
48c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
49c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }
50
51require_env() {
52 if [[ ! -f "$ROOT/.env" ]]; then
53 c_red "No .env at $ROOT/.env — the containers cannot start without it."
54 echo "Copy .env.example and fill in DATABASE_URL and the OIDC_* values."
55 exit 1
56 fi
57}
58
59# Read a value out of .env without sourcing it, so quoting and stray shell
60# metacharacters in secrets cannot execute.
61env_get() {
62 sed -n "s/^$1=//p" "$ROOT/.env" | head -1
63}
64
65cmd_build() {
66 require_env
67 c_dim "building $SERVICE…"
68 "${COMPOSE[@]}" build "${SERVICES[@]}"
69 c_green "built"
70}
71
72cmd_up() {
73 require_env
74 "${COMPOSE[@]}" up -d
75 wait_healthy
76}
77
78cmd_down() {
79 "${COMPOSE[@]}" down
80}
81
82cmd_restart() {
83 require_env
84 # --force-recreate so a changed .env is actually picked up; compose does not
85 # recreate a container just because its env_file changed on disk.
86 "${COMPOSE[@]}" up -d --force-recreate
87 wait_healthy
88}
89
90cmd_deploy() {
91 cmd_build
92 cmd_restart
93}
94
95# Poll until the app answers, so the script fails loudly rather than returning
96# success on a container that crash-looped.
97wait_healthy() {
98 c_dim "waiting for $CONTAINER to serve…"
99 for _ in $(seq 1 40); do
100 if docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1; then
101 c_green "healthy"
102 cmd_status
103 return 0
104 fi
105 if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
106 c_red "container exited — last 30 log lines:"
107 docker logs --tail 30 "$CONTAINER" 2>&1 || true
108 return 1
109 fi
110 sleep 1
111 done
112 c_red "timed out waiting for health — last 30 log lines:"
113 docker logs --tail 30 "$CONTAINER" 2>&1 || true
114 return 1
115}
116
117cmd_logs() {
118 "${COMPOSE[@]}" logs "${@:-}" "${SERVICES[@]}"
119}
120
121cmd_status() {
122 echo
123 "${COMPOSE[@]}" ps
124 local base
125 base="$(env_get BASE_URL)"
126 base="${base:-https://dogfood.sh}"
127 echo
128 printf '%-28s ' "$base/healthz"
129 curl -s -o /dev/null -w '%{http_code}\n' "$base/healthz" --max-time 15 || echo "unreachable"
130 printf '%-28s ' "$base/readyz"
131 curl -s -o /dev/null -w '%{http_code}\n' "$base/readyz" --max-time 15 || echo "unreachable"
132}
133
134cmd_test() {
135 # Fixtures are required so the corpus tests cannot silently skip.
136 [[ -d "$ROOT/fixtures/repos" ]] || cmd_fixtures
137 docker run "${CARGO_ARGS[@]}" -e DF_REQUIRE_FIXTURES=1 "$RUST_IMAGE" bash -c '
138 apt-get update -qq >/dev/null 2>&1
139 apt-get install -y -qq git >/dev/null 2>&1
140 cargo test --workspace'
141}
142
143cmd_check() {
144 docker run "${CARGO_ARGS[@]}" "$RUST_IMAGE" cargo check --workspace --all-targets
145}
146
147cmd_fixtures() {
148 "$ROOT/fixtures/gen.sh"
149}
150
151cmd_psql() {
152 require_env
153 local url
154 url="$(env_get DATABASE_URL)"
155 [[ -n "$url" ]] || { c_red "DATABASE_URL not set in .env"; exit 1; }
156 # Only request a TTY when we actually have one, so `./run.sh psql -tAc '…'`
157 # works from a script or a pipeline.
158 local tty=()
159 [[ -t 0 && -t 1 ]] && tty=(-it)
160 docker run --rm "${tty[@]}" postgres:17-alpine psql "$url" "$@"
161}
162
163cmd_token() {
164 require_env
165 local url
166 url="$(env_get DATABASE_URL)"
167 echo "Outstanding setup tokens (plaintext is only ever printed to the log at first boot):"
168 docker run --rm postgres:17-alpine psql "$url" -c \
169 "SELECT id, created_at, consumed_at, consumed_by FROM setup_tokens ORDER BY created_at DESC;"
170 echo "If none is outstanding and you still need admin, promote a user directly:"
171 c_dim " ./run.sh psql -c \"UPDATE users SET is_admin = true WHERE handle = 'your-handle';\""
172}
173
174cmd_shell() {
175 docker exec -it "$CONTAINER" bash
176}
177
178# /metrics is loopback-only by design (spec §7), so it is scraped from inside
179# the container rather than over the network.
180cmd_metrics() {
181 docker exec "$CONTAINER" wget -qO- http://127.0.0.1:8080/metrics
182}
183
184# The reconciling reindex (spec §4, §10). Runs in the worker, which is where the
185# repository volume is mounted.
186cmd_reindex() {
187 if [[ -n "${1:-}" ]]; then
188 "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --repo "$1"
189 else
190 "${COMPOSE[@]}" exec -T worker dogfood-admin reindex --all
191 fi
192}
193
194case "${1:-deploy}" in
195 ""|deploy) cmd_deploy ;;
196 build) cmd_build ;;
197 up) cmd_up ;;
198 down) cmd_down ;;
199 restart) cmd_restart ;;
200 logs) shift; cmd_logs "$@" ;;
201 status) cmd_status ;;
202 test) cmd_test ;;
203 check) cmd_check ;;
204 fixtures) cmd_fixtures ;;
205 psql) shift; cmd_psql "$@" ;;
206 token) cmd_token ;;
207 shell) cmd_shell ;;
208 metrics) cmd_metrics ;;
209 reindex) shift; cmd_reindex "${1:-}" ;;
210 backup) shift; "$ROOT/scripts/backup.sh" backup "$@" ;;
211 rehearse) shift; "$ROOT/scripts/backup.sh" rehearse "$@" ;;
212 loadtest) shift; "$ROOT/scripts/loadtest.sh" "$@" ;;
213 -h|--help|help)
214 sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
215 ;;
216 *)
217 c_red "unknown command: $1"
218 sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
219 exit 1
220 ;;
221esac

221 lines · Shell