| … | … | @@ -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 | |
| 22 | import { spawn } from 'bare-subprocess' |
| 23 | import process from 'bare-process' |
| 24 | import os from 'bare-os' |
| 25 | |
| 26 | const REPO = process.argv[2] |
| 27 | const REMOTE = process.argv[3] || 'stortron.local' |
| 28 | |
| 29 | if (!REPO) { |
| 30 | console.log('Usage: bare test/deploy-sync-test.js <repo> [remote-host]') |
| 31 | process.exit(1) |
| 32 | } |
| 33 | |
| 34 | let passed = 0 |
| 35 | let failed = 0 |
| 36 | |
| 37 | function ok (name, detail) { passed++; console.log(` ✓ ${name}${detail ? ' — ' + detail : ''}`) } |
| 38 | function fail (name, reason) { failed++; console.log(` ✗ ${name}: ${reason}`) } |
| 39 | |
| 40 | function 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 | |
| 71 | async function sshExec (host, cmd) { |
| 72 | return exec('ssh', [host, cmd], { timeout: 15000 }) |
| 73 | } |
| 74 | |
| 75 | console.log('Deploy Sync Test') |
| 76 | console.log('================') |
| 77 | console.log(`Repo: ${REPO}`) |
| 78 | console.log(`Local: ${os.hostname()}`) |
| 79 | console.log(`Remote: ${REMOTE}`) |
| 80 | console.log('') |
| 81 | |
| 82 | // Test 1: Local release ref |
| 83 | console.log('1. Local release ref') |
| 84 | let localRef |
| 85 | try { |
| 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 |
| 98 | console.log('2. Remote bare repo release ref') |
| 99 | let remoteRef |
| 100 | try { |
| 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) |
| 109 | console.log('3. Release refs match') |
| 110 | if (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 |
| 122 | console.log('4. Remote app install version') |
| 123 | try { |
| 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) |
| 137 | console.log('5. Remote install has key files') |
| 138 | try { |
| 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 | |
| 155 | console.log('') |
| 156 | console.log(`Results: ${passed} passed, ${failed} failed`) |
| 157 | process.exit(failed > 0 ? 1 : 0) |