added deploy watch, pull script. minor refactors

This commit is contained in:
Hunter
2026-10-03 14:31:43 -04:00
parent 6dbaa647dc
commit 30c8ad9293
30 changed files with 370 additions and 213 deletions

47
app/api/deploy/route.ts Normal file
View File

@@ -0,0 +1,47 @@
import { createHash, timingSafeEqual } from "node:crypto";
import { writeFile } from "node:fs/promises";
import path from "node:path";
/**
* POST /api/deploy
*
* Drops the redeploy flag that scripts/deploy-watch.sh polls for. Requires
* `Authorization: Bearer <DEPLOY_TRIGGER_SECRET>`.
*
* DEPLOY_TRIGGER_DIR must point at the same directory the watcher reads
* (mounted into the container by docker-compose.yml).
*/
const TRIGGER_DIR = process.env.DEPLOY_TRIGGER_DIR ?? "/deploy-trigger";
const TRIGGER_FILE = path.join(TRIGGER_DIR, "redeploy.flag");
// Hashing both sides gives equal-length buffers, so the comparison is
// constant-time regardless of the supplied token's length.
function tokenMatches(supplied: string, expected: string): boolean {
const a = createHash("sha256").update(supplied).digest();
const b = createHash("sha256").update(expected).digest();
return timingSafeEqual(a, b);
}
export async function POST(request: Request) {
const secret = process.env.DEPLOY_TRIGGER_SECRET;
if (!secret) {
// Not configured: behave as if the route doesn't exist.
return new Response(null, { status: 404 });
}
const header = request.headers.get("authorization") ?? "";
const match = /^Bearer\s+(.+)$/i.exec(header);
if (!match || !tokenMatches(match[1].trim(), secret)) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
try {
await writeFile(TRIGGER_FILE, new Date().toISOString() + "\n");
} catch (err) {
console.error("[deploy] failed to write trigger file", TRIGGER_FILE, err);
return Response.json({ error: "failed to queue redeploy" }, { status: 500 });
}
return Response.json({ status: "queued" }, { status: 202 });
}