g1t/deploy/self-host/smoke.sh

273 lines14,683 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Running g1t yourself: the design, a docker compose proof, and a guide to what works today1#!/usr/bin/env bash
2# End-to-end check of a self-hosted g1t: sign up, confirm the email, make a
3# workspace and a repository, push and clone over HTTP, open an issue, and
Merge branch 'worktree-agent-aaf03bdceac799c89'4# read the code back through the site. Then the API on its own port (REST,
5# OAuth metadata and MCP, with an access token), pull requests from a branch
6# and from a fork merged onto main, the merge queue taking a pull request
7# and giving it back, and every cron handler the scheduler runs.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today8#
9# ./smoke.sh # against the compose stack's defaults
10# G1T_URL=http://localhost:8787 MAIL_LOG=wrangler.log ./smoke.sh
11#
12# The confirmation link is read from Mailpit (MAILPIT_URL, the default) or,
Merge branch 'worktree-agent-aaf03bdceac799c89'13# with MAIL_LOG set, from a log the mail Worker printed it to. API_URL and
14# MCP_URL are where the API is (default: G1T_URL's host on port 8789).
15# SCHEDULER_ONCE is the command that runs every cron once (scheduler.mjs
16# --once, inside the g1t container); unset, that step is skipped:
17#
18# SCHEDULER_ONCE="docker compose -f deploy/self-host/docker-compose.yml exec -T g1t \
19# node deploy/self-host/scheduler.mjs --once /data/generated/schedules.json" ./smoke.sh
20#
21# (In Git Bash on Windows, start the command with `env MSYS_NO_PATHCONV=1`
22# so /data is not rewritten into a Windows path.)
23# PACK_CACHE=off skips the check that a second clone is served from the
24# clone pack cache.
25#
26# Needs curl, git and node (to read JSON).
Running g1t yourself: the design, a docker compose proof, and a guide to what works today27set -euo pipefail
28
29G1T_URL="${G1T_URL:-http://localhost:8787}"
Merge branch 'worktree-agent-aaf03bdceac799c89'30API_URL="${API_URL:-$(node -e 'const u = new URL(process.argv[1]); u.port = "8789"; console.log(u.origin)' "$G1T_URL")}"
31MCP_URL="${MCP_URL:-$API_URL/mcp}"
Running g1t yourself: the design, a docker compose proof, and a guide to what works today32MAILPIT_URL="${MAILPIT_URL:-http://localhost:8025}"
33MAIL_LOG="${MAIL_LOG:-}"
Merge branch 'worktree-agent-aaf03bdceac799c89'34SCHEDULER_ONCE="${SCHEDULER_ONCE:-}"
35# off: this installation keeps no clone packs (no PACK_STORE), so a second
36# clone is not checked for a kept one.
37PACK_CACHE="${PACK_CACHE:-on}"
Running g1t yourself: the design, a docker compose proof, and a guide to what works today38RUN="$(date +%s)"
39USER_NAME="smoke${RUN}"
40EMAIL="${USER_NAME}@example.com"
41PASSWORD="correct-horse-${RUN}"
42WORKSPACE="ws${RUN}"
43REPO="hello"
44WORK="$(mktemp -d)"
45JAR="$WORK/cookies"
46trap 'rm -rf "$WORK"' EXIT
47
48step() { printf '\n== %s\n' "$*"; }
49fail() { printf 'FAILED: %s\n' "$*" >&2; exit 1; }
50
51# A form POST as a browser sends it, with the Origin the site checks.
52post() {
53 local path="$1"; shift
54 curl -sS -o "$WORK/body" -w '%{http_code} %{redirect_url}' -b "$JAR" -c "$JAR" \
55 -H "Origin: $G1T_URL" "$@" "$G1T_URL$path"
56}
57get() {
58 curl -sS -o "$WORK/body" -w '%{http_code}' -b "$JAR" -c "$JAR" "$G1T_URL$1"
59}
Merge branch 'worktree-agent-aaf03bdceac799c89'60# An API call with the access token: method, path, optional JSON body. The
61# answer is in $WORK/api; the status is printed.
62api() {
63 local method="$1" path="$2" body="${3:-}"
64 local args=(-sS -o "$WORK/api" -w '%{http_code}' -X "$method" -H "Authorization: Bearer $TOKEN")
65 [ -n "$body" ] && args+=(-H "content-type: application/json" --data "$body")
66 curl "${args[@]}" "$API_URL$path"
67}
68# A field of the last API answer (or of FILE), by a JavaScript path: `json pull.number`.
69json() {
70 node -e 'const v = process.argv[2].split(".").reduce((o, k) => o?.[k], JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))); console.log(typeof v === "object" ? JSON.stringify(v) : v ?? "")' "${2:-$WORK/api}" "$1"
71}
72# Waits for pull request $1 to have status $2.
73until_status() {
74 local status=""
75 for _ in $(seq 1 30); do
76 [ "$(api GET "/repos/$WORKSPACE/$REPO/pulls/$1")" = 200 ] && status="$(json pull.status)"
77 [ "$status" = "$2" ] && return 0
78 sleep 1
79 done
80 fail "pull request #$1 is $status, not $2: $(head -c 400 "$WORK/api")"
81}
82# Whether main, cloned fresh, has a file.
83main_has() {
84 rm -rf "$WORK/main"
85 git -c credential.helper= clone -q "$G1T_URL/$WORKSPACE/$REPO.git" "$WORK/main"
86 [ -f "$WORK/main/$1" ]
87}
88# A commit on a new branch of $1 (a clone), pushed to $2.
89commit_file() {
90 local dir="$1" remote="$2" branch="$3" file="$4"
91 (
92 cd "$dir"
93 git config user.name "Smoke Test"
94 git config user.email "$EMAIL"
95 git config commit.gpgsign false
96 git switch -q -c "$branch" 2>/dev/null || git switch -q "$branch"
97 mkdir -p "$(dirname "$file")"
98 printf 'Changed by smoke.sh on %s.\n' "$branch" > "$file"
99 git add . && git commit -qm "Add $file"
100 git -c credential.helper= push -q "$remote" "HEAD:$branch"
101 )
102}
Running g1t yourself: the design, a docker compose proof, and a guide to what works today103
104step "site answers at $G1T_URL"
105[ "$(get /)" = 200 ] || fail "GET / did not answer 200"
106
107step "sign up as $USER_NAME"
108out="$(post /register --data-urlencode "username=$USER_NAME" --data-urlencode "email=$EMAIL" --data-urlencode "password=$PASSWORD")"
109echo "$out"
110case "$out" in 30[23]*) ;; *) fail "register: $out $(head -c 300 "$WORK/body")" ;; esac
111# curl keeps Secure cookies only for https or localhost; carry it by hand.
112grep -q g1t_session "$JAR" || fail "no session cookie"
113
114step "confirm the email"
115link=""
116for _ in $(seq 1 20); do
117 if [ -n "$MAIL_LOG" ]; then
118 link="$(grep -ao "[a-z]*://[^ \"<]*/verify?token=[0-9a-zA-Z_-]*" "$MAIL_LOG" | tail -1 || true)"
119 else
120 id="$(curl -sS "$MAILPIT_URL/api/v1/search?query=to:$EMAIL" | sed -n 's/.*"ID":"\([^"]*\)".*/\1/p' | head -1)"
121 [ -n "$id" ] && link="$(curl -sS "$MAILPIT_URL/api/v1/message/$id" | grep -ao '[a-z]*://[^ "<\\]*/verify?token=[0-9a-zA-Z_-]*' | head -1 || true)"
122 fi
123 [ -n "$link" ] && break
124 sleep 1
125done
126[ -n "$link" ] || fail "no confirmation email arrived"
127echo "$link"
128[ "$(get "/verify?${link#*\?}")" = 200 ] || fail "verify"
129grep -q "$USER_NAME" "$WORK/body" || fail "verify page does not name the account"
130
131step "create workspace $WORKSPACE"
132out="$(post /workspaces/new --data-urlencode "slug=$WORKSPACE" --data-urlencode "displayName=Smoke $RUN")"
133echo "$out"
134case "$out" in 30[23]*) ;; *) fail "workspace: $out $(head -c 300 "$WORK/body")" ;; esac
135
136step "create repository $WORKSPACE/$REPO"
137out="$(post /new --data-urlencode "workspace=$WORKSPACE" --data-urlencode "name=$REPO" --data-urlencode "description=Self-host smoke test" --data-urlencode "visibility=public" --data-urlencode "source=empty")"
138echo "$out"
139case "$out" in 30[23]*) ;; *) fail "repo: $out $(head -c 300 "$WORK/body")" ;; esac
140
141step "push over HTTP"
142remote="${G1T_URL/:\/\//://$USER_NAME:$PASSWORD@}/$WORKSPACE/$REPO.git"
143git init -q -b main "$WORK/src"
144(
145 cd "$WORK/src"
146 git config user.name "Smoke Test"
147 git config user.email "$EMAIL"
148 # A throwaway commit: never signed, whatever the global config says.
149 git config commit.gpgsign false
150 printf '# hello\n\nPushed to a self-hosted g1t.\n' > README.md
151 mkdir -p src && printf 'fn main() {\n println!("hello from g1t");\n}\n' > src/main.rs
152 git add . && git commit -qm "First commit"
153 git -c credential.helper= push -q "$remote" main
154)
155echo "pushed $(git -C "$WORK/src" rev-parse --short HEAD)"
156
157step "clone over HTTP"
158git -c credential.helper= clone -q "$G1T_URL/$WORKSPACE/$REPO.git" "$WORK/clone"
159diff -q "$WORK/src/README.md" "$WORK/clone/README.md" || fail "clone differs"
160echo "clone matches"
161
Merge branch 'worktree-agent-aaf03bdceac799c89'162if [ "$PACK_CACHE" != off ]; then
163 step "clone again: the pack comes from the cache"
164 # The repos service says in Server-Timing whether the pack was kept.
165 GIT_TRACE_CURL=1 GIT_TRACE_CURL_NO_DATA=1 git -c credential.helper= clone -q "$G1T_URL/$WORKSPACE/$REPO.git" "$WORK/again" 2> "$WORK/trace"
166 grep -qi 'server-timing:.*pack;desc=hit' "$WORK/trace" || fail "the second clone's pack was not kept: $(grep -io 'pack;desc=[a-z]*' "$WORK/trace" | tr '\n' ' ')"
167 diff -qr --exclude=.git "$WORK/clone" "$WORK/again" >/dev/null || fail "the kept pack differs"
168 echo "hit"
169fi
170
Running g1t yourself: the design, a docker compose proof, and a guide to what works today171step "open an issue"
172out="$(post "/$WORKSPACE/$REPO/issues/new" --data-urlencode "title=It works" --data-urlencode "body=Opened by smoke.sh")"
173echo "$out"
174case "$out" in 30[23]*/issues/1) ;; *) fail "issue: $out $(head -c 300 "$WORK/body")" ;; esac
175[ "$(get "/$WORKSPACE/$REPO/issues/1")" = 200 ] || fail "issue page"
176grep -q "It works" "$WORK/body" || fail "issue page does not show the title"
177
178step "browse code in the site"
179[ "$(get "/$WORKSPACE/$REPO/code")" = 200 ] || fail "code page"
180grep -q "README.md" "$WORK/body" || fail "code page does not list README.md"
181[ "$(get "/$WORKSPACE/$REPO/blob/main/src/main.rs")" = 200 ] || fail "blob page"
182grep -q "hello from g1t" "$WORK/body" || fail "blob page does not show the file"
183[ "$(get "/$WORKSPACE/$REPO/commits")" = 200 ] || fail "commits page"
184grep -q "First commit" "$WORK/body" || fail "commits page does not show the commit"
185
Merge branch 'worktree-agent-aaf03bdceac799c89'186step "make an access token"
187out="$(post /settings/tokens --data-urlencode "intent=add-token" --data-urlencode "label=smoke" --data-urlencode "preset=full" --data-urlencode "expires=7")"
188echo "$out"
189TOKEN="$(grep -ao 'g1t_[0-9A-Za-z_-]*' "$WORK/body" | head -1 || true)"
190[ -n "$TOKEN" ] || fail "no token in the tokens page: $out"
191echo "token ${TOKEN:0:8}…"
192
193step "the API answers at $API_URL"
194[ "$(api GET /user)" = 200 ] || fail "GET /user: $(head -c 300 "$WORK/api")"
195[ "$(json username)" = "$USER_NAME" ] || fail "GET /user names $(json username), not $USER_NAME"
196[ "$(api GET /)" = 200 ] || fail "GET /"
197[ "$(json mcp_url)" = "$MCP_URL" ] || fail "the API's index names $(json mcp_url) as MCP, not $MCP_URL"
198[ "$(json git_url)" = "$G1T_URL/{owner}/{name}.git" ] || fail "git_url is $(json git_url)"
199curl -sS -o "$WORK/api" "$API_URL/.well-known/oauth-authorization-server"
200[ "$(json issuer)" = "$API_URL" ] || fail "the OAuth issuer is $(json issuer), not $API_URL"
201[ "$(json authorization_endpoint)" = "$G1T_URL/oauth/authorize" ] || fail "people approve at $(json authorization_endpoint)"
202echo "issuer $(json issuer)"
203code="$(curl -sS -o "$WORK/api" -D "$WORK/headers" -w '%{http_code}' -X POST -H "content-type: application/json" \
204 --data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' "$MCP_URL")"
205[ "$code" = 401 ] || fail "MCP without a token answered $code"
206grep -qi "resource_metadata=\"$API_URL/.well-known/oauth-protected-resource/mcp\"" "$WORK/headers" || fail "MCP's challenge: $(grep -i www-authenticate "$WORK/headers")"
207code="$(curl -sS -o "$WORK/api" -w '%{http_code}' -X POST -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
208 -H "accept: application/json, text/event-stream" --data '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' "$MCP_URL")"
209[ "$code" = 200 ] && grep -q '"tools"' "$WORK/api" || fail "MCP tools/list: $code $(head -c 300 "$WORK/api")"
210echo "MCP lists its tools at $MCP_URL"
211
212step "a pull request from a branch, merged"
213git -C "$WORK/clone" config credential.helper ""
214commit_file "$WORK/clone" "$remote" from-branch docs/branch.md
215[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls" '{"title":"From a branch","branch":"from-branch","body":"Opened by smoke.sh"}')" = 200 ] \
216 || fail "open from a branch: $(head -c 300 "$WORK/api")"
217BRANCH_PULL="$(json pull.number)"
218echo "pull request #$BRANCH_PULL ($(json pull.status))"
219[ "$(get "/$WORKSPACE/$REPO/pull/$BRANCH_PULL")" = 200 ] && grep -q "From a branch" "$WORK/body" || fail "pull request page"
220[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls/$BRANCH_PULL/merge" '{}')" = 200 ] || fail "merge: $(head -c 300 "$WORK/api")"
221until_status "$BRANCH_PULL" merged
222main_has docs/branch.md || fail "main does not have the branch's change"
223echo "merged onto main"
224
225step "a pull request from a fork, merged"
226[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls" '{"title":"From a fork","agent":"smoke"}')" = 200 ] \
227 || fail "open with a fork: $(head -c 300 "$WORK/api")"
228FORK_PULL="$(json pull.number)"
229fork_remote="$(json git.remote)"
230echo "pull request #$FORK_PULL ($(json pull.status)), fork $fork_remote"
231case "$fork_remote" in "$G1T_URL"/*) ;; *) fail "the fork's remote is not on $G1T_URL: $fork_remote" ;; esac
232[ "$fork_remote" != "$G1T_URL/$WORKSPACE/$REPO.git" ] || fail "no fork was made"
233authed_fork="${fork_remote/:\/\//://$USER_NAME:$TOKEN@}"
234git -c credential.helper= clone -q "$authed_fork" "$WORK/fork"
235commit_file "$WORK/fork" "$authed_fork" "$(git -C "$WORK/fork" branch --show-current)" docs/fork.md
236[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls/$FORK_PULL/ready" '{"summary":"Adds docs/fork.md, from a fork."}')" = 200 ] \
237 || fail "ready: $(head -c 300 "$WORK/api")"
238[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls/$FORK_PULL/merge" '{}')" = 200 ] || fail "merge: $(head -c 300 "$WORK/api")"
239until_status "$FORK_PULL" merged
240main_has docs/fork.md || fail "main does not have the fork's change"
241echo "merged onto main"
242
243step "the merge queue takes a pull request, and gives it back"
244[ "$(api PATCH "/repos/$WORKSPACE/$REPO/settings" '{"merge_queue":true}')" = 200 ] || fail "turn the queue on: $(head -c 300 "$WORK/api")"
245git -C "$WORK/clone" fetch -q "$G1T_URL/$WORKSPACE/$REPO.git" main
246git -C "$WORK/clone" switch -q -c queued FETCH_HEAD
247commit_file "$WORK/clone" "$remote" queued docs/queued.md
248[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls" '{"title":"Through the queue","branch":"queued"}')" = 200 ] || fail "open: $(head -c 300 "$WORK/api")"
249QUEUE_PULL="$(json pull.number)"
250[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls/$QUEUE_PULL/merge" '{}')" = 200 ] || fail "merge into the queue: $(head -c 300 "$WORK/api")"
251[ "$(api GET "/repos/$WORKSPACE/$REPO/queue")" = 200 ] || fail "queue: $(head -c 300 "$WORK/api")"
252[ "$(json enabled)" = true ] || fail "the queue is not on"
253node -e 'const q = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); const e = q.active.find((e) => e.number === Number(process.argv[2])); if (!e) process.exit(1); console.log(`#${e.number} is ${e.state} in the queue`)' "$WORK/api" "$QUEUE_PULL" \
254 || fail "#$QUEUE_PULL is not in the queue: $(head -c 400 "$WORK/api")"
255[ "$(get "/$WORKSPACE/$REPO/queue")" = 200 ] && grep -q "Through the queue" "$WORK/body" || fail "queue page"
256# Testing a queued state needs a sandbox, and agents are off: take it out.
257out="$(post "/$WORKSPACE/$REPO/pull/$QUEUE_PULL" --data-urlencode "action=unqueue")"
258echo "unqueue: $out"
259[ "$(api GET "/repos/$WORKSPACE/$REPO/queue")" = 200 ] || fail "queue"
260node -e 'const q = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); process.exit(q.active.some((e) => e.number === Number(process.argv[2])) ? 1 : 0)' "$WORK/api" "$QUEUE_PULL" \
261 || fail "#$QUEUE_PULL is still in the queue"
262[ "$(api PATCH "/repos/$WORKSPACE/$REPO/settings" '{"merge_queue":false}')" = 200 ] || fail "turn the queue off"
263[ "$(api POST "/repos/$WORKSPACE/$REPO/pulls/$QUEUE_PULL/merge" '{}')" = 200 ] || fail "merge: $(head -c 300 "$WORK/api")"
264until_status "$QUEUE_PULL" merged
265main_has docs/queued.md || fail "main does not have the change"
266echo "out of the queue, then merged onto main"
267
268if [ -n "$SCHEDULER_ONCE" ]; then
269 step "every cron handler runs"
270 $SCHEDULER_ONCE || fail "a cron handler failed"
271fi
272
Running g1t yourself: the design, a docker compose proof, and a guide to what works today273printf '\nAll checks passed: %s/%s/%s\n' "$G1T_URL" "$WORKSPACE" "$REPO"

This file's history is long; its oldest lines are credited to the oldest commit read.