OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / 225ab01

Add deploy sync test — verify releases propagate between servers
Run after a deploy to check federation sync worked: bare test/deploy-sync-test.js hyper-tps stortron.local Tests: 1. Local release ref exists 2. Remote bare repo has release branch 3. Refs match (federation synced) 4. Remote app install is current 5. Key files present (index.js, node_modules) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225ab01d1216789a3c5053d2b105cd4dc38b45b9
by plankatron <fleeky@gmail.com> · 2026-04-07T16:15:19.000Z

Changed files (1) +157 -0

test/deploy-sync-test.js +157 -0
……@@ -0,0 +1,157 @@
1#!/usr/bin/env bare
2/**
3 * Deploy Sync Test
4 *
5 * Verifies that a deploy on tabertron propagates to stortron.
6 * Run from tabertron after a deploy completes.
7 *
8 * Usage:
9 * bare test/deploy-sync-test.js <repo> [stortron-host]
10 *
11 * Example:
12 * bare test/deploy-sync-test.js hyper-tps stortron.local
13 * bare test/deploy-sync-test.js hyper-overlay
14 *
15 * Tests:
16 * 1. Local release ref exists
17 * 2. Remote bare repo has the release branch
18 * 3. Remote release ref matches local (federation synced)
19 * 4. Remote app install has the latest version
20 */
21
22import { spawn } from 'bare-subprocess'
23import process from 'bare-process'
24import os from 'bare-os'
25
26const REPO = process.argv[2]
27const REMOTE = process.argv[3] || 'stortron.local'
28
29if (!REPO) {
30 console.log('Usage: bare test/deploy-sync-test.js <repo> [remote-host]')
31 process.exit(1)
32}
33
34let passed = 0
35let failed = 0
36
37function ok (name, detail) { passed++; console.log(` ✓ ${name}${detail ? ' — ' + detail : ''}`) }
38function fail (name, reason) { failed++; console.log(` ✗ ${name}: ${reason}`) }
39
40function exec (cmd, args, opts = {}) {
41 return new Promise((resolve, reject) => {
42 const timeout = opts.timeout || 30000
43 let done = false
44 const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] })
45 const timer = setTimeout(() => {
46 if (done) return
47 done = true
48 try { child.kill('SIGKILL') } catch {}
49 reject(new Error('timeout'))
50 }, timeout)
51 let stdout = ''
52 let stderr = ''
53 child.stdout.on('data', (d) => { stdout += d.toString() })
54 child.stderr.on('data', (d) => { stderr += d.toString() })
55 child.on('exit', (code) => {
56 if (done) return
57 done = true
58 clearTimeout(timer)
59 if (code === 0) resolve(stdout.trim())
60 else reject(new Error(stderr.trim() || `exit ${code}`))
61 })
62 child.on('error', (err) => {
63 if (done) return
64 done = true
65 clearTimeout(timer)
66 reject(err)
67 })
68 })
69}
70
71async function sshExec (host, cmd) {
72 return exec('ssh', [host, cmd], { timeout: 15000 })
73}
74
75console.log('Deploy Sync Test')
76console.log('================')
77console.log(`Repo: ${REPO}`)
78console.log(`Local: ${os.hostname()}`)
79console.log(`Remote: ${REMOTE}`)
80console.log('')
81
82// Test 1: Local release ref
83console.log('1. Local release ref')
84let localRef
85try {
86 localRef = await exec('git', [
87 '--git-dir', `${os.homedir()}/git-repos/${REPO}.git`,
88 'rev-parse', 'refs/heads/release'
89 ])
90 ok('local release', localRef.slice(0, 12))
91} catch (err) {
92 fail('local release', err.message)
93 console.log('\nResults: 0 passed, 1 failed — no release branch')
94 process.exit(1)
95}
96
97// Test 2: Remote bare repo has release branch
98console.log('2. Remote bare repo release ref')
99let remoteRef
100try {
101 remoteRef = await sshExec(REMOTE,
102 `GIT_DIR=$HOME/git-repos/${REPO}.git git rev-parse refs/heads/release 2>/dev/null`)
103 ok('remote bare repo', remoteRef.slice(0, 12))
104} catch (err) {
105 fail('remote bare repo', err.message)
106}
107
108// Test 3: Refs match (federation synced)
109console.log('3. Release refs match')
110if (localRef && remoteRef) {
111 if (localRef === remoteRef) {
112 ok('refs match', localRef.slice(0, 12))
113 } else {
114 fail('refs DIVERGED',
115 `local=${localRef.slice(0, 12)} remote=${remoteRef.slice(0, 12)}`)
116 }
117} else {
118 fail('refs match', 'missing ref(s)')
119}
120
121// Test 4: Remote app install version
122console.log('4. Remote app install version')
123try {
124 const installRef = await sshExec(REMOTE,
125 `cd $HOME/.local/share/${REPO} && git rev-parse HEAD 2>/dev/null`)
126 if (installRef === localRef) {
127 ok('install current', installRef.slice(0, 12))
128 } else {
129 fail('install BEHIND',
130 `install=${installRef.slice(0, 12)} release=${localRef.slice(0, 12)}`)
131 }
132} catch (err) {
133 fail('install not found', err.message)
134}
135
136// Test 5: Key files present (for by-arch repos)
137console.log('5. Remote install has key files')
138try {
139 const check = await sshExec(REMOTE,
140 `ls $HOME/.local/share/${REPO}/index.js 2>/dev/null && echo "index.js:OK"; ` +
141 `ls $HOME/.local/share/${REPO}/node_modules/.package-lock.json 2>/dev/null && echo "node_modules:OK"; ` +
142 `ls $HOME/.local/share/${REPO}/by-arch/*/app/${REPO}/node_modules/.package-lock.json 2>/dev/null && echo "by-arch-nm:OK"; ` +
143 `echo "done"`)
144 const hasIndex = check.includes('index.js:OK')
145 const hasNM = check.includes('node_modules:OK')
146 const hasByArchNM = check.includes('by-arch-nm:OK')
147 if (hasIndex) ok('index.js present')
148 else fail('index.js', 'MISSING')
149 if (hasNM || hasByArchNM) ok('node_modules present' + (hasByArchNM ? ' (by-arch)' : ' (root)'))
150 else fail('node_modules', 'MISSING')
151} catch (err) {
152 fail('file check', err.message)
153}
154
155console.log('')
156console.log(`Results: ${passed} passed, ${failed} failed`)
157process.exit(failed > 0 ? 1 : 0)