feat(deploy): register n8n publisher replace v2

This commit is contained in:
Codex 2026-07-21 13:39:15 +03:00
parent 92d292ce87
commit 9bebd2f504
4 changed files with 54 additions and 28 deletions

View File

@ -103,7 +103,7 @@ not build an image. A fresh transition id is mandatory and previously issued
ids are rejected: ids are rejected:
```bash ```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 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: read-only Engine release tree:
```text ```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 The override sets `N8N_USER_FOLDER=/home/node`, which is required because the

View File

@ -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 artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(here, "../deploy-artifacts"));
const stageArtifact = resolve( const stageArtifact = resolve(
process.env.NODEDC_N8N_EXTENSION_STAGE_ARTIFACT 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( const predecessorArtifact = resolve(
process.env.NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT 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 stageArtifactSha256 = "0c680d2436917eb6f2416c5c855f2e01dd59ed50bedb1e36456a7405f3f595af";
const predecessorArtifactSha256 = "f0f687ddcc1bc006db56c75e73c8213e99298c31c2d881cde078e5ddc9600172"; const predecessorArtifactSha256 = "5ef46404dfd7753b4ffd14c5e9b9666f96d2c9875c8fe470714cdea1c6decdce";
const releaseId = "0.1.4-59dc9f7882721d6a"; const releaseId = "0.1.5-3c8ae53f010d7c88";
const packageVersion = "0.1.4"; const packageVersion = "0.1.5";
const packageSha256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f"; const packageSha256 = "3c8ae53f010d7c88b6e90cb8fc4929e7d0880089ad8c3a9edb876ce034786743";
const n8nVersion = "2.3.2"; const n8nVersion = "2.3.2";
const baseImage = "docker.n8n.io/n8nio/n8n:2.3.2"; const baseImage = "docker.n8n.io/n8nio/n8n:2.3.2";
const architecture = "amd64"; const architecture = "amd64";
const generatedAt = "2026-07-18T21:00:00.000Z"; const generatedAt = "2026-07-21T10:30:00.000Z";
const previouslyIssuedTransitionIds = new Set([ const previouslyIssuedTransitionIds = new Set([
"20260715-002", "20260715-002",
"20260716-003", "20260716-003",
@ -36,6 +36,7 @@ const previouslyIssuedTransitionIds = new Set([
"20260717-005", "20260717-005",
"20260718-006", "20260718-006",
"20260718-007", "20260718-007",
"20260718-008",
]); ]);
const transitionId = readTransitionId(process.argv.slice(2), process.env.NODEDC_N8N_TRANSITION_ID); const transitionId = readTransitionId(process.argv.slice(2), process.env.NODEDC_N8N_TRANSITION_ID);
const activationId = `engine-n8n-private-extension-${transitionId}`; const activationId = `engine-n8n-private-extension-${transitionId}`;
@ -64,7 +65,7 @@ const expectedCredentialTypes = [
"ndcFoundryBindingApi", "ndcFoundryBindingApi",
"ndcProviderRotatingAccessApi", "ndcProviderRotatingAccessApi",
]; ];
const predecessorCredentialTypes = expectedCredentialTypes.slice(0, 3); const predecessorCredentialTypes = [...expectedCredentialTypes];
const nodeModules = [ const nodeModules = [
["dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js", "NdcDataProductPublish"], ["dist/nodes/NdcDataProductPublish/NdcDataProductPublish.node.js", "NdcDataProductPublish"],
["dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js", "NdcDataProductRead"], ["dist/nodes/NdcDataProductRead/NdcDataProductRead.node.js", "NdcDataProductRead"],
@ -125,6 +126,7 @@ try {
}); });
assertExact(privateNodes.map((item) => item.name), expectedNodeTypes, "node types"); assertExact(privateNodes.map((item) => item.name), expectedNodeTypes, "node types");
assertExact(privateCredentials.map((item) => item.name), expectedCredentialTypes, "credential types"); assertExact(privateCredentials.map((item) => item.name), expectedCredentialTypes, "credential types");
assertPublishNodeV2(privateNodes[0]);
const predecessorDescriptor = JSON.parse(predecessorArtifactFile(descriptorRel)); const predecessorDescriptor = JSON.parse(predecessorArtifactFile(descriptorRel));
const predecessorNodes = JSON.parse(predecessorArtifactFile(nodesCatalogRel)); const predecessorNodes = JSON.parse(predecessorArtifactFile(nodesCatalogRel));
@ -352,17 +354,17 @@ function assertPackage(value) {
function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) { function assertPredecessorCatalogs(descriptorValue, nodes, credentials, meta) {
if (descriptorValue?.action !== "activate" if (descriptorValue?.action !== "activate"
|| descriptorValue?.releaseId !== "0.1.3-3354149b5245e39a" || descriptorValue?.releaseId !== "0.1.4-59dc9f7882721d6a"
|| descriptorValue?.packageVersion !== "0.1.3" || descriptorValue?.packageVersion !== "0.1.4"
|| descriptorValue?.packageSha256 !== "3354149b5245e39a10f6f03a4b43796602d7e57ea1f91dcc6cb6774ead97501d") { || descriptorValue?.packageSha256 !== "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f") {
throw new Error("predecessor_descriptor_mismatch"); throw new Error("predecessor_descriptor_mismatch");
} }
if (!Array.isArray(nodes) || nodes.length !== 437 if (!Array.isArray(nodes) || nodes.length !== 437
|| !Array.isArray(credentials) || credentials.length !== 388 || !Array.isArray(credentials) || credentials.length !== 389
|| meta?.n8nVersion !== n8nVersion || 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?.nodeCount !== 437
|| meta?.credentialCount !== 388) { || meta?.credentialCount !== 389) {
throw new Error("predecessor_catalog_mismatch"); throw new Error("predecessor_catalog_mismatch");
} }
assertExact( 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) { function assertBaselineCatalogs(nodes, credentials) {
if (!Array.isArray(nodes) || nodes.length !== 434 || nodes.some((item) => String(item?.name || "").startsWith("n8n-nodes-ndc."))) { if (!Array.isArray(nodes) || nodes.length !== 434 || nodes.some((item) => String(item?.name || "").startsWith("n8n-nodes-ndc."))) {
throw new Error("baseline_node_catalog_mismatch"); throw new Error("baseline_node_catalog_mismatch");

View File

@ -286,6 +286,11 @@ ENGINE_N8N_RELEASE_CATALOG_JSON_SHA256 = {
"credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e", "credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e",
"meta": "8fadbc594c5d14b1d53ab69b677f00823949410b9a37f4e43ecc321e89d68983", "meta": "8fadbc594c5d14b1d53ab69b677f00823949410b9a37f4e43ecc321e89d68983",
}, },
"0.1.5-3c8ae53f010d7c88": {
"nodes": "fb2ef4d38b9c071fae117a27381714658a1a19334b82fc79d64d13f27555fe5a",
"credentials": "af8ada839070a4c24b9b10981a751c2b2db651dced1392333d7a4ef13de4564e",
"meta": "b324592c38d518af8320d8540c7061e4684056ae88a676cf5e50c6eef03c0dd7",
},
} }
ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256 = "b70d9d8130d498c55de46a5d0758c844f1242b70803457b2291ab3a9de8056f2" ENGINE_N8N_INACTIVE_NODES_CATALOG_JSON_SHA256 = "b70d9d8130d498c55de46a5d0758c844f1242b70803457b2291ab3a9de8056f2"
ENGINE_N8N_INACTIVE_CREDENTIALS_CATALOG_JSON_SHA256 = "680e9f52aac791efbd38e3bd99bd51ef5cded9756d867897c6c2755850e87b50" 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.2-05e4b38b14b4a019": ENGINE_N8N_CREDENTIAL_TYPES[:3],
"0.1.3-3354149b5245e39a": ENGINE_N8N_CREDENTIAL_TYPES[:3], "0.1.3-3354149b5245e39a": ENGINE_N8N_CREDENTIAL_TYPES[:3],
"0.1.4-59dc9f7882721d6a": ENGINE_N8N_CREDENTIAL_TYPES, "0.1.4-59dc9f7882721d6a": ENGINE_N8N_CREDENTIAL_TYPES,
"0.1.5-3c8ae53f010d7c88": ENGINE_N8N_CREDENTIAL_TYPES,
} }
ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES = ( ENGINE_CREDENTIAL_SINK_ARTIFACT_ENTRIES = (
"nodedc-source/server/credentialPolicies/ndcPrivateNode.js", "nodedc-source/server/credentialPolicies/ndcPrivateNode.js",

View File

@ -23,17 +23,17 @@ BUILDER_PATH = SCRIPT_DIR / "build-engine-n8n-private-extension-artifact.mjs"
STAGE_ARTIFACT = ( STAGE_ARTIFACT = (
PLATFORM_ROOT PLATFORM_ROOT
/ "infra/deploy-artifacts" / "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 = ( PREDECESSOR_ARTIFACT = (
PLATFORM_ROOT PLATFORM_ROOT
/ "infra/deploy-artifacts" / "infra/deploy-artifacts"
/ "nodedc-engine-n8n-private-extension-20260717-005.tgz" / "nodedc-engine-n8n-private-extension-20260718-008.tgz"
) )
TRANSITION_ID = "20260718-008" TRANSITION_ID = "20260721-005"
SEALED_RELEASE_ID = "0.1.4-59dc9f7882721d6a" SEALED_RELEASE_ID = "0.1.5-3c8ae53f010d7c88"
SEALED_PACKAGE_SHA256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f" SEALED_PACKAGE_SHA256 = "3c8ae53f010d7c88b6e90cb8fc4929e7d0880089ad8c3a9edb876ce034786743"
PREDECESSOR_RELEASE_ID = "0.1.3-3354149b5245e39a" PREDECESSOR_RELEASE_ID = "0.1.4-59dc9f7882721d6a"
BUILDER_ENGINE_PATHS = ( BUILDER_ENGINE_PATHS = (
"nodedc-source/server/assets/n8n/schema/v2.3.2/nodes.catalog.json", "nodedc-source/server/assets/n8n/schema/v2.3.2/nodes.catalog.json",
"nodedc-source/server/assets/n8n/schema/v2.3.2/credentials.catalog.json", "nodedc-source/server/assets/n8n/schema/v2.3.2/credentials.catalog.json",
@ -54,7 +54,7 @@ EXPECTED_CREDENTIALS = [
"ndcFoundryBindingApi", "ndcFoundryBindingApi",
"ndcProviderRotatingAccessApi", "ndcProviderRotatingAccessApi",
] ]
PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS[:3] PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS
PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 = { PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 = {
"expected_engine_n8n_compose_override": "8ee93f3e7c407f5557c711119236449a440d73a7fcac1bbfa50a09e6a13c1cff", "expected_engine_n8n_compose_override": "8ee93f3e7c407f5557c711119236449a440d73a7fcac1bbfa50a09e6a13c1cff",
"engine_n8n_package_loader_probe_script": "2b3b3c96fad6e5e48ebea74426b21bc7eea6b6fe0e786b537da3366d18aedd9b", "engine_n8n_package_loader_probe_script": "2b3b3c96fad6e5e48ebea74426b21bc7eea6b6fe0e786b537da3366d18aedd9b",
@ -176,7 +176,8 @@ class EngineN8nPrivateExtensionTest(unittest.TestCase):
cases = ( cases = (
([], "transition_id_required"), ([], "transition_id_required"),
(["20260718-007"], "transition_id_already_issued"), (["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"), (["20260230-004"], "transition_id_invalid"),
(["20260717-000"], "transition_id_invalid"), (["20260717-000"], "transition_id_invalid"),
([TRANSITION_ID, "unexpected-second-id"], "transition_id_argument_count_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.assertEqual([item["name"] for item in private_credentials], EXPECTED_CREDENTIALS)
self.assertTrue(all("usableAsTool" not in item for item in private_nodes)) self.assertTrue(all("usableAsTool" not in item for item in private_nodes))
self.assertEqual((len(nodes), len(credentials)), (437, 389)) 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): def test_rollback_catalog_restores_verified_predecessor_release(self):
with tempfile.TemporaryDirectory() as directory: 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], [item.get("name") for item in credentials if item.get("name") in PREDECESSOR_CREDENTIALS],
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): def test_compose_override_is_runner_derived_and_offline(self):
with tempfile.TemporaryDirectory() as directory: 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(override, RUNNER.expected_engine_n8n_compose_override(descriptor))
self.assertEqual( self.assertEqual(
hashlib.sha256(override.encode("utf-8")).hexdigest(), hashlib.sha256(override.encode("utf-8")).hexdigest(),
"d6d1249e93b5bed5e697715b6b773c7efa4ac234f12eab57d868407e6bb5390e", "304377edf7d0e184fa4156475df74b7a807a47e4472c8d8e13102639eb9e9d0b",
) )
self.assertIn("pull_policy: never", override) self.assertIn("pull_policy: never", override)
self.assertIn("N8N_USER_FOLDER: /home/node", 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): def test_sealed_release_exact_set_includes_implicit_directories(self):
release_relative = Path( 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: with tempfile.TemporaryDirectory() as directory:
work = Path(directory) work = Path(directory)