From c28a3da8ea5b749913891372ba057daaee3b69bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Fri, 7 Aug 2026 14:54:43 +0800 Subject: [PATCH] fix(deploy): treat finalized manifests as clean no-op --- .../architecture-provision-broker.js | 5 +++- .../architecture-provision-broker.test.js | 16 +++++++++++ .../deployment-event-worker.js | 28 +++++++++++++++++-- .../deployment-event-worker.test.js | 28 +++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/server-tools/lake-lamp-authz/architecture-provision-broker.js b/server-tools/lake-lamp-authz/architecture-provision-broker.js index ff65030..dab5059 100644 --- a/server-tools/lake-lamp-authz/architecture-provision-broker.js +++ b/server-tools/lake-lamp-authz/architecture-provision-broker.js @@ -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); diff --git a/server-tools/lake-lamp-authz/architecture-provision-broker.test.js b/server-tools/lake-lamp-authz/architecture-provision-broker.test.js index 43299a7..a04b315 100644 --- a/server-tools/lake-lamp-authz/architecture-provision-broker.test.js +++ b/server-tools/lake-lamp-authz/architecture-provision-broker.test.js @@ -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); diff --git a/server-tools/lake-lamp-authz/deployment-event-worker.js b/server-tools/lake-lamp-authz/deployment-event-worker.js index 8e34680..fdd05bd 100644 --- a/server-tools/lake-lamp-authz/deployment-event-worker.js +++ b/server-tools/lake-lamp-authz/deployment-event-worker.js @@ -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`); diff --git a/server-tools/lake-lamp-authz/deployment-event-worker.test.js b/server-tools/lake-lamp-authz/deployment-event-worker.test.js index 79834ee..f1aa285 100644 --- a/server-tools/lake-lamp-authz/deployment-event-worker.test.js +++ b/server-tools/lake-lamp-authz/deployment-event-worker.test.js @@ -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);