OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / deploy-sync-test.js

158 lines · javascript ref:
#!/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)