@blucca/n8n-check 0.0.0-stage → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +10 -0
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/bin/n8n-check.mjs +62 -0
- package/examples/agregado-enrichment/README.md +68 -0
- package/examples/agregado-enrichment/n8n-synthetic +11 -0
- package/examples/agregado-enrichment/observed-results.json +806 -0
- package/examples/agregado-enrichment/prepare.mjs +39 -0
- package/examples/identity-projection/README.md +39 -0
- package/examples/identity-projection/case.json +33 -0
- package/examples/identity-projection/observed-results.json +117 -0
- package/examples/identity-projection/workflow.json +71 -0
- package/examples/identity-projection/wrong-id.json +71 -0
- package/examples/morsof-invoice-history/README.md +77 -0
- package/examples/morsof-invoice-history/UPSTREAM-LICENSE +21 -0
- package/examples/morsof-invoice-history/n8n-datatable.cjs +28 -0
- package/examples/morsof-invoice-history/observed-results.json +113 -0
- package/examples/morsof-invoice-history/prepare.mjs +57 -0
- package/examples/morsof-invoice-history/replay.mjs +35 -0
- package/examples/morsof-invoice-history/run.sh +19 -0
- package/examples/object-body/broken.json +74 -0
- package/examples/object-body/case.json +81 -0
- package/examples/object-body/fixed.json +74 -0
- package/examples/object-body/observed-results.json +449 -0
- package/examples/object-body/retry-case.json +112 -0
- package/examples/object-body/retry.json +77 -0
- package/examples/propfind-body/README.md +75 -0
- package/examples/propfind-body/case.json +51 -0
- package/examples/propfind-body/no-body-control.json +96 -0
- package/examples/propfind-body/observed-results.json +307 -0
- package/examples/propfind-body/workflow.json +96 -0
- package/examples/raw-response/README.md +71 -0
- package/examples/raw-response/file-case.json +54 -0
- package/examples/raw-response/json-body.json +58 -0
- package/examples/raw-response/json-case.json +52 -0
- package/examples/raw-response/observed-results.json +383 -0
- package/examples/raw-response/raw-file.json +88 -0
- package/examples/raw-response/raw-json.json +58 -0
- package/examples/raw-response/raw-text.json +58 -0
- package/examples/raw-response/text-case.json +50 -0
- package/examples/retry-isolation/README.md +71 -0
- package/examples/retry-isolation/direct-case.json +112 -0
- package/examples/retry-isolation/direct.json +101 -0
- package/examples/retry-isolation/editor-template.json +168 -0
- package/examples/retry-isolation/http-batching-case.json +112 -0
- package/examples/retry-isolation/http-batching.json +107 -0
- package/examples/retry-isolation/loop-case.json +117 -0
- package/examples/retry-isolation/loop.json +133 -0
- package/examples/retry-isolation/observed-results.json +155 -0
- package/examples/silent-filter/README.md +57 -0
- package/examples/silent-filter/broken.json +96 -0
- package/examples/silent-filter/case.json +120 -0
- package/examples/silent-filter/fixed.json +96 -0
- package/examples/silent-filter/observed-results.json +273 -0
- package/examples/stop-after/README.md +66 -0
- package/examples/stop-after/broken.json +134 -0
- package/examples/stop-after/case.json +34 -0
- package/examples/stop-after/fixed.json +134 -0
- package/examples/stop-after/observed-results.json +162 -0
- package/examples/vapi-tool-contract/README.md +67 -0
- package/examples/vapi-tool-contract/backend-503.case.json +113 -0
- package/examples/vapi-tool-contract/empty-orders.case.json +113 -0
- package/examples/vapi-tool-contract/observed-results.json +234 -0
- package/examples/vapi-tool-contract/success.case.json +118 -0
- package/examples/vapi-tool-contract/workflow.json +143 -0
- package/examples/vapi-tool-contract/wrong-id.json +143 -0
- package/examples/version-matrix/README.md +65 -0
- package/examples/version-matrix/observed-results.json +307 -0
- package/examples/version-matrix/upgrade-check.yml +32 -0
- package/package.json +41 -4
- package/src/action.mjs +108 -0
- package/src/case-draft.mjs +163 -0
- package/src/local-bundle.mjs +154 -0
- package/src/mock.mjs +72 -0
- package/src/report.mjs +73 -0
- package/src/runner.mjs +111 -0
- package/src/version.mjs +3 -0
- package/src/workflow.mjs +140 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"name": "Vapi tool contract: success",
|
|
4
|
+
"input": {
|
|
5
|
+
"node": "Vapi webhook",
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"body": {
|
|
9
|
+
"message": {
|
|
10
|
+
"type": "tool-calls",
|
|
11
|
+
"toolCallList": [
|
|
12
|
+
{
|
|
13
|
+
"id": "call-order-A",
|
|
14
|
+
"type": "function",
|
|
15
|
+
"function": {
|
|
16
|
+
"name": "lookupOrders",
|
|
17
|
+
"arguments": {
|
|
18
|
+
"customerId": "customer-A"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "call-order-B",
|
|
24
|
+
"type": "function",
|
|
25
|
+
"function": {
|
|
26
|
+
"name": "lookupOrders",
|
|
27
|
+
"arguments": {
|
|
28
|
+
"customerId": "customer-B"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
"stopAfter": "Build Vapi response",
|
|
39
|
+
"mocks": [
|
|
40
|
+
{
|
|
41
|
+
"node": "Lookup orders",
|
|
42
|
+
"url": "/lookup/{{ $json.customerId }}",
|
|
43
|
+
"routes": [
|
|
44
|
+
{
|
|
45
|
+
"method": "POST",
|
|
46
|
+
"path": "/lookup/customer-A",
|
|
47
|
+
"responses": [
|
|
48
|
+
{
|
|
49
|
+
"status": 200,
|
|
50
|
+
"json": {
|
|
51
|
+
"orders": [
|
|
52
|
+
{
|
|
53
|
+
"id": "order-A-101",
|
|
54
|
+
"status": "shipped"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"expect": {
|
|
61
|
+
"count": 1,
|
|
62
|
+
"bodies": [
|
|
63
|
+
{
|
|
64
|
+
"customerId": "customer-A",
|
|
65
|
+
"toolCallId": "call-order-A"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"method": "POST",
|
|
72
|
+
"path": "/lookup/customer-B",
|
|
73
|
+
"responses": [
|
|
74
|
+
{
|
|
75
|
+
"status": 200,
|
|
76
|
+
"json": {
|
|
77
|
+
"orders": [
|
|
78
|
+
{
|
|
79
|
+
"id": "order-B-202",
|
|
80
|
+
"status": "processing"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"expect": {
|
|
87
|
+
"count": 1,
|
|
88
|
+
"bodies": [
|
|
89
|
+
{
|
|
90
|
+
"customerId": "customer-B",
|
|
91
|
+
"toolCallId": "call-order-B"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"assertions": [
|
|
100
|
+
{
|
|
101
|
+
"node": "Build Vapi response",
|
|
102
|
+
"equals": [
|
|
103
|
+
{
|
|
104
|
+
"results": [
|
|
105
|
+
{
|
|
106
|
+
"toolCallId": "call-order-A",
|
|
107
|
+
"result": "{\"orders\":[{\"id\":\"order-A-101\",\"status\":\"shipped\"}]}"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"toolCallId": "call-order-B",
|
|
111
|
+
"result": "{\"orders\":[{\"id\":\"order-B-202\",\"status\":\"processing\"}]}"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Vapi tool-call contract \u2014 synthetic order lookup",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "vapi-webhook",
|
|
6
|
+
"name": "Vapi webhook",
|
|
7
|
+
"type": "n8n-nodes-base.webhook",
|
|
8
|
+
"typeVersion": 2.1,
|
|
9
|
+
"position": [
|
|
10
|
+
0,
|
|
11
|
+
0
|
|
12
|
+
],
|
|
13
|
+
"parameters": {
|
|
14
|
+
"httpMethod": "POST",
|
|
15
|
+
"path": "vapi-tool-contract",
|
|
16
|
+
"responseMode": "responseNode",
|
|
17
|
+
"options": {}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "split-tool-calls",
|
|
22
|
+
"name": "Split tool calls",
|
|
23
|
+
"type": "n8n-nodes-base.code",
|
|
24
|
+
"typeVersion": 2,
|
|
25
|
+
"position": [
|
|
26
|
+
220,
|
|
27
|
+
0
|
|
28
|
+
],
|
|
29
|
+
"parameters": {
|
|
30
|
+
"mode": "runOnceForAllItems",
|
|
31
|
+
"jsCode": "const message = $input.first().json.body.message;\nif (message.type !== 'tool-calls' || !Array.isArray(message.toolCallList) || !message.toolCallList.length) throw new Error('Expected a non-empty tool-calls message');\nconst seen = new Set();\nreturn message.toolCallList.map(call => {\n if (typeof call.id !== 'string' || !call.id || seen.has(call.id)) throw new Error('Each tool call needs a unique ID');\n seen.add(call.id);\n if (call.function?.name !== 'lookupOrders' || typeof call.function.arguments?.customerId !== 'string' || !/^[a-zA-Z0-9-]+$/.test(call.function.arguments.customerId)) throw new Error('Expected lookupOrders with a customerId');\n return {json: {toolCallId: call.id, customerId: call.function.arguments.customerId}, pairedItem: {item: 0}};\n});"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "lookup-orders",
|
|
36
|
+
"name": "Lookup orders",
|
|
37
|
+
"type": "n8n-nodes-base.httpRequest",
|
|
38
|
+
"typeVersion": 4.2,
|
|
39
|
+
"position": [
|
|
40
|
+
440,
|
|
41
|
+
0
|
|
42
|
+
],
|
|
43
|
+
"parameters": {
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"url": "=https://orders.example.invalid/lookup/{{ $json.customerId }}",
|
|
46
|
+
"sendBody": true,
|
|
47
|
+
"specifyBody": "json",
|
|
48
|
+
"jsonBody": "={{ {customerId: $json.customerId, toolCallId: $json.toolCallId} }}",
|
|
49
|
+
"options": {
|
|
50
|
+
"timeout": 5000,
|
|
51
|
+
"response": {
|
|
52
|
+
"response": {
|
|
53
|
+
"fullResponse": true,
|
|
54
|
+
"neverError": true,
|
|
55
|
+
"responseFormat": "json"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "build-vapi-response",
|
|
63
|
+
"name": "Build Vapi response",
|
|
64
|
+
"type": "n8n-nodes-base.code",
|
|
65
|
+
"typeVersion": 2,
|
|
66
|
+
"position": [
|
|
67
|
+
660,
|
|
68
|
+
0
|
|
69
|
+
],
|
|
70
|
+
"parameters": {
|
|
71
|
+
"mode": "runOnceForAllItems",
|
|
72
|
+
"jsCode": "const results = $input.all().map((item, index) => {\n const { toolCallId } = $('Split tool calls').itemMatching(index).json;\n if (item.json.statusCode === 503) return {toolCallId, error: 'Order service temporarily unavailable. Please try again later.'};\n if (item.json.statusCode !== 200) return {toolCallId, error: 'Order lookup failed.'};\n if (!Array.isArray(item.json.body?.orders)) return {toolCallId, error: 'Order service returned an invalid response.'};\n return {toolCallId, result: JSON.stringify({orders: item.json.body.orders})};\n});\nreturn [{json: {results}}];"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "respond-to-vapi",
|
|
77
|
+
"name": "Respond to Vapi",
|
|
78
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
79
|
+
"typeVersion": 1.4,
|
|
80
|
+
"position": [
|
|
81
|
+
880,
|
|
82
|
+
0
|
|
83
|
+
],
|
|
84
|
+
"parameters": {
|
|
85
|
+
"respondWith": "json",
|
|
86
|
+
"responseBody": "={{ $json }}",
|
|
87
|
+
"options": {
|
|
88
|
+
"responseCode": 200
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"connections": {
|
|
94
|
+
"Vapi webhook": {
|
|
95
|
+
"main": [
|
|
96
|
+
[
|
|
97
|
+
{
|
|
98
|
+
"node": "Split tool calls",
|
|
99
|
+
"type": "main",
|
|
100
|
+
"index": 0
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"Split tool calls": {
|
|
106
|
+
"main": [
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"node": "Lookup orders",
|
|
110
|
+
"type": "main",
|
|
111
|
+
"index": 0
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"Lookup orders": {
|
|
117
|
+
"main": [
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
"node": "Build Vapi response",
|
|
121
|
+
"type": "main",
|
|
122
|
+
"index": 0
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"Build Vapi response": {
|
|
128
|
+
"main": [
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
"node": "Respond to Vapi",
|
|
132
|
+
"type": "main",
|
|
133
|
+
"index": 0
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"settings": {
|
|
140
|
+
"executionOrder": "v1"
|
|
141
|
+
},
|
|
142
|
+
"active": false
|
|
143
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Vapi tool-call contract \u2014 synthetic order lookup \u2014 wrong ID control",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "vapi-webhook",
|
|
6
|
+
"name": "Vapi webhook",
|
|
7
|
+
"type": "n8n-nodes-base.webhook",
|
|
8
|
+
"typeVersion": 2.1,
|
|
9
|
+
"position": [
|
|
10
|
+
0,
|
|
11
|
+
0
|
|
12
|
+
],
|
|
13
|
+
"parameters": {
|
|
14
|
+
"httpMethod": "POST",
|
|
15
|
+
"path": "vapi-tool-contract",
|
|
16
|
+
"responseMode": "responseNode",
|
|
17
|
+
"options": {}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "split-tool-calls",
|
|
22
|
+
"name": "Split tool calls",
|
|
23
|
+
"type": "n8n-nodes-base.code",
|
|
24
|
+
"typeVersion": 2,
|
|
25
|
+
"position": [
|
|
26
|
+
220,
|
|
27
|
+
0
|
|
28
|
+
],
|
|
29
|
+
"parameters": {
|
|
30
|
+
"mode": "runOnceForAllItems",
|
|
31
|
+
"jsCode": "const message = $input.first().json.body.message;\nif (message.type !== 'tool-calls' || !Array.isArray(message.toolCallList) || !message.toolCallList.length) throw new Error('Expected a non-empty tool-calls message');\nconst seen = new Set();\nreturn message.toolCallList.map(call => {\n if (typeof call.id !== 'string' || !call.id || seen.has(call.id)) throw new Error('Each tool call needs a unique ID');\n seen.add(call.id);\n if (call.function?.name !== 'lookupOrders' || typeof call.function.arguments?.customerId !== 'string' || !/^[a-zA-Z0-9-]+$/.test(call.function.arguments.customerId)) throw new Error('Expected lookupOrders with a customerId');\n return {json: {toolCallId: call.id, customerId: call.function.arguments.customerId}, pairedItem: {item: 0}};\n});"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "lookup-orders",
|
|
36
|
+
"name": "Lookup orders",
|
|
37
|
+
"type": "n8n-nodes-base.httpRequest",
|
|
38
|
+
"typeVersion": 4.2,
|
|
39
|
+
"position": [
|
|
40
|
+
440,
|
|
41
|
+
0
|
|
42
|
+
],
|
|
43
|
+
"parameters": {
|
|
44
|
+
"method": "POST",
|
|
45
|
+
"url": "=https://orders.example.invalid/lookup/{{ $json.customerId }}",
|
|
46
|
+
"sendBody": true,
|
|
47
|
+
"specifyBody": "json",
|
|
48
|
+
"jsonBody": "={{ {customerId: $json.customerId, toolCallId: $json.toolCallId} }}",
|
|
49
|
+
"options": {
|
|
50
|
+
"timeout": 5000,
|
|
51
|
+
"response": {
|
|
52
|
+
"response": {
|
|
53
|
+
"fullResponse": true,
|
|
54
|
+
"neverError": true,
|
|
55
|
+
"responseFormat": "json"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "build-vapi-response",
|
|
63
|
+
"name": "Build Vapi response",
|
|
64
|
+
"type": "n8n-nodes-base.code",
|
|
65
|
+
"typeVersion": 2,
|
|
66
|
+
"position": [
|
|
67
|
+
660,
|
|
68
|
+
0
|
|
69
|
+
],
|
|
70
|
+
"parameters": {
|
|
71
|
+
"mode": "runOnceForAllItems",
|
|
72
|
+
"jsCode": "const results = $input.all().map((item, index) => {\n const { toolCallId } = $('Split tool calls').itemMatching(index).json;\n if (item.json.statusCode === 503) return {toolCallId, error: 'Order service temporarily unavailable. Please try again later.'};\n if (item.json.statusCode !== 200) return {toolCallId, error: 'Order lookup failed.'};\n if (!Array.isArray(item.json.body?.orders)) return {toolCallId, error: 'Order service returned an invalid response.'};\n return {toolCallId, result: JSON.stringify({orders: item.json.body.orders})};\n});\nresults[1].toolCallId = results[0].toolCallId;\nreturn [{json: {results}}];"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"id": "respond-to-vapi",
|
|
77
|
+
"name": "Respond to Vapi",
|
|
78
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
79
|
+
"typeVersion": 1.4,
|
|
80
|
+
"position": [
|
|
81
|
+
880,
|
|
82
|
+
0
|
|
83
|
+
],
|
|
84
|
+
"parameters": {
|
|
85
|
+
"respondWith": "json",
|
|
86
|
+
"responseBody": "={{ $json }}",
|
|
87
|
+
"options": {
|
|
88
|
+
"responseCode": 200
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"connections": {
|
|
94
|
+
"Vapi webhook": {
|
|
95
|
+
"main": [
|
|
96
|
+
[
|
|
97
|
+
{
|
|
98
|
+
"node": "Split tool calls",
|
|
99
|
+
"type": "main",
|
|
100
|
+
"index": 0
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"Split tool calls": {
|
|
106
|
+
"main": [
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"node": "Lookup orders",
|
|
110
|
+
"type": "main",
|
|
111
|
+
"index": 0
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"Lookup orders": {
|
|
117
|
+
"main": [
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
"node": "Build Vapi response",
|
|
121
|
+
"type": "main",
|
|
122
|
+
"index": 0
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"Build Vapi response": {
|
|
128
|
+
"main": [
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
"node": "Respond to Vapi",
|
|
132
|
+
"type": "main",
|
|
133
|
+
"index": 0
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"settings": {
|
|
140
|
+
"executionOrder": "v1"
|
|
141
|
+
},
|
|
142
|
+
"active": false
|
|
143
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Check the workflow before upgrading n8n
|
|
2
|
+
|
|
3
|
+
**Keep the workflow and expected behavior fixed. Change the n8n version. Inspect what changes.**
|
|
4
|
+
|
|
5
|
+
n8n-check's Action accepts `n8n-version`, starting with v0.1.4. A GitHub matrix runs your case against the release you deploy today and the release you want to adopt. Each job produces its own version-labelled summary and JSON/JUnit artifacts.
|
|
6
|
+
|
|
7
|
+
## Use your own workflow
|
|
8
|
+
|
|
9
|
+
1. Export the workflow and [build a case](https://blucca.github.io/n8n-check/) with synthetic inputs, HTTP responses, and expected behavior.
|
|
10
|
+
2. Copy [`upgrade-check.yml`](upgrade-check.yml) to `.github/workflows/n8n-upgrade.yml` in your repository.
|
|
11
|
+
3. Set the two `matrix.n8n` versions and the `workflow` / `case` file paths.
|
|
12
|
+
4. Commit and inspect both jobs. `fail-fast: false` preserves the other result when one version fails.
|
|
13
|
+
|
|
14
|
+
The supplied workflow treats every failed contract as a failed job. Both artifact names include the version, so the reports stay separate. Review a mismatch against your intended behavior before changing the expectation. A failing deployed baseline identifies an existing workflow or engine behavior worth addressing before the upgrade.
|
|
15
|
+
|
|
16
|
+
The HTTP responses remain controlled local fixtures. Real credentials, provider behavior, triggers, and production data remain separate integration checks. Node types and parameters must exist in each selected n8n release. Data Table initialization adapters in other examples retain their own explicit version bounds.
|
|
17
|
+
|
|
18
|
+
## A real version difference: a missing PROPFIND body
|
|
19
|
+
|
|
20
|
+
The author of [n8n #39395](https://github.com/n8n-io/n8n/issues/39395) supplied a Nextcloud workflow on **2.38.4**, where PROPFIND silently drops the request's XML body. Upstream [PR #24151](https://github.com/n8n-io/n8n/pull/24151) adds body handling for this method; **2.41.7** preserves it.
|
|
21
|
+
|
|
22
|
+
The [workflow and exact-body case](../propfind-body/) use the author's HTTP Request 4.5 parameters and complete 698-character diagnostic XML. The fixture replaces connection information with synthetic values and redirects the URL to a local capture server. This compares the actual released engines with the same export and expected body.
|
|
23
|
+
|
|
24
|
+
### Observed on October 7, 2026
|
|
25
|
+
|
|
26
|
+
| Same export and expected XML | n8n 2.38.4 | n8n 2.41.7 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Engine execution | Success | Success |
|
|
29
|
+
| PROPFIND requests | 1 | 1 |
|
|
30
|
+
| Captured body | Empty (`null`) | All 698 characters |
|
|
31
|
+
| Contract | **5/6 · exit 1** | **6/6 · exit 0** |
|
|
32
|
+
|
|
33
|
+
[Actual GitHub matrix run](https://github.com/blucca/n8n-check/actions/runs/37524798259) · [Versioned JSON record, requests and assertions](observed-results.json)
|
|
34
|
+
|
|
35
|
+
Both jobs used n8n-check 0.1.4, the same file hashes, and loopback-only networking. The record also includes a separate n8n **2.37.9** smoke check: the object-valued JSON request case passes 6/6 with two complete requests and outputs. This checks the selected runtime path; each project's own nodes and contracts supply its upgrade coverage.
|
|
36
|
+
|
|
37
|
+
To run the recorded comparison in your fork:
|
|
38
|
+
|
|
39
|
+
1. Fork n8n-check and enable Actions.
|
|
40
|
+
2. Choose **Compare n8n versions on a real PROPFIND regression → Run workflow**.
|
|
41
|
+
3. Inspect the contract step summary and download `propfind-n8n-2.38.4` and `propfind-n8n-2.41.7`.
|
|
42
|
+
|
|
43
|
+
[Comparison workflow source](../../.github/workflows/compare-versions.yml)
|
|
44
|
+
|
|
45
|
+
This demonstration allows the known older-version contract failure, then asserts the specific missing-body difference. The copyable `upgrade-check.yml` above keeps ordinary strict pass/fail gating for your project.
|
|
46
|
+
|
|
47
|
+
## Local version selection
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git clone --branch v0.1.4 https://github.com/blucca/n8n-check.git
|
|
51
|
+
cd n8n-check
|
|
52
|
+
docker build --build-arg N8N_VERSION=2.38.4 -t n8n-check:2.38.4 .
|
|
53
|
+
docker run --rm --network none -v "$PWD:/work" n8n-check:2.38.4 \
|
|
54
|
+
examples/propfind-body/workflow.json examples/propfind-body/case.json \
|
|
55
|
+
--out /work/results/2.38.4
|
|
56
|
+
# Expected: exit 1, with the request-body assertion failing.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Repeat with `2.41.7` for the complete-body contract. Linux users with a UID other than 1000 can add `--user "$(id -u):$(id -g)"` to `docker run`.
|
|
60
|
+
|
|
61
|
+
Runtime images come from `ghcr.io/n8n-io/n8n`. The default is 2.41.7; supply an exact available release. Every report records the actual `n8nVersion`, `runnerVersion`, and `nodeVersion`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
**Have a client upgrade to ship?** The [$650 implementation pilot](https://blucca.github.io/n8n-release-checks/) supplies an agreed fixture pack and handoff for one workflow or slice. Send the deployed version, target version, workflow purpose, and release date to [belgialucca@gmail.com](mailto:belgialucca@gmail.com?subject=n8n%20upgrade%20check).
|