Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
295 linesCodeBlameRaw

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.

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