chore: sync workspace — configs, docs, scripts, CI, pnpm, submodules
Some checks failed
Deploy to Phoenix / validate (push) Failing after 15s
Deploy to Phoenix / deploy (push) Has been skipped

- Submodule pins: dbis_core, cross-chain-pmm-lps, mcp-proxmox (local, push may be pending), metamask-integration, smom-dbis-138
- Atomic swap + cross-chain-pmm-lops-publish, deploy-portal workflow, phoenix deploy-targets, routing/aggregator matrices
- Docs, token-lists, forge proxy, phoenix API, runbooks, verify scripts

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-21 22:01:33 -07:00
parent e6bc7a6d7c
commit b8613905bd
231 changed files with 31657 additions and 2184 deletions

View File

@@ -4,6 +4,7 @@ PORT=4001
GITEA_URL=https://gitea.d-bis.org
GITEA_TOKEN=
PHOENIX_DEPLOY_SECRET=
PHOENIX_WEBHOOK_DEPLOY_ENABLED=0
# Proxmox (for Infra/VE API; omit for stub responses)
PROXMOX_HOST=
@@ -32,4 +33,6 @@ PHOENIX_PARTNER_KEYS=
# Optional: override path for GET /api/v1/public-sector/programs (else bundled copy, repo config/, or ../config/)
PUBLIC_SECTOR_MANIFEST_PATH=
# Optional: proxmox repo root on host (manifest = $PHOENIX_REPO_ROOT/config/public-sector-program-manifest.json)
PHOENIX_REPO_ROOT=
PHOENIX_REPO_ROOT=/home/intlc/projects/proxmox
# Optional: deploy target config file (defaults to phoenix-deploy-api/deploy-targets.json)
DEPLOY_TARGETS_PATH=

View File

@@ -28,7 +28,9 @@ npm start
```bash
sudo nano /opt/phoenix-deploy-api/.env
# Set GITEA_TOKEN=... and optionally PHOENIX_DEPLOY_SECRET
# Set GITEA_TOKEN=..., PHOENIX_DEPLOY_SECRET=..., PHOENIX_REPO_ROOT=...
# Optional: PHOENIX_WEBHOOK_DEPLOY_ENABLED=1 only for repos using webhook-triggered deploys
# Optional: SANKOFA_PORTAL_SRC=/home/intlc/projects/Sankofa/portal for the portal-live target
sudo systemctl restart phoenix-deploy-api
```

View File

