OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / routes / deploy.js

292 lines · javascript ref:
/**
 * Deploy Routes
 *
 * Handles:
 * - GET  /deploy                         - Deploy settings page (HTML)
 * - GET  /api/deploy/config/:repo        - Get deploy config (JSON)
 * - POST /api/deploy/config/:repo        - Save deploy config (JSON)
 * - POST /api/deploy/trigger/:repo       - Execute deploy (JSON)
 * - GET  /api/deploy/log/:repo           - Deploy history (JSON)
 * - GET  /api/deploy/status/:repo        - Current version + seed status (JSON)
 * - GET  /api/deploy/registry            - SharedDeployRegistry status (JSON)
 * - GET  /api/deploy/registry/apps       - All apps in deploy registry (JSON)
 */

import { html, json, badRequest, readJson } from '../response.js'
import { requireAdmin } from '../middleware/admin.js'
import { renderDeployDashboard } from '../deploy-ui.js'
import { executeDeploy, getDeployLog, notifyPeersOfDeploy, broadcastRefs } from '../ci/deploy-hook.js'

export function registerDeployRoutes (router) {
  // Deploy dashboard — serve HTML to everyone, JS checks permissions client-side
  router.get('/deploy', serveDeployDashboard)
  router.get('/deploy/', serveDeployDashboard)

  // Deploy config API
  router.get('/api/deploy/config/:repo', requireAdmin, handleGetDeployConfig)
  router.post('/api/deploy/config/:repo', requireAdmin, handleSaveDeployConfig)

  // Deploy trigger
  router.post('/api/deploy/trigger/:repo', requireAdmin, handleTriggerDeploy)

  // Deploy log
  router.get('/api/deploy/log/:repo', requireAdmin, handleGetDeployLog)

  // Deploy status
  router.get('/api/deploy/status/:repo', requireAdmin, handleGetDeployStatus)

  // SharedDeployRegistry endpoints
  router.get('/api/deploy/registry', requireAdmin, handleGetRegistry)
  router.get('/api/deploy/registry/apps', requireAdmin, handleGetRegistryApps)
}

async function serveDeployDashboard (req, res, server) {
  const { repos } = server.repoData
  const { pathPrefix } = server.config
  const repoList = repos.map(r => ({
    name: r.name,
    deploy: r.deploy || null
  }))
  const content = renderDeployDashboard(repoList, pathPrefix)
  html(res, content)
}

