Skip to content

Commit 727c17f

Browse files
test(installer): add cross-version migration update e2e
Add a focused end-to-end harness that installs an older managed payload, updates to a ref with a new migration, and verifies backup, service health, migration application, database reuse, and rollback. Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent 7301fae commit 727c17f

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

scripts/e2e-update-migrations.sh

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env bash
2+
# End-to-end proof that `paperclipai update` works ACROSS VERSIONS, including
3+
# database migrations, against a real managed install with a live service.
4+
#
5+
# Journey (real GitHub, real embedded Postgres, real systemd/launchd service):
6+
# install --ref BASE (payload with N migrations) -> onboard + service active
7+
# -> point the tracked ref at NEXT (payload with N+1 migrations; NEXT adds a
8+
# probe migration creating table `e2e_update_probe`)
9+
# -> update --check (exit 10) -> update --yes
10+
# -> assert: pre-update DB backup written, payload flipped, service healthy,
11+
# probe migration applied to the real database (visible in a pg dump),
12+
# the database cluster was reused (no re-initialization)
13+
# -> update --rollback -> old payload must still boot against the migrated DB
14+
#
15+
# The tracked-ref edit in install.json simulates the real user state "installed
16+
# from this ref a while ago, the ref has since gained migrations" without having
17+
# to mutate refs on GitHub while the test runs.
18+
#
19+
# Env knobs:
20+
# E2E_REPO GitHub repo (default: paperclipai/paperclip)
21+
# E2E_UPDATE_BASE_REF ref to install first (required; e.g. test/e2e-update-base)
22+
# E2E_UPDATE_NEXT_REF ref to update to (required; BASE + one probe migration)
23+
# E2E_BOOTSTRAP_CLI path to an already-built bootstrap CLI entry point
24+
# (dist/index.js); when unset, builds one from BASE
25+
# E2E_SERVICE_TIMEOUT_SECS service active/health wait (default 300)
26+
set -uo pipefail
27+
28+
E2E_REPO="${E2E_REPO:-paperclipai/paperclip}"
29+
BASE_REF="${E2E_UPDATE_BASE_REF:?E2E_UPDATE_BASE_REF is required}"
30+
NEXT_REF="${E2E_UPDATE_NEXT_REF:?E2E_UPDATE_NEXT_REF is required}"
31+
E2E_SERVICE_TIMEOUT_SECS="${E2E_SERVICE_TIMEOUT_SECS:-300}"
32+
33+
# Clean environment, then isolate ALL Paperclip state (managed store, config,
34+
# embedded Postgres, backups) under a dedicated home for this test.
35+
for var in $(env | grep -o '^PAPERCLIP_[A-Z_]*' || true); do unset "$var"; done
36+
unset NODE_ENV npm_config_prefix 2>/dev/null || true
37+
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
38+
export CI="${CI:-1}"
39+
export PAPERCLIP_HOME="$HOME/.paperclip-e2e-update"
40+
41+
SHIM="$HOME/.local/bin/paperclipai"
42+
STORE="$PAPERCLIP_HOME/cli"
43+
BACKUP_DIR="$PAPERCLIP_HOME/instances/default/data/backups"
44+
RESULTS=()
45+
FAILED=0
46+
47+
note() { printf '\n\033[1;34m== %s ==\033[0m\n' "$*"; }
48+
pass() { RESULTS+=("PASS $1"); printf '\033[1;32mPASS\033[0m %s\n' "$1"; }
49+
fail_() { RESULTS+=("FAIL $1"); printf '\033[1;31mFAIL\033[0m %s\n' "$1"; FAILED=1; }
50+
51+
shim() { "$SHIM" "$@"; }
52+
current_target() { readlink "$STORE/current" 2>/dev/null || echo "<missing>"; }
53+
backup_count() { ls "$BACKUP_DIR" 2>/dev/null | grep -c -E '\.sql(\.gz)?$'; }
54+
newest_backup() { ls -t "$BACKUP_DIR"/*.sql* 2>/dev/null | head -1; }
55+
dump_text() { case "$1" in *.gz) gunzip -c "$1" ;; *) cat "$1" ;; esac; }
56+
first_run_count() { shim service logs -n 2000 2>/dev/null | grep -c "first-run embedded PostgreSQL" || true; }
57+
58+
wait_active() {
59+
local deadline=$(( $(date +%s) + E2E_SERVICE_TIMEOUT_SECS )) status_json=""
60+
while [ "$(date +%s)" -lt "$deadline" ]; do
61+
status_json="$(shim service status --json 2>/dev/null || true)"
62+
if echo "$status_json" | grep -q '"active"[[:space:]]*:[[:space:]]*true'; then return 0; fi
63+
sleep 5
64+
done
65+
echo "last status: ${status_json:-<none>}"
66+
shim service logs -n 80 || true
67+
return 1
68+
}
69+
70+
cleanup() {
71+
shim service stop >/dev/null 2>&1 || true
72+
shim service uninstall >/dev/null 2>&1 || true
73+
}
74+
trap cleanup EXIT
75+
76+
note "0. Preflight"
77+
uname -a
78+
node --version && npm --version
79+
command -v corepack >/dev/null || npm install -g corepack
80+
[ -e "$SHIM" ] && { echo "shim already exists at $SHIM — refusing to run"; exit 2; }
81+
[ -d "$PAPERCLIP_HOME" ] && { echo "$PAPERCLIP_HOME already exists — refusing to run"; exit 2; }
82+
if [ "$(uname -s)" = "Linux" ] && [ ! -S "/run/user/$(id -u)/bus" ]; then
83+
echo "no systemd user bus at /run/user/$(id -u)/bus — this test needs a real service"; exit 2
84+
fi
85+
echo "repo=$E2E_REPO base=$BASE_REF next=$NEXT_REF paperclip_home=$PAPERCLIP_HOME"
86+
87+
if [ -n "${E2E_BOOTSTRAP_CLI:-}" ] && [ -f "$E2E_BOOTSTRAP_CLI" ]; then
88+
BOOTSTRAP_CLI="$E2E_BOOTSTRAP_CLI"
89+
note "1. Bootstrap CLI reused: $BOOTSTRAP_CLI"
90+
else
91+
note "1. Bootstrap: build the CLI from the GitHub tarball of $BASE_REF"
92+
BOOT="$HOME/e2e-upd-bootstrap"
93+
mkdir -p "$BOOT"
94+
curl --fail --silent --show-error --location \
95+
"https://codeload.github.com/$E2E_REPO/tar.gz/$BASE_REF" \
96+
| tar -xz --strip-components=1 -C "$BOOT" || { fail_ "1a bootstrap tarball"; exit 1; }
97+
( cd "$BOOT" \
98+
&& corepack pnpm install --frozen-lockfile > "$HOME/e2e-upd-bootstrap-install.log" 2>&1 \
99+
&& bash scripts/build-npm.sh --skip-checks --skip-typecheck > "$HOME/e2e-upd-bootstrap-build.log" 2>&1 ) \
100+
|| { tail -40 "$HOME"/e2e-upd-bootstrap-*.log; fail_ "1b bootstrap build"; exit 1; }
101+
TARBALL="$(cd "$BOOT/cli" && npm pack --silent 2>/dev/null | tail -1)"
102+
mkdir -p "$HOME/e2e-upd-bootstrap-cli"
103+
( cd "$HOME/e2e-upd-bootstrap-cli" && npm install --no-fund --no-audit "$BOOT/cli/$TARBALL" > "$HOME/e2e-upd-bootstrap-npm.log" 2>&1 ) \
104+
|| { tail -40 "$HOME/e2e-upd-bootstrap-npm.log"; fail_ "1c bootstrap npm install"; exit 1; }
105+
BOOTSTRAP_CLI="$HOME/e2e-upd-bootstrap-cli/node_modules/paperclipai/dist/index.js"
106+
fi
107+
node "$BOOTSTRAP_CLI" --version >/dev/null || { fail_ "1d bootstrap CLI smoke"; exit 1; }
108+
pass "1 bootstrap CLI ready"
109+
110+
note "2. install --ref $BASE_REF (the 'old version' the user installed a while ago)"
111+
if node "$BOOTSTRAP_CLI" install --repo "$E2E_REPO" --ref "$BASE_REF" --yes; then
112+
pass "2a install base ref exits 0"
113+
else
114+
fail_ "2a install base ref exits 0"; exit 1
115+
fi
116+
BASE_TARGET="$(current_target)"
117+
case "$BASE_TARGET" in
118+
*"installs/git/"*) pass "2b current -> git payload ($(basename "$BASE_TARGET"))" ;;
119+
*) fail_ "2b current -> git payload (got: $BASE_TARGET)"; exit 1 ;;
120+
esac
121+
122+
note "3. onboard + service start on the old version (initializes the database)"
123+
if shim onboard --yes --install-service; then
124+
pass "3a onboard --yes --install-service exits 0"
125+
else
126+
fail_ "3a onboard --yes --install-service exits 0"; exit 1
127+
fi
128+
if wait_active; then
129+
pass "3b service active on base version"
130+
else
131+
fail_ "3b service active on base version"; exit 1
132+
fi
133+
FIRST_RUN_BEFORE="$(first_run_count)"
134+
PG_VERSION_FILE="$(find "$PAPERCLIP_HOME" -name PG_VERSION 2>/dev/null | head -1)"
135+
PG_INODE_BEFORE="$([ -n "$PG_VERSION_FILE" ] && ls -i "$PG_VERSION_FILE" | awk '{print $1}')"
136+
[ -n "$PG_VERSION_FILE" ] && pass "3c embedded Postgres cluster initialized" || fail_ "3c embedded Postgres cluster found"
137+
138+
note "4. baseline dump: probe table must NOT exist on the old schema"
139+
if shim db-backup >/dev/null 2>&1; then
140+
pass "4a db-backup on base version exits 0"
141+
else
142+
fail_ "4a db-backup on base version exits 0"
143+
fi
144+
BASELINE_DUMP="$(newest_backup)"
145+
if [ -n "$BASELINE_DUMP" ] && ! dump_text "$BASELINE_DUMP" | grep -q "e2e_update_probe"; then
146+
pass "4b baseline schema has no e2e_update_probe table"
147+
else
148+
fail_ "4b baseline schema has no e2e_update_probe table (dump: ${BASELINE_DUMP:-<none>})"
149+
fi
150+
BACKUPS_BEFORE="$(backup_count)"
151+
152+
note "5. simulate time passing: tracked ref now points at $NEXT_REF (adds one migration)"
153+
node -e '
154+
const fs = require("fs");
155+
const p = process.argv[1];
156+
const m = JSON.parse(fs.readFileSync(p, "utf8"));
157+
m.ref = process.argv[2];
158+
fs.writeFileSync(p, JSON.stringify(m, null, 2));
159+
' "$STORE/install.json" "$NEXT_REF" && pass "5a tracked ref updated in install.json" || fail_ "5a tracked ref update"
160+
161+
shim update --check --json; CHECK_EXIT=$?
162+
if [ "$CHECK_EXIT" -eq 10 ]; then
163+
pass "5b update --check sees the new version (exit 10)"
164+
else
165+
fail_ "5b update --check sees the new version (got exit $CHECK_EXIT)"
166+
fi
167+
168+
note "6. update --yes (backup -> build new payload -> flip -> restart service)"
169+
if shim update --yes > "$HOME/e2e-upd-update.log" 2>&1; then
170+
pass "6a update --yes exits 0"
171+
else
172+
tail -40 "$HOME/e2e-upd-update.log"; fail_ "6a update --yes exits 0"; exit 1
173+
fi
174+
tail -5 "$HOME/e2e-upd-update.log"
175+
BACKUPS_AFTER="$(backup_count)"
176+
if [ "$BACKUPS_AFTER" -gt "$BACKUPS_BEFORE" ]; then
177+
pass "6b pre-update database backup was written ($BACKUPS_BEFORE -> $BACKUPS_AFTER)"
178+
else
179+
fail_ "6b pre-update database backup was written ($BACKUPS_BEFORE -> $BACKUPS_AFTER)"
180+
fi
181+
NEXT_TARGET="$(current_target)"
182+
if [ "$NEXT_TARGET" != "$BASE_TARGET" ] && [ "${NEXT_TARGET#*installs/git/}" != "$NEXT_TARGET" ]; then
183+
pass "6c payload flipped to new git payload ($(basename "$NEXT_TARGET"))"
184+
else
185+
fail_ "6c payload flipped (before: $BASE_TARGET after: $NEXT_TARGET)"
186+
fi
187+
188+
note "7. service healthy on the new version, with the migration applied"
189+
if wait_active; then
190+
pass "7a service active after update"
191+
else
192+
fail_ "7a service active after update"; exit 1
193+
fi
194+
if shim service logs -n 2000 2>/dev/null | grep -q "e2e_update_probe"; then
195+
pass "7b service logs mention applying the probe migration"
196+
else
197+
fail_ "7b service logs mention applying the probe migration"
198+
fi
199+
if shim db-backup >/dev/null 2>&1; then
200+
pass "7c db-backup on new version exits 0"
201+
else
202+
fail_ "7c db-backup on new version exits 0"
203+
fi
204+
POST_DUMP="$(newest_backup)"
205+
if [ -n "$POST_DUMP" ] && dump_text "$POST_DUMP" | grep -q "e2e_update_probe"; then
206+
pass "7d probe migration table exists in the real database after update"
207+
else
208+
fail_ "7d probe migration table exists in the real database (dump: ${POST_DUMP:-<none>})"
209+
fi
210+
211+
note "8. data continuity: same database cluster, not re-initialized"
212+
FIRST_RUN_AFTER="$(first_run_count)"
213+
if [ "$FIRST_RUN_AFTER" = "$FIRST_RUN_BEFORE" ]; then
214+
pass "8a no new first-run database initialization after update ($FIRST_RUN_BEFORE -> $FIRST_RUN_AFTER)"
215+
else
216+
fail_ "8a no new first-run database initialization ($FIRST_RUN_BEFORE -> $FIRST_RUN_AFTER)"
217+
fi
218+
PG_INODE_AFTER="$([ -n "$PG_VERSION_FILE" ] && ls -i "$PG_VERSION_FILE" 2>/dev/null | awk '{print $1}')"
219+
if [ -n "$PG_INODE_BEFORE" ] && [ "$PG_INODE_AFTER" = "$PG_INODE_BEFORE" ]; then
220+
pass "8b embedded Postgres cluster reused (same PG_VERSION inode)"
221+
else
222+
fail_ "8b embedded Postgres cluster reused (inode $PG_INODE_BEFORE -> ${PG_INODE_AFTER:-<gone>})"
223+
fi
224+
225+
note "9. rollback: old payload must still boot against the migrated database"
226+
if shim update --rollback; then
227+
pass "9a update --rollback exits 0"
228+
else
229+
fail_ "9a update --rollback exits 0"
230+
fi
231+
case "$(current_target)" in
232+
"$BASE_TARGET") pass "9b rollback restored the base payload" ;;
233+
*) fail_ "9b rollback restored the base payload (got: $(current_target))" ;;
234+
esac
235+
if wait_active; then
236+
pass "9c rolled-back service is active against the migrated schema"
237+
else
238+
fail_ "9c rolled-back service is active against the migrated schema"
239+
fi
240+
241+
note "10. cleanup"
242+
shim service stop >/dev/null 2>&1 || true
243+
if shim service uninstall && shim uninstall; then
244+
pass "10a service uninstall + uninstall exit 0"
245+
else
246+
fail_ "10a service uninstall + uninstall exit 0"
247+
fi
248+
trap - EXIT
249+
250+
note "RESULTS ($E2E_REPO $BASE_REF -> $NEXT_REF on $(uname -sm))"
251+
printf '%s\n' "${RESULTS[@]}"
252+
if [ "$FAILED" = "1" ]; then echo; echo "OVERALL: FAIL"; exit 1; fi
253+
echo; echo "OVERALL: PASS"

0 commit comments

Comments
 (0)