fix(deploy): treat finalized manifests as clean no-op

This commit is contained in:
冰朔 2026-08-07 14:54:43 +08:00
commit c28a3da8ea
4 changed files with 73 additions and 4 deletions

View file

@ -29,7 +29,10 @@ function validateManifest(manifest, resource) {
const unit = String(manifest.module && manifest.module.unit || "");
if (!/^[A-Za-z0-9_.@-]+\.service$/.test(unit)) throw new Error("invalid_unit_name");
if (manifest.schema === "guanghu.existing-service-update-request/v1") return validateServiceUpdateManifest(manifest, unit);
if (manifest.status !== "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING") throw new Error("manifest_not_pending");
if (manifest.status !== "ARCHITECTURE_PACKAGE_READY · INITIAL_PROVISION_PENDING") {
if (/^DEPLOYED(?:_| )/.test(String(manifest.status || ""))) throw new Error("manifest_already_finalized");
throw new Error("manifest_not_pending");
}
if (!manifest.initial_provision || manifest.initial_provision.kind !== "new-architecture-unit") throw new Error("not_initial_architecture_unit");
if (!Array.isArray(manifest.source_paths) || manifest.source_paths.length < 1 || manifest.source_paths.length > 64 || manifest.source_paths.some(item => !safeRelative(item))) throw new Error("invalid_source_paths");
const unitMatches = manifest.source_paths.filter(item => path.basename(item) === unit);

View file

@ -35,6 +35,22 @@ test("architecture deployment defaults to the current Fifth Domain code channel"
assert.doesNotMatch(source, /ARCHITECTURE_PROVISION_REPO_URL \|\| "https:\/\/guanghulab\.com\/fifth-domain\//);
});
test("a completed architecture manifest is distinguished from an invalid pending state", () => {
const completed = manifest();
completed.status = "DEPLOYED_RUNNING_COMPANION · STAGE_B_BOUNDED_RESTART_PROVEN";
assert.throws(
() => validateManifest(completed, { requestId, commit }),
/manifest_already_finalized/,
);
const invalid = manifest();
invalid.status = "UNKNOWN";
assert.throws(
() => validateManifest(invalid, { requestId, commit }),
/manifest_not_pending/,
);
});
test("unit requires non-root systemd hardening and release placeholder", () => {
const unit = "[Service]\nUser=guanghu\nGroup=guanghu\nNoNewPrivileges=true\nPrivateTmp=true\nProtectSystem=strict\nProtectHome=true\nExecStart=/usr/bin/node __RELEASE_ROOT__/server.js\n";
assert.equal(validateUnit(unit), unit);

View file

@ -32,10 +32,32 @@ async function processOne(options = {}) {
const checked = validateEvent(event, options.registry || loadRegistry(options.registryFile));
if (checked) throw new Error(checked);
const result = await (options.provisionFn || provision)({ target: event.target, action: "provision-approved-architecture", resource: event.resource }, { repoUrl: options.registry ? options.registry[event.repo].repo_url : loadRegistry(options.registryFile)[event.repo].repo_url });
const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event.event_id, workorder_id: event.workorder_id, repo: event.repo, branch: event.branch, commit_sha: event.commit_sha, resource: event.resource, result: result.ok ? "DEPLOYED_AND_VERIFIED" : "FAILED_OR_ROLLED_BACK", diagnostic_code: result.ok ? "deployment_succeeded" : String(result.error || "deployment_failed"), evidence: result, recorded_at: new Date().toISOString() };
const noDeploymentRequired = result.ok === false && result.error === "manifest_already_finalized";
const completed = result.ok || noDeploymentRequired;
const receipt = {
schema: "guanghu.deployment-agent-receipt/v1",
event_id: event.event_id,
workorder_id: event.workorder_id,
repo: event.repo,
branch: event.branch,
commit_sha: event.commit_sha,
resource: event.resource,
result: result.ok
? "DEPLOYED_AND_VERIFIED"
: noDeploymentRequired
? "NO_DEPLOYMENT_REQUIRED"
: "FAILED_OR_ROLLED_BACK",
diagnostic_code: result.ok
? "deployment_succeeded"
: noDeploymentRequired
? "manifest_already_finalized"
: String(result.error || "deployment_failed"),
evidence: result,
recorded_at: new Date().toISOString(),
};
writeAtomic(path.join(receiptsDir, `${event.event_id}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
fs.renameSync(processing, `${processing}.${result.ok ? "done" : "failed"}`);
return { ok: result.ok, state: receipt.result, receipt };
fs.renameSync(processing, `${processing}.${completed ? "done" : "failed"}`);
return { ok: completed, state: receipt.result, receipt };
} catch (error) {
const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event && event.event_id || "", result: "REJECTED", diagnostic_code: String(error && error.message || "deployment_event_invalid").slice(0, 160), recorded_at: new Date().toISOString() };
writeAtomic(path.join(receiptsDir, `${path.basename(file, ".json")}.rejected.json`), `${JSON.stringify(receipt, null, 2)}\n`);

View file

@ -14,6 +14,34 @@ test("resident deployment agent consumes only an explicit immutable event and wr
assert.equal(JSON.parse(fs.readFileSync(path.join(receipts, "event-1.json"))).diagnostic_code, "deployment_succeeded");
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test("a finalized manifest produces a clean no-deployment receipt", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-deploy-noop-"));
const queue = path.join(root, "queue"), receipts = path.join(root, "receipts"), sha = "c".repeat(40);
fs.mkdirSync(queue);
fs.writeFileSync(path.join(queue, "event.json"), JSON.stringify({
schema: "guanghu.deployment-event/v1",
event_id: "event-finalized",
state: "queued_for_resident_agent",
repo: "bingshuo/guanghu-ice-heart",
branch: "main",
commit_sha: sha,
workorder_id: "order-finalized",
resource: `GLS-FINALIZED-DEPLOY@${sha}`,
manifest: "deployment/requests/GLS-FINALIZED-DEPLOY.json",
}));
try {
const result = await processOne({
queueDir: queue,
receiptsDir: receipts,
registry: { "bingshuo/guanghu-ice-heart": { repo_url: "https://example.invalid/code.git" } },
provisionFn: async () => ({ ok: false, error: "manifest_already_finalized" }),
});
assert.equal(result.ok, true);
assert.equal(result.state, "NO_DEPLOYMENT_REQUIRED");
assert.equal(result.receipt.diagnostic_code, "manifest_already_finalized");
assert.equal(fs.existsSync(path.join(queue, "event.json.processing.done")), true);
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
test("resident agent rechecks source ownership before deployment", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-source-policy-"));
const queue = path.join(root, "queue"), receipts = path.join(root, "receipts"), sha = "b".repeat(40);