remove deploy folder

This commit is contained in:
2025-10-08 23:43:36 +07:00
parent ffc7aa50dd
commit 303c4ea19d
-183
View File
@@ -1,183 +0,0 @@
#!/usr/bin/env node
'use strict';
const http = require('http');
const { execFile } = require('child_process');
const url = require('url');
const fs = require('fs');
// Config via environment variables (see README guidance in CI output)
const PORT = parseInt(process.env.PORT || '8080', 10);
const TOKEN = process.env.DEPLOY_WEBHOOK_TOKEN || process.env.WEBHOOK_TOKEN || '';
const STACK_FILE = process.env.STACK_FILE || '/opt/deploy/docker-stack.yml';
const DOCKER_BIN = process.env.DOCKER_BIN || 'docker';
const RATE_LIMIT_SECONDS = parseInt(process.env.RATE_LIMIT_SECONDS || '15', 10);
const LOCK_FILE = process.env.LOCK_FILE || '/tmp/savy-deploy.lock';
const ALLOW_STACKS = (process.env.ALLOW_STACKS || 'savy').split(',').map(s => s.trim()).filter(Boolean);
const ALLOW_SERVICES = (process.env.ALLOW_SERVICES || 'api').split(',').map(s => s.trim()).filter(Boolean);
// Optional registry credentials if your manager node needs to login before pull/deploy
const DOCKER_REGISTRY = process.env.DOCKER_REGISTRY || '';
const DOCKER_REGISTRY_USER = process.env.DOCKER_REGISTRY_USER || '';
const DOCKER_REGISTRY_PASSWORD = process.env.DOCKER_REGISTRY_PASSWORD || '';
let lastDeployTs = 0;
function json(res, code, obj) {
const body = JSON.stringify(obj);
res.writeHead(code, { 'Content-Type': 'application/json' });
res.end(body);
}
function readBody(req) {
return new Promise((resolve, reject) => {
let data = '';
req.on('data', chunk => {
data += chunk;
if (data.length > 1e6) { // 1MB safety limit
try { req.socket.destroy(); } catch {}
reject(new Error('payload too large'));
}
});
req.on('end', () => resolve(data));
req.on('error', reject);
});
}
function validateImage(image) {
// Basic allowlist regex for image:tag
return typeof image === 'string' && /^[a-z0-9._\/:-]+:[a-zA-Z0-9._-]+$/.test(image);
}
function execCmd(cmd, args, envExtra) {
return new Promise((resolve, reject) => {
execFile(cmd, args, { env: { ...process.env, ...envExtra } }, (err, stdout, stderr) => {
if (err) {
err.stdout = stdout;
err.stderr = stderr;
return reject(err);
}
resolve({ stdout, stderr });
});
});
}
function acquireLock() {
try {
const fd = fs.openSync(LOCK_FILE, 'wx');
fs.writeFileSync(fd, String(process.pid));
return fd;
} catch {
return null;
}
}
function releaseLock(fd) {
if (fd) {
try { fs.closeSync(fd); } catch {}
try { fs.unlinkSync(LOCK_FILE); } catch {}
}
}
function unauthorized(res) {
res.writeHead(401);
res.end('unauthorized');
}
const server = http.createServer(async (req, res) => {
const parsed = url.parse(req.url || '', true);
if (req.method === 'GET' && parsed.pathname === '/health') {
return json(res, 200, { status: 'ok', ts: Date.now() });
}
if (req.method !== 'POST' || parsed.pathname !== '/deploy') {
return json(res, 404, { error: 'not_found' });
}
// Bearer token check (do not log the token)
const auth = req.headers['authorization'] || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : '';
if (!TOKEN || token !== TOKEN) {
return unauthorized(res);
}
// Simple rate-limit to avoid rapid repeat-triggering
const now = Date.now();
if (now - lastDeployTs < RATE_LIMIT_SECONDS * 1000) {
return json(res, 429, {
error: 'rate_limited',
retry_after_seconds: Math.ceil((RATE_LIMIT_SECONDS * 1000 - (now - lastDeployTs)) / 1000)
});
}
let body = '';
try {
body = await readBody(req);
} catch {
return json(res, 400, { error: 'read_error' });
}
let payload;
try {
payload = JSON.parse(body || '{}');
} catch {
return json(res, 400, { error: 'invalid_json' });
}
const app = String(payload.app || '');
const image = String(payload.image || '');
const env = String(payload.env || '');
const stack = String(payload.stack || '');
const service = String(payload.service || '');
if (!validateImage(image)) {
return json(res, 400, { error: 'invalid_image' });
}
if (!ALLOW_STACKS.includes(stack)) {
return json(res, 403, { error: 'stack_not_allowed' });
}
if (service && !ALLOW_SERVICES.includes(service)) {
return json(res, 403, { error: 'service_not_allowed' });
}
const lockfd = acquireLock();
if (!lockfd) {
return json(res, 423, { error: 'another_deploy_in_progress' });
}
lastDeployTs = now;
console.log(`[deploy] image=${image} env=${env} stack=${stack} service=${service} at ${new Date().toISOString()}`);
try {
if (DOCKER_REGISTRY && DOCKER_REGISTRY_USER && DOCKER_REGISTRY_PASSWORD) {
console.log('[deploy] docker login registry');
await execCmd(DOCKER_BIN, ['login', '-u', DOCKER_REGISTRY_USER, '-p', DOCKER_REGISTRY_PASSWORD, DOCKER_REGISTRY], {});
}
console.log('[deploy] docker pull');
await execCmd(DOCKER_BIN, ['pull', image], {});
// Pass image and environment to stack via env
const envExtra = {
IMAGE: image,
APP_NAME: app,
DEPLOY_ENV: env
};
console.log('[deploy] docker stack deploy');
await execCmd(DOCKER_BIN, ['stack', 'deploy', '-c', STACK_FILE, stack, '--with-registry-auth'], envExtra);
console.log('[deploy] success');
return json(res, 200, { ok: true, deployed_image: image, stack, service, env });
} catch (e) {
console.error('[deploy] error', e && e.message, e && e.stderr);
return json(res, 500, { error: 'deploy_failed', message: (e && e.message) || 'unknown' });
} finally {
releaseLock(lockfd);
}
});
server.listen(PORT, () => {
console.log(`Webhook receiver listening on :${PORT}`);
});