/**
* 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
}