@@ -1,13 +1,14 @@
# Phoenix Deploy API
Gitea webhook receiver and deploy endpoint stub for Gitea → Phoenix deployment integration.
Gitea webhook receiver and deploy endpoint for Gitea → Phoenix / Proxmox deployment integration.
## Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | /webhook/gitea | Receives Gitea push/tag/PR webhooks |
| POST | /api/deploy | Deploy request (repo, branch, target) |
| POST | /webhook/gitea | Receives Gitea push/tag/PR webhooks; executes deploys only when `PHOENIX_WEBHOOK_DEPLOY_ENABLED=1` |
| POST | /api/deploy | Deploy request (repo, branch, target); resolves a target from `deploy-targets.json` and runs its command |
| GET | /api/deploy-targets | Lists configured deploy targets and whether each has a health check |
| GET | /api/v1/infra/nodes | Cluster nodes (Proxmox; stub if PROXMOX_* unset) |
| GET | /api/v1/infra/storage | Storage pools (Proxmox; stub if unset) |
| GET | /api/v1/ve/vms | List VMs/CTs (optional `?node=`) |
@@ -31,10 +32,11 @@ Copy `.env.example` to `.env` and set `GITEA_TOKEN` (and optionally `PHOENIX_DEP
| GITEA_URL | https://gitea.d-bis.org | Gitea instance URL |
| GITEA_TOKEN | | Token for commit status API |
| PHOENIX_DEPLOY_SECRET | | Optional secret for webhook/deploy auth |
| PROXMOX_HOST | | Proxmox host (IP or hostname) for API Railing |
| PHOENIX_WEBHOOK_DEPLOY_ENABLED | 0 | Set to 1 to allow `/webhook/gitea` to execute the default target on matching pushes |
| PROXMOX_HOST | proxmox-api.d-bis.org | Proxmox host (IP or hostname) for API Railing |
| PROXMOX_PORT | 8006 | Proxmox API port |
| PROXMOX_USER | root@pam | Proxmox API user |
| PROXMOX_TOKEN_NAME | | Proxmox API token name |
| PROXMOX_TOKEN_NAME | | Proxmox API token name; bare token name preferred, full token id also accepted |
| PROXMOX_TOKEN_VALUE | | Proxmox API token secret |
| PROXMOX_TLS_VERIFY | 1 | Set to 0 to allow self-signed Proxmox certs |
| PHOENIX_VE_LIFECYCLE_ENABLED | 0 | Set to 1 to enable VM/CT start/stop/reboot |
@@ -45,6 +47,7 @@ Copy `.env.example` to `.env` and set `GITEA_TOKEN` (and optionally `PHOENIX_DEP
| PHOENIX_PARTNER_KEYS | | Comma-separated API keys for /api/v1/* (optional) |
| PUBLIC_SECTOR_MANIFEST_PATH | | Override JSON path for `/api/v1/public-sector/programs` |
| PHOENIX_REPO_ROOT | | Proxmox repo root; loads `config/public-sector-program-manifest.json` if present |
| DEPLOY_TARGETS_PATH | | Override deploy target file; default is `phoenix-deploy-api/deploy-targets.json` |
**Program manifest:** From a full repo checkout, the file is `config/public-sector-program-manifest.json`. `scripts/install-systemd.sh` copies it next to `server.js` on `/opt/phoenix-deploy-api` so the endpoint works without a full tree.
@@ -52,11 +55,13 @@ Copy `.env.example` to `.env` and set `GITEA_TOKEN` (and optionally `PHOENIX_DEP
In Gitea: Repository → Settings → Webhooks → Add Webhook
- **URL:** `https://phoenix-api-host/api/webhook/gitea` (or your Phoenix API URL)
- **URL:** `https://phoenix-api-host/webhook/gitea` (or your Phoenix API URL)
- **Content type:** application/json
- **Events:** Push events, Tag creation (and optionally Pull requests)
- **Secret:** Optional, set PHOENIX_DEPLOY_SECRET to match
Use webhook-triggered deploys only for repos that are not already deploying through Gitea Actions, unless you intentionally want both paths.
## Deploy API (Trigger from Gitea Actions)
```bash
@@ -66,9 +71,56 @@ curl -X POST "https://phoenix-api-host/api/deploy" \
-d '{"repo":"d-bis/proxmox","branch":"main","sha":"abc123","target":"default"}'
```
The API returns `404` when no matching deploy target exists for `{repo, branch, target}` and `500` when the target command fails.
If a target defines `healthcheck`, the deploy is only marked successful after the post-deploy URL check passes.
## Deploy target configuration
Targets live in [`deploy-targets.json`](deploy-targets.json). Each entry maps a `{repo, branch, target}` tuple to a command array and working directory.
Example:
```json
{
"repo": "d-bis/proxmox",
"branch": "main",
"target": "default",
"cwd": "${PHOENIX_REPO_ROOT}",
"command": ["bash", "scripts/deployment/deploy-phoenix-deploy-api-to-dev-vm.sh", "--apply", "--start-ct"],
"required_env": ["PHOENIX_REPO_ROOT"]
}
```
Use this to promote different repos to different VMIDs or CTs by adding more entries.
Optional per-target health check:
```json
{
"healthcheck": {
"url": "http://192.168.11.59:4001/health",
"expect_status": 200,
"expect_body_includes": "phoenix-deploy-api",
"attempts": 8,
"delay_ms": 3000,
"timeout_ms": 10000
}
}
```
## Manual smoke trigger
From the repo root:
```bash
bash scripts/dev-vm/trigger-phoenix-deploy.sh
```
This calls `/api/deploy` directly using `PHOENIX_DEPLOY_URL` and `PHOENIX_DEPLOY_TOKEN` from root `.env`.
## Integration with Sankofa Phoenix
This service is a standalone stub. Full deployment logic should be implemented in the Sankofa Phoenix API (VMID 8600). Migrate the webhook handler and deploy logic into the Phoenix API when ready.
This service is still standalone, but it now executes real target commands. If you later fold it into the Sankofa Phoenix API (VMID 8600), keep the same target-file pattern so repo-to-VM routing stays declarative.
## OpenAPI / Swagger

View File

@@ -0,0 +1,101 @@
{
"defaults": {
"timeout_sec": 1800
},
"targets": [
{
"repo": "d-bis/proxmox",
"branch": "main",
"target": "default",
"description": "Deploy the Phoenix deploy API bundle to the dev VM on Proxmox.",
"cwd": "${PHOENIX_REPO_ROOT}",
"command": [
"bash",
"scripts/deployment/deploy-phoenix-deploy-api-to-dev-vm.sh",
"--apply",
"--start-ct"
],
"required_env": [
"PHOENIX_REPO_ROOT"
],
"healthcheck": {
"url": "http://192.168.11.59:4001/health",
"expect_status": 200,
"expect_body_includes": "phoenix-deploy-api",
"attempts": 8,
"delay_ms": 3000,
"timeout_ms": 10000
}
},
{
"repo": "d-bis/proxmox",
"branch": "main",
"target": "portal-live",
"description": "Deploy the Sankofa portal to CT 7801 on Proxmox.",
"cwd": "${PHOENIX_REPO_ROOT}",
"command": [
"bash",
"scripts/deployment/sync-sankofa-portal-7801.sh"
],
"required_env": [
"PHOENIX_REPO_ROOT",
"SANKOFA_PORTAL_SRC"
],
"healthcheck": {
"url": "http://192.168.11.51:3000/",
"expect_status": 200,
"expect_body_includes": "<html",
"attempts": 10,
"delay_ms": 5000,
"timeout_ms": 10000
}
},
{
"repo": "d-bis/proxmox",
"branch": "master",
"target": "default",
"description": "Deploy the Phoenix deploy API bundle to the dev VM on Proxmox.",
"cwd": "${PHOENIX_REPO_ROOT}",
"command": [
"bash",
"scripts/deployment/deploy-phoenix-deploy-api-to-dev-vm.sh",
"--apply",
"--start-ct"
],
"required_env": [
"PHOENIX_REPO_ROOT"
],
"healthcheck": {
"url": "http://192.168.11.59:4001/health",
"expect_status": 200,
"expect_body_includes": "phoenix-deploy-api",
"attempts": 8,
"delay_ms": 3000,
"timeout_ms": 10000
}
},
{
"repo": "d-bis/proxmox",
"branch": "master",
"target": "portal-live",
"description": "Deploy the Sankofa portal to CT 7801 on Proxmox.",
"cwd": "${PHOENIX_REPO_ROOT}",
"command": [
"bash",
"scripts/deployment/sync-sankofa-portal-7801.sh"
],
"required_env": [
"PHOENIX_REPO_ROOT",
"SANKOFA_PORTAL_SRC"
],
"healthcheck": {
"url": "http://192.168.11.51:3000/",
"expect_status": 200,
"expect_body_includes": "<html",
"attempts": 10,
"delay_ms": 5000,
"timeout_ms": 10000
}
}
]
}

View File

@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Phoenix Deploy API / Phoenix API Railing
description: |
Gitea webhook, deploy stub, and Phoenix API Railing (Infra, VE, Health).
Gitea webhook, deploy execution API, and Phoenix API Railing (Infra, VE, Health).
Optional partner API key for /api/v1/* when PHOENIX_PARTNER_KEYS is set.
version: 1.0.0
@@ -38,13 +38,16 @@ paths:
post:
tags: [Webhook]
summary: Gitea webhook receiver
description: |
Validates webhook signatures when `PHOENIX_DEPLOY_SECRET` is set.
Executes the default deploy target only when `PHOENIX_WEBHOOK_DEPLOY_ENABLED=1`.
requestBody:
required: true
content:
application/json:
schema: { type: object }
responses:
'200': { description: Accepted }
'200': { description: Accepted or executed depending on webhook deploy mode }
'400': { description: No payload }
'401': { description: Invalid signature }
@@ -64,8 +67,17 @@ paths:
target: { type: string }
sha: { type: string }
responses:
'202': { description: Accepted }
'200': { description: Deploy completed successfully }
'401': { description: Unauthorized }
'404': { description: No matching deploy target }
'500': { description: Deploy command or health check failed }
/api/deploy-targets:
get:
tags: [Webhook]
summary: List configured deploy targets
responses:
'200': { description: Target list }
/api/v1/public-sector/programs:
get:

View File

@@ -1,7 +1,7 @@
{
"name": "phoenix-deploy-api",
"version": "1.0.0",
"description": "Phoenix deploy API stub and Gitea webhook receiver for GiteaPhoenix deployment integration",
"description": "Phoenix deploy API and Gitea webhook receiver for Gitea to Phoenix and Proxmox deployment integration",
"type": "module",
"main": "server.js",
"scripts": {

View File

@@ -17,7 +17,8 @@ fi
echo "Installing Phoenix Deploy API to $TARGET ..."
mkdir -p "$TARGET"
cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$APP_DIR/package-lock.json" "$TARGET/" 2>/dev/null || cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$TARGET/"
cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$APP_DIR/package-lock.json" "$APP_DIR/deploy-targets.json" "$TARGET/" 2>/dev/null || \
cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$APP_DIR/deploy-targets.json" "$TARGET/"
# Program manifest for GET /api/v1/public-sector/programs (server loads from cwd-adjacent copy on /opt)
if [[ -f "$REPO_ROOT/config/public-sector-program-manifest.json" ]]; then
cp -a "$REPO_ROOT/config/public-sector-program-manifest.json" "$TARGET/public-sector-program-manifest.json"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Phoenix Deploy API — Gitea webhook receiver, deploy stub, and Phoenix API Railing (Infra/VE)
* Phoenix Deploy API — Gitea webhook receiver, deploy execution API, and Phoenix API Railing (Infra/VE)
*
* Endpoints:
* POST /webhook/gitea — Receives Gitea push/tag/PR webhooks
@@ -19,6 +19,8 @@
import crypto from 'crypto';
import https from 'https';
import path from 'path';
import { promisify } from 'util';
import { execFile as execFileCallback } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import express from 'express';
@@ -42,6 +44,13 @@ const PROMETHEUS_URL = (process.env.PROMETHEUS_URL || 'http://localhost:9090').r
const PHOENIX_WEBHOOK_URL = process.env.PHOENIX_WEBHOOK_URL || '';
const PHOENIX_WEBHOOK_SECRET = process.env.PHOENIX_WEBHOOK_SECRET || '';
const PARTNER_KEYS = (process.env.PHOENIX_PARTNER_KEYS || '').split(',').map((k) => k.trim()).filter(Boolean);
const WEBHOOK_DEPLOY_ENABLED = process.env.PHOENIX_WEBHOOK_DEPLOY_ENABLED === '1' || process.env.PHOENIX_WEBHOOK_DEPLOY_ENABLED === 'true';
const execFile = promisify(execFileCallback);
function expandEnvTokens(value) {
if (typeof value !== 'string') return value;
return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, key) => process.env[key] || '');
}
/**
* Manifest resolution order:
@@ -63,15 +72,249 @@ function resolvePublicSectorManifestPath() {
return path.join(__dirname, '..', 'config', 'public-sector-program-manifest.json');
}
function resolveDeployTargetsPath() {
const override = (process.env.DEPLOY_TARGETS_PATH || '').trim();
if (override && existsSync(override)) return override;
const bundled = path.join(__dirname, 'deploy-targets.json');
if (existsSync(bundled)) return bundled;
return bundled;
}
function loadDeployTargetsConfig() {
const configPath = resolveDeployTargetsPath();
if (!existsSync(configPath)) {
return {
path: configPath,
defaults: {},
targets: [],
};
}
const raw = readFileSync(configPath, 'utf8');
const parsed = JSON.parse(raw);
return {
path: configPath,
defaults: parsed.defaults || {},
targets: Array.isArray(parsed.targets) ? parsed.targets : [],
};
}
function findDeployTarget(repo, branch, requestedTarget) {
const config = loadDeployTargetsConfig();
const wantedTarget = requestedTarget || 'default';
const match = config.targets.find((entry) => {
if (entry.repo !== repo) return false;
if ((entry.branch || 'main') !== branch) return false;
return (entry.target || 'default') === wantedTarget;
});
return { config, match, wantedTarget };
}
async function sleep(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function verifyHealthCheck(healthcheck) {
if (!healthcheck || !healthcheck.url) return null;
const attempts = Math.max(1, Number(healthcheck.attempts || 1));
const delayMs = Math.max(0, Number(healthcheck.delay_ms || 0));
const timeoutMs = Math.max(1000, Number(healthcheck.timeout_ms || 10000));
const expectedStatus = Number(healthcheck.expect_status || 200);
const expectBodyIncludes = healthcheck.expect_body_includes || '';
let lastError = null;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const res = await fetch(healthcheck.url, { signal: controller.signal });
const body = await res.text();
clearTimeout(timeout);
if (res.status !== expectedStatus) {
throw new Error(`Expected HTTP ${expectedStatus}, got ${res.status}`);
}
if (expectBodyIncludes && !body.includes(expectBodyIncludes)) {
throw new Error(`Health body missing expected text: ${expectBodyIncludes}`);
}
return {
ok: true,
url: healthcheck.url,
status: res.status,
attempt,
};
} catch (err) {
lastError = err;
if (attempt < attempts && delayMs > 0) {
await sleep(delayMs);
}
}
}
throw new Error(`Health check failed for ${healthcheck.url}: ${lastError?.message || 'unknown error'}`);
}
async function runDeployTarget(definition, configDefaults, context) {
if (!Array.isArray(definition.command) || definition.command.length === 0) {
throw new Error('Deploy target is missing a command array');
}
const cwd = expandEnvTokens(definition.cwd || configDefaults.cwd || process.cwd());
const timeoutSeconds = Number(definition.timeout_sec || configDefaults.timeout_sec || 1800);
const timeout = Number.isFinite(timeoutSeconds) && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 1800 * 1000;
const command = definition.command.map((part) => expandEnvTokens(part));
const missingEnv = (definition.required_env || []).filter((key) => !process.env[key]);
if (missingEnv.length > 0) {
throw new Error(`Missing required env for deploy target: ${missingEnv.join(', ')}`);
}
if (!existsSync(cwd)) {
throw new Error(`Deploy working directory does not exist: ${cwd}`);
}
const childEnv = {
...process.env,
PHOENIX_DEPLOY_REPO: context.repo,
PHOENIX_DEPLOY_BRANCH: context.branch,
PHOENIX_DEPLOY_SHA: context.sha || '',
PHOENIX_DEPLOY_TARGET: context.target,
PHOENIX_DEPLOY_TRIGGER: context.trigger,
};
const { stdout, stderr } = await execFile(command[0], command.slice(1), {
cwd,
env: childEnv,
timeout,
maxBuffer: 10 * 1024 * 1024,
});
const healthcheck = await verifyHealthCheck(definition.healthcheck || configDefaults.healthcheck || null);
return {
cwd,
command,
stdout: stdout || '',
stderr: stderr || '',
timeout_sec: timeoutSeconds,
healthcheck,
};
}
async function executeDeploy({ repo, branch = 'main', target = 'default', sha = '', trigger = 'api' }) {
if (!repo) {
const error = new Error('repo required');
error.statusCode = 400;
error.payload = { error: error.message };
throw error;
}
const [owner, repoName] = repo.includes('/') ? repo.split('/') : ['d-bis', repo];
const commitSha = sha || '';
const requestedTarget = target || 'default';
const { config, match, wantedTarget } = findDeployTarget(repo, branch, requestedTarget);
if (!match) {
const error = new Error('Deploy target not configured');
error.statusCode = 404;
error.payload = {
error: error.message,
repo,
branch,
target: wantedTarget,
config_path: config.path,
};
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, commitSha, 'failure', `No deploy target for ${repo} ${branch} ${wantedTarget}`);
}
throw error;
}
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, commitSha, 'pending', 'Phoenix deployment in progress');
}
console.log(`[deploy] ${repo} branch=${branch} target=${wantedTarget} sha=${commitSha} trigger=${trigger}`);
let deployResult = null;
let deployError = null;
try {
deployResult = await runDeployTarget(match, config.defaults, {
repo,
branch,
sha: commitSha,
target: wantedTarget,
trigger,
});
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, commitSha, 'success', `Deployed to ${wantedTarget}`);
}
return {
status: 'completed',
repo,
branch,
target: wantedTarget,
config_path: config.path,
command: deployResult.command,
cwd: deployResult.cwd,
stdout: deployResult.stdout,
stderr: deployResult.stderr,
healthcheck: deployResult.healthcheck,
};
} catch (err) {
deployError = err;
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, commitSha, 'failure', `Deploy failed: ${err.message.slice(0, 120)}`);
}
err.statusCode = err.statusCode || 500;
err.payload = err.payload || {
error: err.message,
repo,
branch,
target: wantedTarget,
config_path: config.path,
};
throw err;
} finally {
if (PHOENIX_WEBHOOK_URL) {
const payload = {
event: 'deploy.completed',
repo,
branch,
target: wantedTarget,
sha: commitSha,
success: Boolean(deployResult),
command: deployResult?.command,
cwd: deployResult?.cwd,
error: deployError?.message || null,
};
const body = JSON.stringify(payload);
const sig = crypto.createHmac('sha256', PHOENIX_WEBHOOK_SECRET || '').update(body).digest('hex');
fetch(PHOENIX_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Phoenix-Signature': `sha256=${sig}` },
body,
}).catch((e) => console.error('[webhook] outbound failed', e.message));
}
}
}
const httpsAgent = new https.Agent({ rejectUnauthorized: process.env.PROXMOX_TLS_VERIFY !== '0' });
function formatProxmoxAuthHeader(user, tokenName, tokenValue) {
if (tokenName.includes('!')) {
return `PVEAPIToken=${tokenName}=${tokenValue}`;
}
return `PVEAPIToken=${user}!${tokenName}=${tokenValue}`;
}
async function proxmoxRequest(endpoint, method = 'GET', body = null) {
const baseUrl = `https://${PROXMOX_HOST}:${PROXMOX_PORT}/api2/json`;
const url = `${baseUrl}${endpoint}`;
const options = {
method,
headers: {
Authorization: `PVEAPIToken=${PROXMOX_USER}!${PROXMOX_TOKEN_NAME}=${PROXMOX_TOKEN_VALUE}`,
Authorization: formatProxmoxAuthHeader(PROXMOX_USER, PROXMOX_TOKEN_NAME, PROXMOX_TOKEN_VALUE),
'Content-Type': 'application/json',
},
agent: httpsAgent,
@@ -162,12 +405,44 @@ app.post('/webhook/gitea', async (req, res) => {
if (action === 'push' || (action === 'synchronize' && payload.pull_request)) {
if (branch === 'main' || branch === 'master' || ref.startsWith('refs/tags/')) {
if (sha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, sha, 'pending', 'Phoenix deployment triggered');
if (!WEBHOOK_DEPLOY_ENABLED) {
return res.status(200).json({
received: true,
repo: fullName,
branch,
sha,
deployed: false,
message: 'Webhook accepted; set PHOENIX_WEBHOOK_DEPLOY_ENABLED=1 to execute deploys from webhook events.',
});
}
try {
const result = await executeDeploy({
repo: fullName,
branch,
sha,
target: 'default',
trigger: 'webhook',
});
return res.status(200).json({
received: true,
repo: fullName,
branch,
sha,
deployed: true,
result,
});
} catch (err) {
return res.status(200).json({
received: true,
repo: fullName,
branch,
sha,
deployed: false,
error: err.message,
details: err.payload || null,
});
}
// Stub: enqueue deploy; actual implementation would call Proxmox/deploy logic
console.log(`[deploy-stub] Would deploy ${fullName} branch=${branch} sha=${sha}`);
// Stub: when full deploy runs, call setGiteaCommitStatus(owner, repoName, sha, 'success'|'failure', ...)
}
}
@@ -185,47 +460,36 @@ app.post('/api/deploy', async (req, res) => {
}
const { repo, branch = 'main', target, sha } = req.body;
if (!repo) {
return res.status(400).json({ error: 'repo required' });
try {
const result = await executeDeploy({
repo,
branch,
sha,
target,
trigger: 'api',
});
res.status(200).json(result);
} catch (err) {
res.status(err.statusCode || 500).json(err.payload || { error: err.message });
}
});
const [owner, repoName] = repo.includes('/') ? repo.split('/') : ['d-bis', repo];
const commitSha = sha || '';
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(owner, repoName, commitSha, 'pending', 'Phoenix deployment in progress');
}
console.log(`[deploy] ${repo} branch=${branch} target=${target || 'default'} sha=${commitSha}`);
// Stub: no real deploy yet — report success so Gitea shows green; replace with real deploy + setGiteaCommitStatus on completion
const deploySuccess = true;
if (commitSha && GITEA_TOKEN) {
await setGiteaCommitStatus(
owner,
repoName,
commitSha,
deploySuccess ? 'success' : 'failure',
deploySuccess ? 'Deploy accepted (stub)' : 'Deploy failed (stub)'
);
}
res.status(202).json({
status: 'accepted',
repo,
branch,
target: target || 'default',
message: 'Deploy request queued (stub). Implement full deploy logic in Sankofa Phoenix API.',
app.get('/api/deploy-targets', (req, res) => {
const config = loadDeployTargetsConfig();
const targets = config.targets.map((entry) => ({
repo: entry.repo,
branch: entry.branch || 'main',
target: entry.target || 'default',
description: entry.description || '',
cwd: entry.cwd || config.defaults.cwd || '',
command: entry.command || [],
has_healthcheck: Boolean(entry.healthcheck || config.defaults.healthcheck),
}));
res.json({
config_path: config.path,
count: targets.length,
targets,
});
if (PHOENIX_WEBHOOK_URL) {
const payload = { event: 'deploy.completed', repo, branch, target: target || 'default', sha: commitSha, success: deploySuccess };
const body = JSON.stringify(payload);
const sig = crypto.createHmac('sha256', PHOENIX_WEBHOOK_SECRET || '').update(body).digest('hex');
fetch(PHOENIX_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Phoenix-Signature': `sha256=${sig}` },
body,
}).catch((e) => console.error('[webhook] outbound failed', e.message));
}
});
/**
@@ -474,7 +738,10 @@ app.listen(PORT, () => {
if (!GITEA_TOKEN) console.warn('GITEA_TOKEN not set — commit status updates disabled');
if (!hasProxmox) console.warn('PROXMOX_* not set — Infra/VE API returns stub data');
if (PHOENIX_WEBHOOK_URL) console.log('Outbound webhook enabled:', PHOENIX_WEBHOOK_URL);
if (WEBHOOK_DEPLOY_ENABLED) console.log('Inbound webhook deploy execution enabled');
if (PARTNER_KEYS.length > 0) console.log('Partner API key auth enabled for /api/v1/* (except GET /api/v1/public-sector/programs)');
const mpath = resolvePublicSectorManifestPath();
const dpath = resolveDeployTargetsPath();
console.log(`Public-sector manifest: ${mpath} (${existsSync(mpath) ? 'ok' : 'missing'})`);
console.log(`Deploy targets: ${dpath} (${existsSync(dpath) ? 'ok' : 'missing'})`);
});