guanghu-ice-heart/server-tools/lake-lamp-authz/forgejo-merge-deployment.js

174 lines
7.5 KiB
JavaScript

"use strict";
const crypto = require("node:crypto");
const { enqueueDeploymentEvent } = require("./deployment-event");
const DEFAULT_REPOSITORY = "bingshuo/guanghu-ice-heart";
const DEFAULT_BRANCH = "main";
const DEFAULT_TARGET = "JD-FD-PRIMARY";
const MANIFEST_PATTERN = /^deployment\/requests\/([A-Z0-9][A-Z0-9._-]{5,119})\.json$/;
function safeEqualHex(expected, supplied) {
if (!/^[a-f0-9]{64}$/i.test(String(supplied || ""))) return false;
const left = Buffer.from(expected, "hex");
const right = Buffer.from(String(supplied), "hex");
return left.length === right.length && crypto.timingSafeEqual(left, right);
}
function verifyForgejoSignature(secret, rawBody, supplied) {
if (String(secret || "").length < 32) return false;
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return safeEqualHex(expected, String(supplied || "").replace(/^sha256=/i, ""));
}
function deterministicEventId(value) {
const bytes = crypto.createHash("sha256").update(String(value)).digest().subarray(0, 16);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = bytes.toString("hex");
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
function parseMergedPullRequest(payload, eventName, policy = {}) {
if (eventName !== "pull_request") return { state: "ignored", diagnostic_code: "not_pull_request_event" };
if (!payload || payload.action !== "closed" || payload.pull_request?.merged !== true) {
return { state: "ignored", diagnostic_code: "pull_request_not_merged" };
}
const pull = payload.pull_request;
const repository = String(payload.repository?.full_name || pull.base?.repo?.full_name || "").toLowerCase();
const branch = String(pull.base?.ref || "");
const commit = String(pull.merge_commit_sha || "").toLowerCase();
const merger = String(pull.merged_by?.login || pull.merged_by?.username || "");
const number = Number(pull.number || pull.index || payload.number);
const expectedRepository = String(policy.repository || DEFAULT_REPOSITORY).toLowerCase();
const expectedBranch = String(policy.branch || DEFAULT_BRANCH);
const allowedMergers = policy.allowedMergers || [];
if (repository !== expectedRepository || branch !== expectedBranch) {
return { state: "rejected", diagnostic_code: "merge_target_not_allowed" };
}
if (!/^[0-9a-f]{40}$/.test(commit) || !Number.isSafeInteger(number) || number < 1) {
return { state: "rejected", diagnostic_code: "merge_identity_invalid" };
}
if (!merger || !allowedMergers.includes(merger)) {
return { state: "rejected", diagnostic_code: "human_merger_not_allowed" };
}
return { state: "accepted", repository, branch, commit, merger, number };
}
async function listPullRequestFiles(merge, options = {}) {
const fetchImpl = options.fetchImpl || fetch;
const base = String(options.forgejoApiBase || "http://127.0.0.1:3340/api/v1").replace(/\/$/, "");
const files = [];
for (let page = 1; page <= 5; page += 1) {
const url = `${base}/repos/${merge.repository}/pulls/${merge.number}/files?limit=100&page=${page}`;
const response = await fetchImpl(url, {
headers: { accept: "application/json" },
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error(`forgejo_pull_files_unavailable:${response.status}`);
const batch = await response.json();
if (!Array.isArray(batch)) throw new Error("forgejo_pull_files_invalid");
files.push(...batch);
if (batch.length < 100) return files;
}
throw new Error("forgejo_pull_files_limit_exceeded");
}
function deploymentManifests(files) {
const selected = [];
for (const file of files || []) {
const name = String(file?.filename || "");
const match = name.match(MANIFEST_PATTERN);
if (!match || file.status === "deleted") continue;
selected.push({ path: name, requestId: match[1] });
}
const unique = [...new Map(selected.map(item => [item.path, item])).values()];
if (unique.length > 5) throw new Error("too_many_deployment_manifests");
return unique;
}
async function processForgejoMergeWebhook(input, options = {}) {
const rawBody = Buffer.isBuffer(input.rawBody) ? input.rawBody : Buffer.from(String(input.rawBody || ""));
if (!verifyForgejoSignature(options.secret, rawBody, input.signature)) {
return { ok: false, status: 401, state: "rejected", diagnostic_code: "forgejo_signature_invalid" };
}
let payload;
try { payload = JSON.parse(rawBody.toString("utf8")); }
catch { return { ok: false, status: 400, state: "rejected", diagnostic_code: "forgejo_payload_invalid" }; }
const merge = parseMergedPullRequest(payload, String(input.eventName || ""), {
repository: options.repository,
branch: options.branch,
allowedMergers: options.allowedMergers || [],
});
if (merge.state === "ignored") return { ok: true, status: 202, ...merge, queued: 0 };
if (merge.state !== "accepted") return { ok: false, status: 409, ...merge };
let manifests;
try {
const files = await (options.listFiles || (value => listPullRequestFiles(value, options)))(merge);
manifests = deploymentManifests(files);
} catch (error) {
return { ok: false, status: 502, state: "rejected", diagnostic_code: String(error.message || "forgejo_pull_files_failed") };
}
if (manifests.length === 0) {
return { ok: true, status: 202, state: "accepted_no_deployment_manifest", queued: 0, commit_sha: merge.commit };
}
const delivery = String(input.delivery || "");
if (!/^[A-Za-z0-9._:-]{8,160}$/.test(delivery)) {
return { ok: false, status: 400, state: "rejected", diagnostic_code: "forgejo_delivery_invalid" };
}
const deploymentRepositories = options.deploymentRepositories || {};
const queue = options.enqueue || enqueueDeploymentEvent;
const events = [];
for (const manifest of manifests) {
const dedupeKey = `${delivery}:${merge.repository}:${merge.commit}:${manifest.requestId}`;
const eventId = deterministicEventId(dedupeKey);
const queued = queue({
schema: "guanghu.deployment-intent/v1",
repo: merge.repository,
branch: merge.branch,
commit_sha: merge.commit,
resource: `${manifest.requestId}@${merge.commit}`,
manifest: manifest.path,
workorder_id: `forgejo-pr:${merge.number}:${delivery}`,
}, {
repo: merge.repository,
branch: merge.branch,
commit_sha: merge.commit,
}, options.queueDir, {
authorizer_id: "ICE-GL∞",
persona_id: "SYS-GLW-ZY-EXEC-0001",
execution_runtime_id: "HLCC-MERGE-DEPLOY-GATE-001",
target: options.target || DEFAULT_TARGET,
registry: deploymentRepositories,
event_id: eventId,
dedupe_key: dedupeKey,
});
if (!["queued_for_resident_agent", "duplicate"].includes(queued.state)) {
return { ok: false, status: 409, state: "rejected", diagnostic_code: queued.diagnostic_code || "deployment_event_rejected" };
}
events.push({ request_id: manifest.requestId, event_id: queued.event_id, state: queued.state });
}
return {
ok: true,
status: 202,
state: events.every(item => item.state === "duplicate") ? "duplicate" : "queued_for_resident_agent",
repository: merge.repository,
branch: merge.branch,
commit_sha: merge.commit,
pull_request: merge.number,
merger: merge.merger,
queued: events.filter(item => item.state === "queued_for_resident_agent").length,
events,
};
}
module.exports = {
deploymentManifests,
deterministicEventId,
listPullRequestFiles,
parseMergedPullRequest,
processForgejoMergeWebhook,
verifyForgejoSignature,
};