feat(platform): add managed data product history plane
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user