diff --git a/infra/deploy-runner/README.md b/infra/deploy-runner/README.md index 969e58e..d140dd5 100644 --- a/infra/deploy-runner/README.md +++ b/infra/deploy-runner/README.md @@ -103,7 +103,7 @@ not build an image. A fresh transition id is mandatory and previously issued ids are rejected: ```bash -node infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs 20260717-005 +node infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs 20260721-005 ``` The builder emits a narrowly scoped Compose override plus a strict transition @@ -113,7 +113,7 @@ same immutable image ID, and extracts the package into the root-owned, read-only Engine release tree: ```text -/volume2/nodedc-demo/n8n-private-extensions/releases/n8n-nodes-ndc/0.1.3-3354149b5245e39a/package +/volume2/nodedc-demo/n8n-private-extensions/releases/n8n-nodes-ndc/0.1.5-3c8ae53f010d7c88/package ``` The override sets `N8N_USER_FOLDER=/home/node`, which is required because the diff --git a/infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs b/infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs index a8b80ed..af9db60 100644 --- a/infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs +++ b/infra/deploy-runner/build-engine-n8n-private-extension-artifact.mjs @@ -13,22 +13,22 @@ 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-provider-rotating-access-20260718-006.tgz"), + || join(artifactRoot, "nodedc-n8n-private-extension-n8n-nodes-ndc-geozone-replace-20260720-017.tgz"), ); const predecessorArtifact = resolve( process.env.NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT - || join(artifactRoot, "nodedc-engine-n8n-private-extension-20260717-005.tgz"), + || join(artifactRoot, "nodedc-engine-n8n-private-extension-20260718-008.tgz"), ); -const stageArtifactSha256 = "2699e53c482f2d2bec6d13709a7e9b9a0bb3791ad2018c142e9bc4e7bf371e4a"; -const predecessorArtifactSha256 = "f0f687ddcc1bc006db56c75e73c8213e99298c31c2d881cde078e5ddc9600172"; -const releaseId = "0.1.4-59dc9f7882721d6a"; -const packageVersion = "0.1.4"; -const packageSha256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f"; +const stageArtifactSha256 = "0c680d2436917eb6f2416c5c855f2e01dd59ed50bedb1e36456a7405f3f595af"; +const predecessorArtifactSha256 = "5ef46404dfd7753b4ffd14c5e9b9666f96d2c9875c8fe470714cdea1c6decdce"; +const releaseId = "0.1.5-3c8ae53f010d7c88"; +const packageVersion = "0.1.5"; +const packageSha256 = "3c8ae53f010d7c88b6e90cb8fc4929e7d0880089ad8c3a9edb876ce034786743"; const n8nVersion = "2.3.2"; const baseImage = "docker.n8n.io/n8nio/n8n:2.3.2"; const architecture = "amd64"; -const generatedAt = "2026-07-18T21:00:00.000Z"; +const generatedAt = "2026-07-21T10:30:00.000Z"; const previouslyIssuedTransitionIds = new Set([ "20260715-002", "20260716-003", @@ -36,6 +36,7 @@ const previouslyIssuedTransitionIds = new Set([ "20260717-005", "20260718-006", "20260718-007", + "20260718-008", ]); const transitionId = readTransitionId(process.argv.slice(2), process.env.NODEDC_N8N_TRANSITION_ID); const activationId = `engine-n8n-private-extension-${transitionId}`; @@ -64,7 +65,7 @@ const expectedCredentialTypes = [ "ndcFoundryBindingApi", "ndcProviderRotatingAccessApi", ]; -const predecessorCredentialTypes = expectedCredentialTypes.slice(0, 3); +const predecessorCredentialTypes = [...expectedCredentialTypes]; const nodeModules = [ ["dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js", "NdcDataProductPublish"], ["dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js", "NdcDataProductRead"], @@ -125,6 +126,7 @@ try { }); assertExact(privateNodes.map((item) => item.name), expectedNodeTypes, "node types"); assertExact(privateCredentials.map((item) => item.name), expectedCredentialTypes, "credential types"); + assertPublishNodeV2(privateNodes[0]); const predecessorDescriptor = JSON.parse(predecessorArtifactFile(descriptorRel)); const predecessorNodes = JSON.parse(predecessorArtifactFile(nodesCatalogRel)); @@ -352,17 +354,17 @@ function assertPackage(value) { function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) { if (descriptorValue?.action !== "activate" - || descriptorValue?.releaseId !== "0.1.3-3354149b5245e39a" - || descriptorValue?.packageVersion !== "0.1.3" - || descriptorValue?.packageSha256 !== "3354149b5245e39a10f6f03a4b43796602d7e57ea1f91dcc6cb6774ead97501d") { + || descriptorValue?.releaseId !== "0.1.4-59dc9f7882721d6a" + || descriptorValue?.packageVersion !== "0.1.4" + || descriptorValue?.packageSha256 !== "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f") { throw new Error("predecessor_descriptor_mismatch"); } if (!Array.isArray(nodes) || nodes.length !== 437 - || !Array.isArray(credentials) || credentials.length !== 388 + || !Array.isArray(credentials) || credentials.length !== 389 || meta?.n8nVersion !== n8nVersion - || meta?.source !== "n8n-core+n8n-nodes-ndc@0.1.3" + || meta?.source !== "n8n-core+n8n-nodes-ndc@0.1.4" || meta?.nodeCount !== 437 - || meta?.credentialCount !== 388) { + || meta?.credentialCount !== 389) { throw new Error("predecessor_catalog_mismatch"); } assertExact( @@ -377,6 +379,18 @@ function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) { ); } +function assertPublishNodeV2(node) { + if (JSON.stringify(node?.version) !== JSON.stringify([1, 2])) { + throw new Error("publish_node_versions_mismatch"); + } + const property = node?.properties?.find((item) => item?.name === "publishMode"); + const values = property?.options?.map((item) => item?.value); + if (JSON.stringify(values) !== JSON.stringify(["upsert", "replace"]) + || !property?.displayOptions?.show?.["@version"]?.includes(2)) { + throw new Error("publish_node_replace_contract_missing"); + } +} + 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"); diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 5b48684..022abf9 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -286,6 +286,11 @@ ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256 = { "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", "meta": "8fadbc594c5d14b1d53ab69b677f00823949410b9a37f4e43ecc321e89d68983", }, + "0.1.5-3c8ae53f010d7c88": { + "nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a", + "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", + "meta": "b324592c38d518af8320d8540c7061e4684056ae88a676cf5e50c6eef03c0dd7", + }, } ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256 = "b70d9d8130d498c55de46a5d0758c844f1242b70803457b2291ab3a9de8056f2" ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256 = "680e9f52aac791efbd38e3bd99bd51ef5cded9756d867897c6c2755850e87b50" @@ -331,6 +336,7 @@ ENGINE_N8N_CREDENTIAL_TYPES_BY_RELEASE = { "0.1.2-05e4b38b14b4a019": ENGINE_N8N_CREDENTIAL_TYPES[:3], "0.1.3-3354149b5245e39a": ENGINE_N8N_CREDENTIAL_TYPES[:3], "0.1.4-59dc9f7882721d6a": ENGINE_N8N_CREDENTIAL_TYPES, + "0.1.5-3c8ae53f010d7c88": ENGINE_N8N_CREDENTIAL_TYPES, } ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES = ( "nodedc-source/server/credentialPolicies/ndcPrivateNode.js", diff --git a/infra/deploy-runner/test_engine_n8n_private_extension.py b/infra/deploy-runner/test_engine_n8n_private_extension.py index 45db412..a5fed15 100644 --- a/infra/deploy-runner/test_engine_n8n_private_extension.py +++ b/infra/deploy-runner/test_engine_n8n_private_extension.py @@ -23,17 +23,17 @@ BUILDER_PATH = SCRIPT_DIR / "build-engine-n8n-private-extension-artifact.mjs" STAGE_ARTIFACT = ( PLATFORM_ROOT / "infra/deploy-artifacts" - / "nodedc-n8n-private-extension-n8n-nodes-ndc-provider-rotating-access-20260718-006.tgz" + / "nodedc-n8n-private-extension-n8n-nodes-ndc-geozone-replace-20260720-017.tgz" ) PREDECESSOR_ARTIFACT = ( PLATFORM_ROOT / "infra/deploy-artifacts" - / "nodedc-engine-n8n-private-extension-20260717-005.tgz" + / "nodedc-engine-n8n-private-extension-20260718-008.tgz" ) -TRANSITION_ID = "20260718-008" -SEALED_RELEASE_ID = "0.1.4-59dc9f7882721d6a" -SEALED_PACKAGE_SHA256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f" -PREDECESSOR_RELEASE_ID = "0.1.3-3354149b5245e39a" +TRANSITION_ID = "20260721-005" +SEALED_RELEASE_ID = "0.1.5-3c8ae53f010d7c88" +SEALED_PACKAGE_SHA256 = "3c8ae53f010d7c88b6e90cb8fc4929e7d0880089ad8c3a9edb876ce034786743" +PREDECESSOR_RELEASE_ID = "0.1.4-59dc9f7882721d6a" BUILDER_ENGINE_PATHS = ( "nodedc-source/server/assets/n8n/schema/v2.3.2/nodes.catalog.json", "nodedc-source/server/assets/n8n/schema/v2.3.2/credentials.catalog.json", @@ -54,7 +54,7 @@ EXPECTED_CREDENTIALS = [ "ndcFoundryBindingApi", "ndcProviderRotatingAccessApi", ] -PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS[:3] +PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 = { "expected_engine_n8n_compose_override": "8ee93f3e7c407f5557c711119236449a440d73a7fcac1bbfa50a09e6a13c1cff", "engine_n8n_package_loader_probe_script": "2b3b3c96fad6e5e48ebea74426b21bc7eea6b6fe0e786b537da3366d18aedd9b", @@ -176,7 +176,8 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase): cases = ( ([], "transition_id_required"), (["20260718-007"], "transition_id_already_issued"), - (["engine-n8n-private-extension-20260718-008"], "transition_id_invalid"), + (["20260718-008"], "transition_id_already_issued"), + (["engine-n8n-private-extension-20260721-005"], "transition_id_invalid"), (["20260230-004"], "transition_id_invalid"), (["20260717-000"], "transition_id_invalid"), ([TRANSITION_ID, "unexpected-second-id"], "transition_id_argument_count_invalid"), @@ -245,6 +246,11 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase): self.assertEqual([item["name"] for item in private_credentials], EXPECTED_CREDENTIALS) self.assertTrue(all("usableAsTool" not in item for item in private_nodes)) self.assertEqual((len(nodes), len(credentials)), (437, 389)) + publisher = next(item for item in private_nodes if item["name"].endswith(".ndcDataProductPublish")) + self.assertEqual(publisher["version"], [1, 2]) + publish_mode = next(item for item in publisher["properties"] if item["name"] == "publishMode") + self.assertEqual([item["value"] for item in publish_mode["options"]], ["upsert", "replace"]) + self.assertEqual(publish_mode["displayOptions"]["show"]["@version"], [2]) def test_rollback_catalog_restores_verified_predecessor_release(self): with tempfile.TemporaryDirectory() as directory: @@ -267,7 +273,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase): [item.get("name") for item in credentials if item.get("name") in PREDECESSOR_CREDENTIALS], PREDECESSOR_CREDENTIALS, ) - self.assertEqual((len(nodes), len(credentials)), (437, 388)) + self.assertEqual((len(nodes), len(credentials)), (437, 389)) def test_compose_override_is_runner_derived_and_offline(self): with tempfile.TemporaryDirectory() as directory: @@ -282,7 +288,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase): self.assertEqual(override, RUNNER.expected_engine_n8n_compose_override(descriptor)) self.assertEqual( hashlib.sha256(override.encode("utf-8")).hexdigest(), - "d6d1249e93b5bed5e697715b6b773c7efa4ac234f12eab57d868407e6bb5390e", + "304377edf7d0e184fa4156475df74b7a807a47e4472c8d8e13102639eb9e9d0b", ) self.assertIn("pull_policy: never", override) self.assertIn("N8N_USER_FOLDER: /home/node", override) @@ -574,7 +580,7 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase): def test_sealed_release_exact_set_includes_implicit_directories(self): release_relative = Path( - "payload/releases/n8n-nodes-ndc/0.1.4-59dc9f7882721d6a" + "payload/releases/n8n-nodes-ndc/0.1.5-3c8ae53f010d7c88" ) with tempfile.TemporaryDirectory() as directory: work = Path(directory)