#!/usr/bin/env bare
/**
* Deploy Sync Test
*
* Verifies that a deploy on tabertron propagates to stortron.
* Run from tabertron after a deploy completes.
*
* Usage:
* bare test/deploy-sync-test.js <repo> [stortron-host]
*
* Example:
* bare test/deploy-sync-test.js hyper-tps stortron.local
* bare test/deploy-sync-test.js hyper-overlay
*
* Tests:
* 1. Local release ref exists
* 2. Remote bare repo has the release branch
* 3. Remote release ref matches local (federation synced)
* 4. Remote app install has the latest version
*/
import { spawn } from 'bare-subprocess'
import process from 'bare-process'
import os from 'bare-os'
const REPO = process.argv[2]
const REMOTE = process.argv[3] || 'stortron.local'
if (!REPO) {
console.log('Usage: bare test/deploy-sync-test.js <repo> [remote-host]')
process.exit(1)
}
let passed = 0
let failed = 0
function ok (name, detail) { passed++; console.log(` ✓ ${name}${detail ? ' — ' + detail : ''}`) }
function fail (name, reason) { failed++; console.log(` ✗ ${name}: ${reason}`) }
function exec (cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
const timeout = opts.timeout || 30000
let done = false
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] })
const timer = setTimeout(() => {
if (done) return
done = true
try { child.kill('SIGKILL') } catch {}
reject(new Error('timeout'))
}, timeout)
let stdout = ''
let stderr = ''
child.stdout.on('data', (d) => { stdout += d.toString() })
child.stderr.on('data', (d) => { stderr += d.toString() })
child.on('exit', (code) => {
if (done) return
done = true
clearTimeout(timer)
if (code === 0) resolve(stdout.trim())
else reject(new Error(stderr.trim() || `exit ${code}`))
})
child.on('error', (err) => {
if (done) return
done = true
clearTimeout(timer)
reject(err)
})
})
}
async function sshExec (host, cmd) {
return exec('ssh', [host, cmd], { timeout: 15000 })
}
console.log('Deploy Sync Test')
console.log('================')
console.log(`Repo: ${REPO}`)
console.log(`Local: ${os.hostname()}`)
console.log(`Remote: ${REMOTE}`)
console.log('')
// Test 1: Local release ref
console.log('1. Local release ref')
let localRef
try {
localRef = await exec('git', [
'--git-dir', `${os.homedir()}/git-repos/${REPO}.git`,
'rev-parse', 'refs/heads/release'
])
ok('local release', localRef.slice(0, 12))
} catch (err) {
fail('local release', err.message)
console.log('\nResults: 0 passed, 1 failed — no release branch')
process.exit(1)
}
// Test 2: Remote bare repo has release branch
console.log('2. Remote bare repo release ref')
let remoteRef
try {
remoteRef = await sshExec(REMOTE,
`GIT_DIR=$HOME/git-repos/${REPO}.git git rev-parse refs/heads/release 2>/dev/null`)
ok('remote bare repo', remoteRef.slice(0, 12))
} catch (err) {
fail('remote bare repo', err.message)
}
// Test 3: Refs match (federation synced)
console.log('3. Release refs match')
if (localRef && remoteRef) {
if (localRef === remoteRef) {
ok('refs match', localRef.slice(0, 12))
} else {
fail('refs DIVERGED',
`local=${localRef.slice(0, 12)} remote=${remoteRef.slice(0, 12)}`)
}
} else {
fail('refs match', 'missing ref(s)')
}
// Test 4: Remote app install version
console.log('4. Remote app install version')
try {
const installRef = await sshExec(REMOTE,
`cd $HOME/.local/share/${REPO} && git rev-parse HEAD 2>/dev/null`)
if (installRef === localRef) {
ok('install current', installRef.slice(0, 12))
} else {
fail('install BEHIND',
`install=${installRef.slice(0, 12)} release=${localRef.slice(0, 12)}`)
}
} catch (err) {
fail('install not found', err.message)
}
// Test 5: Key files present (for by-arch repos)
console.log('5. Remote install has key files')
try {
const check = await sshExec(REMOTE,
`ls $HOME/.local/share/${REPO}/index.js 2>/dev/null && echo "index.js:OK"; ` +
`ls $HOME/.local/share/${REPO}/node_modules/.package-lock.json 2>/dev/null && echo "node_modules:OK"; ` +
`ls $HOME/.local/share/${REPO}/by-arch/*/app/${REPO}/node_modules/.package-lock.json 2>/dev/null && echo "by-arch-nm:OK"; ` +
`echo "done"`)
const hasIndex = check.includes('index.js:OK')
const hasNM = check.includes('node_modules:OK')
const hasByArchNM = check.includes('by-arch-nm:OK')
if (hasIndex) ok('index.js present')
else fail('index.js', 'MISSING')
if (hasNM || hasByArchNM) ok('node_modules present' + (hasByArchNM ? ' (by-arch)' : ' (root)'))
else fail('node_modules', 'MISSING')
} catch (err) {
fail('file check', err.message)
}
console.log('')
console.log(`Results: ${passed} passed, ${failed} failed`)
process.exit(failed > 0 ? 1 : 0)