async function handleGetDeployConfig (req, res, server) {
  const { repoRegistry } = server.repoData
  try {
    const { repo: repoName } = req.params
    const repo = await repoRegistry.get(repoName)

    if (!repo) {
      json(res, { error: `Repository "${repoName}" not found` }, 404)
      return
    }

    json(res, {
      name: repoName,
      deploy: repo.deploy || null
    })
  } catch (err) {
    console.error('[Deploy] Config get error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleSaveDeployConfig (req, res, server) {
  const { repoRegistry } = server.repoData
  try {
    const { repo: repoName } = req.params
    const body = await readJson(req)

    const repo = await repoRegistry.get(repoName)
    if (!repo) {
      json(res, { error: `Repository "${repoName}" not found` }, 404)
      return
    }

    if (!body.channel) {
      badRequest(res, 'channel is required')
      return
    }

    repo.deploy = {
      enabled: body.enabled !== false,
      channel: body.channel,
      mode: body.mode || 'git', // 'git' (default) or 'legacy'
      autoRelease: body.autoRelease !== false,
      autoSeed: body.autoSeed !== false,
      autoDeploy: body.autoDeploy !== false,
      configuredAt: Date.now(),
      lastDeploy: repo.deploy?.lastDeploy || null,
      lastVersion: repo.deploy?.lastVersion || null,
      lastKey: repo.deploy?.lastKey || null,
      lastCommitHash: repo.deploy?.lastCommitHash || null,
      lastBranch: repo.deploy?.lastBranch || null
    }

    await repoRegistry.register(repo)
    server.repoData.repos = await repoRegistry.listAll()

    console.log(`[Deploy] Configured ${repoName} → channel: ${body.channel}, mode: ${repo.deploy.mode}`)

    json(res, { success: true, deploy: repo.deploy })
  } catch (err) {
    console.error('[Deploy] Config save error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleTriggerDeploy (req, res, server) {
  const { repoRegistry } = server.repoData
  try {
    const { repo: repoName } = req.params
    const body = await readJson(req)

    // Check if deploy is configured for this repo
    const repo = await repoRegistry.get(repoName)
    if (!repo) {
      json(res, { error: `Repository "${repoName}" not found` }, 404)
      return
    }

    if (!repo.deploy || !repo.deploy.enabled) {
      // No deploy config — still notify federation peers to sync refs
      broadcastRefs(server, repoName)
      notifyPeersOfDeploy(server, repoName).catch(() => {})
      json(res, { success: true, action: 'notify-only', repoName })
      return
    }

    // Self-deploy guard: don't run pear stage/release on the running server.
    // Deploying pear-git to itself kills the process. Instead, broadcast
    // refs so peers fetch via GitSync + self-update mechanism.
    const selfRepoName = server._selfRepoName || null
    if (selfRepoName && repoName === selfRepoName) {
      broadcastRefs(server, repoName)
      notifyPeersOfDeploy(server, repoName).catch(() => {})
      json(res, { success: true, action: 'self-repo-broadcast', repoName,
        message: 'Self-deploy skipped — peers notified via GitSync' })
      return
    }

    const result = await executeDeploy(server, repoName, {
      release: body.release,
      seed: body.seed,
      legacy: body.legacy
    })

    json(res, { success: true, ...result })
  } catch (err) {
    console.error('[Deploy] Trigger error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleGetDeployLog (req, res, server) {
  try {
    const { repo: repoName } = req.params
    const entries = await getDeployLog(server, repoName)
    json(res, entries)
  } catch (err) {
    console.error('[Deploy] Log error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleGetDeployStatus (req, res, server) {
  const { repoRegistry } = server.repoData
  const { deployer, deployRegistry } = server.deployData
  try {
    const { repo: repoName } = req.params
    const repo = await repoRegistry.get(repoName)

    if (!repo) {
      json(res, { error: `Repository "${repoName}" not found` }, 404)
      return
    }

    const config = repo.deploy
    if (!config || !config.enabled) {
      json(res, { configured: false })
      return
    }

    const isGitMode = config.mode !== 'legacy'

    // Git mode: report commit hash and branch
    if (isGitMode) {
      // Also check deploy registry for cross-machine state
      let registryApp = null
      if (deployRegistry) {
        try {
          registryApp = await deployRegistry.getApp(repoName)
        } catch {}
      }

      json(res, {
        configured: true,
        mode: 'git',
        channel: config.channel,
        lastDeploy: config.lastDeploy,
        lastCommitHash: config.lastCommitHash || null,
        lastBranch: config.lastBranch || 'release',
        registryApp
      })
      return
    }

    // Legacy mode: report version + seeding status
    const seeding = deployer.isSeeding(config.channel)

    let info = null
    try {
      info = await deployer.info(config.channel)
    } catch {}

    json(res, {
      configured: true,
      mode: 'legacy',
      channel: config.channel,
      lastDeploy: config.lastDeploy,
      lastVersion: config.lastVersion,
      lastKey: config.lastKey,
      seeding,
      info
    })
  } catch (err) {
    console.error('[Deploy] Status error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleGetRegistry (req, res, server) {
  const { deployRegistry } = server.deployData
  try {
    if (!deployRegistry) {
      json(res, { available: false, reason: 'Deploy registry not initialized' })
      return
    }

    const servers = await deployRegistry.listServers()
    const apps = await deployRegistry.listApps()

    json(res, {
      available: true,
      bootstrapKey: deployRegistry.bootstrapKeyHex,
      writable: deployRegistry.writable,
      servers,
      appCount: apps.length
    })
  } catch (err) {
    console.error('[Deploy] Registry error:', err)
    json(res, { error: err.message }, 500)
  }
}

async function handleGetRegistryApps (req, res, server) {
  const { deployRegistry } = server.deployData
  try {
    if (!deployRegistry) {
      json(res, { error: 'Deploy registry not initialized' }, 503)
      return
    }

    const apps = await deployRegistry.listApps()
    json(res, apps)
  } catch (err) {
    console.error('[Deploy] Registry apps error:', err)
    json(res, { error: err.message }, 500)
  }
}

export {
  serveDeployDashboard,
  handleGetDeployConfig,
  handleSaveDeployConfig,
  handleTriggerDeploy,
  handleGetDeployLog,
  handleGetDeployStatus,
  handleGetRegistry,
  handleGetRegistryApps
}