feat(platform): add managed data product history plane

This commit is contained in:
Codex
2026-07-18 14:38:06 +03:00
parent 02816c4352
commit 3c5d8f6cef
34 changed files with 2091 additions and 163 deletions
@@ -13,18 +13,19 @@ const engineRoot = resolve(platformRoot, "../NODEDC_ENGINE_INFRA");
const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(here, "../deploy-artifacts"));
const stageArtifact = resolve(
process.env.NODEDC_N8N_EXTENSION_STAGE_ARTIFACT
|| join(artifactRoot, "nodedc-n8n-private-extension-n8n-nodes-ndc-release-20260716-003.tgz"),
|| join(artifactRoot, "nodedc-n8n-private-extension-n8n-nodes-ndc-history-read-20260717-004.tgz"),
);
const stageArtifactSha256 = "4601c16c57e5182996adf18d0163837511b27ab3f7cfdf97eac679418ee2d078";
const releaseId = "0.1.2-05e4b38b14b4a019";
const packageVersion = "0.1.2";
const packageSha256 = "05e4b38b14b4a019ce1f6eee27b9e320094cb3560903bd68074966b3a1267af5";
const stageArtifactSha256 = "a78f8250704e4893eab727cdb862b69ce45a84310fbb73ecd3dfd968b72d12ee";
const releaseId = "0.1.3-3354149b5245e39a";
const packageVersion = "0.1.3";
const packageSha256 = "3354149b5245e39a10f6f03a4b43796602d7e57ea1f91dcc6cb6774ead97501d";
const n8nVersion = "2.3.2";
const baseImage = "docker.n8n.io/n8nio/n8n:2.3.2";
const architecture = "amd64";
const generatedAt = "2026-07-15T21:51:41.000Z";
const previouslyIssuedTransitionIds = new Set(["20260715-002", "20260716-003"]);
const generatedAt = "2026-07-17T21:00:00.000Z";
const predecessorGitRevision = "96ad46e3c62943f818233481957c62e5088ae35c";
const previouslyIssuedTransitionIds = new Set(["20260715-002", "20260716-003", "20260717-004"]);
const transitionId = readTransitionId(process.argv.slice(2), process.env.NODEDC_N8N_TRANSITION_ID);
const activationId = `engine-n8n-private-extension-${transitionId}`;
const rollbackId = `engine-n8n-private-extension-rollback-${transitionId}`;
@@ -39,7 +40,6 @@ const metaRel = `${schemaRoot}/meta.json`;
const iconRoot = "nodedc-source/server/assets/n8n/icons";
const iconRel = `${iconRoot}/ndc.svg`;
const darkIconRel = `${iconRoot}/ndc.dark.svg`;
const sealedReleaseRelativePath = `n8n-private-extensions/releases/n8n-nodes-ndc/${releaseId}/package`;
const runtimePackagePath = "/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc";
const expectedNodeTypes = [
@@ -111,10 +111,23 @@ try {
assertExact(privateNodes.map((item) => item.name), expectedNodeTypes, "node types");
assertExact(privateCredentials.map((item) => item.name), expectedCredentialTypes, "credential types");
const baselineNodes = JSON.parse(gitFile(nodesCatalogRel));
const baselineCredentials = JSON.parse(gitFile(credentialsCatalogRel));
const baselineMeta = JSON.parse(gitFile(metaRel));
assertBaselineCatalogs(baselineNodes, baselineCredentials, baselineMeta);
const predecessorDescriptor = JSON.parse(gitFile(descriptorRel));
const predecessorNodes = JSON.parse(gitFile(nodesCatalogRel));
const predecessorCredentials = JSON.parse(gitFile(credentialsCatalogRel));
const predecessorMeta = JSON.parse(gitFile(metaRel));
assertPredecessorCatalogs(
predecessorDescriptor,
predecessorNodes,
predecessorCredentials,
predecessorMeta,
);
const baselineNodes = predecessorNodes.filter(
(item) => !String(item?.name || "").startsWith("n8n-nodes-ndc."),
);
const baselineCredentials = predecessorCredentials.filter(
(item) => !expectedCredentialTypes.includes(String(item?.name || "")),
);
assertBaselineCatalogs(baselineNodes, baselineCredentials);
const activeNodes = [...baselineNodes, ...privateNodes];
const activeCredentials = [...baselineCredentials, ...privateCredentials];
const activeMeta = {
@@ -125,9 +138,20 @@ try {
credentialCount: activeCredentials.length,
};
const activationDescriptor = descriptor("activate", expectedNodeTypes, expectedCredentialTypes, "verified_inactive");
const rollbackDescriptor = descriptor("rollback-inactive", [], [], releaseId);
const override = composeOverride();
const target = {
releaseId,
packageVersion,
packageSha256,
};
const predecessor = {
releaseId: predecessorDescriptor.releaseId,
packageVersion: predecessorDescriptor.packageVersion,
packageSha256: predecessorDescriptor.packageSha256,
};
const activationDescriptor = descriptor(target, predecessor.releaseId, predecessor.releaseId);
const rollbackDescriptor = descriptor(predecessor, releaseId, releaseId);
const override = composeOverride(target);
const rollbackOverride = composeOverride(predecessor);
const generatedRoot = join(work, "generated-engine-payload");
await writeJson(join(generatedRoot, nodesCatalogRel), activeNodes);
@@ -154,13 +178,16 @@ try {
}
});
const rollbackEntries = [descriptorRel, nodesCatalogRel, credentialsCatalogRel, metaRel];
const rollbackEntries = [...activationEntries];
const rollbackArtifact = await buildArtifact(work, rollbackId, rollbackEntries, async (payload) => {
await writeJson(join(payload, descriptorRel), rollbackDescriptor);
await mkdir(join(payload, schemaRoot), { recursive: true });
await writeFile(join(payload, nodesCatalogRel), `${JSON.stringify(baselineNodes, null, 2)}\n`, "utf8");
await writeFile(join(payload, credentialsCatalogRel), `${JSON.stringify(baselineCredentials, null, 2)}\n`, "utf8");
await writeFile(join(payload, metaRel), `${JSON.stringify(baselineMeta, null, 2)}\n`, "utf8");
await writeFile(join(payload, overrideRel), rollbackOverride, "utf8");
await writeJson(join(payload, nodesCatalogRel), predecessorNodes);
await writeJson(join(payload, credentialsCatalogRel), predecessorCredentials);
await writeJson(join(payload, metaRel), predecessorMeta);
await mkdir(join(payload, iconRoot), { recursive: true });
await cp(join(engineRoot, iconRel), join(payload, iconRel), { force: false });
await cp(join(engineRoot, darkIconRel), join(payload, darkIconRel), { force: false });
});
console.log(JSON.stringify({
@@ -177,25 +204,25 @@ try {
await rm(work, { recursive: true, force: true });
}
function descriptor(action, nodeTypes, credentialTypes, expectedCurrent) {
function descriptor(target, expectedCurrent, rollbackBaseline) {
return {
schemaVersion: "nodedc.engine-n8n-private-extension-transition/v1",
action,
releaseId,
packageVersion,
packageSha256,
action: "activate",
releaseId: target.releaseId,
packageVersion: target.packageVersion,
packageSha256: target.packageSha256,
n8nVersion,
baseImage,
baseImageArchitecture: architecture,
baseImageIdentityPolicy: "running-container-and-local-tag-must-match",
sealedReleaseRelativePath,
sealedReleaseRelativePath: `n8n-private-extensions/releases/n8n-nodes-ndc/${target.releaseId}/package`,
composeOverride: overrideRel,
runtimePackagePath,
topologyServices: ["n8n"],
expectedCurrent,
expectedNodeTypes: nodeTypes,
expectedCredentialTypes: credentialTypes,
rollbackBaseline: "verified_inactive",
expectedNodeTypes,
expectedCredentialTypes,
rollbackBaseline,
};
}
@@ -235,8 +262,9 @@ async function assertArtifactTargetFresh(path) {
throw new Error("transition_artifact_already_exists");
}
function composeOverride() {
function composeOverride(target) {
const health = "const http=require('http');const req=http.get('http://127.0.0.1:5678/healthz/readiness',r=>{r.resume();process.exit(r.statusCode===200?0:1)});req.on('error',()=>process.exit(1));req.setTimeout(4000,()=>{req.destroy();process.exit(1)});";
const sealedReleaseRelativePath = `n8n-private-extensions/releases/n8n-nodes-ndc/${target.releaseId}/package`;
return [
"services:",
" n8n:",
@@ -257,8 +285,8 @@ function composeOverride() {
" retries: 30",
" start_period: 30s",
" labels:",
` nodedc.n8n-private-extension.release: ${releaseId}`,
` nodedc.n8n-private-extension.package-sha256: ${packageSha256}`,
` nodedc.n8n-private-extension.release: ${target.releaseId}`,
` nodedc.n8n-private-extension.package-sha256: ${target.packageSha256}`,
"",
].join("\n");
}
@@ -297,7 +325,34 @@ function assertPackage(value) {
assertExact(value.n8n?.credentials, credentialModules.map(([path]) => path), "package credentials");
}
function assertBaselineCatalogs(nodes, credentials, meta) {
function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) {
if (descriptorValue?.action !== "activate"
|| descriptorValue?.releaseId !== "0.1.2-05e4b38b14b4a019"
|| descriptorValue?.packageVersion !== "0.1.2"
|| descriptorValue?.packageSha256 !== "05e4b38b14b4a019ce1f6eee27b9e320094cb3560903bd68074966b3a1267af5") {
throw new Error("predecessor_descriptor_mismatch");
}
if (!Array.isArray(nodes) || nodes.length !== 437
|| !Array.isArray(credentials) || credentials.length !== 388
|| meta?.n8nVersion !== n8nVersion
|| meta?.source !== "n8n-core+n8n-nodes-ndc@0.1.2"
|| meta?.nodeCount !== 437
|| meta?.credentialCount !== 388) {
throw new Error("predecessor_catalog_mismatch");
}
assertExact(
nodes.filter((item) => String(item?.name || "").startsWith("n8n-nodes-ndc.")).map((item) => item.name),
expectedNodeTypes,
"predecessor node types",
);
assertExact(
credentials.filter((item) => expectedCredentialTypes.includes(String(item?.name || ""))).map((item) => item.name),
expectedCredentialTypes,
"predecessor credential types",
);
}
function assertBaselineCatalogs(nodes, credentials) {
if (!Array.isArray(nodes) || nodes.length !== 434 || nodes.some((item) => String(item?.name || "").startsWith("n8n-nodes-ndc."))) {
throw new Error("baseline_node_catalog_mismatch");
}
@@ -305,9 +360,6 @@ function assertBaselineCatalogs(nodes, credentials, meta) {
|| credentials.some((item) => expectedCredentialTypes.includes(String(item?.name || "")))) {
throw new Error("baseline_credential_catalog_mismatch");
}
if (meta?.n8nVersion !== n8nVersion || meta?.nodeCount !== 434 || meta?.credentialCount !== 385) {
throw new Error("baseline_meta_mismatch");
}
}
function assertEngineBaseline(compose) {
@@ -330,7 +382,7 @@ function assertSha(bytes, expected, label) {
}
function gitFile(rel) {
return run("git", ["show", `HEAD:${rel}`], engineRoot).stdout;
return run("git", ["show", `${predecessorGitRevision}:${rel}`], engineRoot).stdout;
}
async function writeJson(path, value) {