Add Coolify deployment workflow
Deploy / build-and-deploy (push) Failing after 11m57s

This commit is contained in:
Raffael Sheikh committed 2026-05-24 22:13:55 +02:00
1 parent 49bb59a4ab
commit a9dbc23d7e
5 files changed
+230 -11

No files matched your search

+96 -11
View File
@@ -19,32 +19,51 @@ const net = require("net");
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, "public");
const LEADS_DIR = path.join(ROOT, "leads");
const DATA_DIR = process.env.DATA_DIR
? path.resolve(process.env.DATA_DIR)
: ROOT;
const LEADS_DIR = path.join(DATA_DIR, "leads");
const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson");
const LEADS_CSV = path.join(LEADS_DIR, "leads.csv");
/* GameJam-Speicher */
const GAMEJAM_DIR = path.join(ROOT, "gamejam");
const GAMEJAM_DIR = path.join(DATA_DIR, "gamejam");
const SUBMISSIONS_JSON = path.join(GAMEJAM_DIR, "submissions.ndjson");
const GAMES_DIR = path.join(PUBLIC_DIR, "gamejam-games");
const GAMES_DIR = process.env.DATA_DIR
? path.join(DATA_DIR, "gamejam-games")
: path.join(PUBLIC_DIR, "gamejam-games");
const MAX_UPLOAD = 5 * 1024 * 1024; /* 5 MB pro eingereichtem Spiel */
const APP_REVISION = process.env.APP_REVISION || "local";
function envFlag(name, defaultValue) {
const raw = process.env[name];
if (raw == null || raw === "") return defaultValue;
return !/^(0|false|no|off)$/i.test(raw);
}
function envInt(name, defaultValue) {
const raw = Number.parseInt(process.env[name] || "", 10);
return Number.isFinite(raw) ? raw : defaultValue;
}
/* Virenscan über ClamAV. Der Server spricht das clamd-Socket-Protokoll
direkt — keine zusätzliche Abhängigkeit. Voraussetzung: ClamAV ist
installiert und der Daemon "clamd" läuft (Einrichtung siehe README).
enabled:false schaltet den Scan ab (z. B. lokal ohne ClamAV). */
const CLAMAV = {
enabled: true,
host: "127.0.0.1",
port: 3310, /* clamd TCPSocket-Standardport */
timeoutMs: 15000
enabled: envFlag("CLAMAV_ENABLED", true),
host: process.env.CLAMAV_HOST || "127.0.0.1",
port: envInt("CLAMAV_PORT", 3310), /* clamd TCPSocket-Standardport */
timeoutMs: envInt("CLAMAV_TIMEOUT_MS", 15000)
};
/* Bevorzugte Ports — der erste freie wird genutzt. */
const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0];
const PORT_CANDIDATES = process.env.PORT
? [envInt("PORT", 3000)]
: [4317, 4318, 4417, 5317, 8317, 0];
/* 0.0.0.0 = auf allen Schnittstellen erreichbar, auch für andere
Geräte im selben WLAN (z. B. Handys, die den QR-Code scannen). */
const HOST = "0.0.0.0";
const HOST = process.env.HOST || process.env.HOSTNAME || "0.0.0.0";
/* Erste IPv4-LAN-Adresse dieses Rechners ermitteln. */
function getLanIP() {
@@ -88,6 +107,18 @@ function ensureGamejamStore() {
if (!fs.existsSync(GAMES_DIR)) fs.mkdirSync(GAMES_DIR, { recursive: true });
}
function assertInside(baseDir, candidate) {
const relative = path.relative(baseDir, candidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function writeProbe(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, ".write-probe-" + process.pid + "-" + Date.now());
fs.writeFileSync(file, "ok", "utf8");
fs.unlinkSync(file);
}
function csvCell(value) {
const s = String(value == null ? "" : value);
return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"';
@@ -116,8 +147,13 @@ function serveStatic(req, res) {
if (urlPath === "/") urlPath = "/index.html";
/* Path-Traversal verhindern */
const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
if (!filePath.startsWith(PUBLIC_DIR)) {
let baseDir = PUBLIC_DIR;
let filePath = path.normalize(path.join(PUBLIC_DIR, urlPath));
if (urlPath.startsWith("/gamejam-games/")) {
baseDir = GAMES_DIR;
filePath = path.normalize(path.join(GAMES_DIR, urlPath.replace(/^\/gamejam-games\//, "")));
}
if (!assertInside(baseDir, filePath)) {
res.writeHead(403).end("Forbidden");
return;
}
@@ -439,10 +475,57 @@ function sendJSON(res, code, obj) {
res.end(JSON.stringify(obj));
}
function handleHealthz(req, res) {
if (req.method !== "GET" && req.method !== "HEAD") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
}
sendJSON(res, 200, {
ok: true,
service: "toolctrl-website",
revision: APP_REVISION
});
}
async function handleReadyz(req, res) {
if (req.method !== "GET" && req.method !== "HEAD") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
}
const checks = {
leadsWritable: false,
gamejamWritable: false,
gamesWritable: false,
clamav: !CLAMAV.enabled
};
try {
writeProbe(LEADS_DIR);
checks.leadsWritable = true;
writeProbe(GAMEJAM_DIR);
checks.gamejamWritable = true;
writeProbe(GAMES_DIR);
checks.gamesWritable = true;
if (CLAMAV.enabled) checks.clamav = await clamavPing();
} catch (e) {
console.error("[readyz] Prüfung fehlgeschlagen:", e.message);
}
const ok = Object.keys(checks).every((key) => checks[key] === true);
sendJSON(res, ok ? 200 : 503, {
ok: ok,
service: "toolctrl-website",
revision: APP_REVISION,
checks: checks
});
}
/* ---------- Router ---------- */
const server = http.createServer((req, res) => {
const route = req.url.split("?")[0];
if (route === "/healthz") return handleHealthz(req, res);
if (route === "/readyz") return handleReadyz(req, res);
if (route === "/api/lead") {
if (req.method !== "POST") {
return sendJSON(res, 405, { error: "Methode nicht erlaubt." });
@@ -502,6 +585,8 @@ function listen(candidates) {
}
console.log(" Leads: " + LEADS_CSV);
console.log(" GameJam-Einreichungen: " + SUBMISSIONS_JSON);
console.log(" Daten: " + DATA_DIR);
console.log(" Revision: " + APP_REVISION);
if (CLAMAV.enabled) {
clamavPing().then((ok) => {
console.log(" Virenscan: " + (ok