mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-19 16:55:02 +02:00
Compare commits
988 Commits
n8n@2.42.4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a334e3b15 | ||
|
|
e532aae35b | ||
|
|
6e8064c6af | ||
|
|
f73dc337f8 | ||
|
|
967591041e | ||
|
|
791c7bc418 | ||
|
|
d4bde146d6 | ||
|
|
16fb42d76e | ||
|
|
dd30fc447b | ||
|
|
14d2d30563 | ||
|
|
ef30f75e7b | ||
|
|
70f0131be5 | ||
|
|
f527f748fd | ||
|
|
35874f421d | ||
|
|
c94ff47cd8 | ||
|
|
804033af54 | ||
|
|
de4411b6b8 | ||
|
|
51e53ad9b7 | ||
|
|
d6d24f267c | ||
|
|
d74f3f2cdd | ||
|
|
b850125fbd | ||
|
|
198127f9cd | ||
|
|
5ad13f6606 | ||
|
|
b27f132218 | ||
|
|
20ea6717a0 | ||
|
|
35e9ee9da5 | ||
|
|
0c1a78793a | ||
|
|
f4f606754c | ||
|
|
4fd7a1bad7 | ||
|
|
086fcf9437 | ||
|
|
660849d93e | ||
|
|
117aa146ce | ||
|
|
8323dfd1e1 | ||
|
|
95b54e5390 | ||
|
|
d27de60b95 | ||
|
|
38a1b15646 | ||
|
|
d9d6c027f6 | ||
|
|
28602ed56e | ||
|
|
c084d996af | ||
|
|
06ba85cbb9 | ||
|
|
b98640f0ab | ||
|
|
04761593da | ||
|
|
191ea98482 | ||
|
|
9a025a188f | ||
|
|
4ceb7dc33e | ||
|
|
275f7ebf68 | ||
|
|
6e04287582 | ||
|
|
b2edcb5727 | ||
|
|
952cbf2bf2 | ||
|
|
5a02fc57aa | ||
|
|
9eb7cb9849 | ||
|
|
eac2f46b0c | ||
|
|
9d2df34333 | ||
|
|
4d292a6009 | ||
|
|
10b1b30e72 | ||
|
|
ffec16672c | ||
|
|
0544d7e597 | ||
|
|
d9cfa767ab | ||
|
|
f6d70c140b | ||
|
|
862c7eceab | ||
|
|
60d6231375 | ||
|
|
0936dfddc3 | ||
|
|
b10c97ed45 | ||
|
|
e9723095d6 | ||
|
|
b8a02f70dc | ||
|
|
d2dc2f9ce5 | ||
|
|
84943adc6c | ||
|
|
ff975f1b9e | ||
|
|
936349a9c7 | ||
|
|
842ec5b62d | ||
|
|
ec074519d9 | ||
|
|
6f3b397a1e | ||
|
|
21221adc78 | ||
|
|
532e8ef930 | ||
|
|
24621ad4ca | ||
|
|
8ee5cf1845 | ||
|
|
7f16040253 | ||
|
|
ddc97694b5 | ||
|
|
eaf47cfe39 | ||
|
|
986b82fa28 | ||
|
|
76cce99626 | ||
|
|
f34c093f08 | ||
|
|
158152496a | ||
|
|
1f223c85f8 | ||
|
|
6a015ecb18 | ||
|
|
033ec05bff | ||
|
|
dd79a610c5 | ||
|
|
a63a8edb55 | ||
|
|
f5dc06f753 | ||
|
|
b78913f75d | ||
|
|
dfec6dee81 | ||
|
|
f92b4c11d6 | ||
|
|
5120d25cbc | ||
|
|
a6719b8db7 | ||
|
|
7001886432 | ||
|
|
a906d2fdfc | ||
|
|
683fd11259 | ||
|
|
1b02863c00 | ||
|
|
d0cb582163 | ||
|
|
ea98bd0600 | ||
|
|
1405d4a351 | ||
|
|
f84413516a | ||
|
|
333a010112 | ||
|
|
b7b3d72737 | ||
|
|
5a3fc39972 | ||
|
|
d460eb28ca | ||
|
|
4a5fdd5b68 | ||
|
|
e7f0607bf1 | ||
|
|
64d8a3ad19 | ||
|
|
c70b5d49b3 | ||
|
|
c28d130c6c | ||
|
|
ca9661c091 | ||
|
|
a94f5dc8b7 | ||
|
|
fffea4c0c0 | ||
|
|
14e633d481 | ||
|
|
575eb6fa43 | ||
|
|
3ea0a3da98 | ||
|
|
fed8b1ec69 | ||
|
|
eba25caa70 | ||
|
|
fe3627914a | ||
|
|
aba5258930 | ||
|
|
aadbb7c498 | ||
|
|
8bee1eeb20 | ||
|
|
2eab299dfa | ||
|
|
fc9f9575e2 | ||
|
|
116a81e06e | ||
|
|
b36f6c0baf | ||
|
|
fa88c3e251 | ||
|
|
8120328661 | ||
|
|
1ee29b2875 | ||
|
|
f553331100 | ||
|
|
1a6b6a65d4 | ||
|
|
88a2426947 | ||
|
|
f5febab84d | ||
|
|
dde9871497 | ||
|
|
cc83b5bd77 | ||
|
|
1ea18e9ef9 | ||
|
|
1ba56b6579 | ||
|
|
baa38dbdc6 | ||
|
|
601f6ac0da | ||
|
|
e29c3a4085 | ||
|
|
447fc866bb | ||
|
|
860f1273eb | ||
|
|
deed9a2f32 | ||
|
|
a32c652c09 | ||
|
|
1903d53bc0 | ||
|
|
131e9c8679 | ||
|
|
236220b248 | ||
|
|
af0202d840 | ||
|
|
adc6d35388 | ||
|
|
b2b2a8641a | ||
|
|
952b1e73b2 | ||
|
|
f596162dc7 | ||
|
|
378847ae1e | ||
|
|
9e3b52cbbb | ||
|
|
054288e488 | ||
|
|
c839d6cd49 | ||
|
|
154cd63bbc | ||
|
|
3c447b42a7 | ||
|
|
1659fa3737 | ||
|
|
9806798e89 | ||
|
|
cdeb405948 | ||
|
|
e45a65a9bf | ||
|
|
b68bd3129b | ||
|
|
e8ded61fe1 | ||
|
|
d046047dd0 | ||
|
|
f483ebd9d1 | ||
|
|
fddef57734 | ||
|
|
ba97e46afc | ||
|
|
4cb7a6ee3d | ||
|
|
8ed8607a52 | ||
|
|
67c1981f2f | ||
|
|
a8880cb449 | ||
|
|
d411a44be0 | ||
|
|
0c0b38e3cb | ||
|
|
315e0a5377 | ||
|
|
efd4e21aad | ||
|
|
bab83634ce | ||
|
|
d6c9b9fba3 | ||
|
|
b36befcb75 | ||
|
|
ed2500ffb2 | ||
|
|
9c0b546a30 | ||
|
|
07595a0007 | ||
|
|
1e67d0315d | ||
|
|
338b0ca3fd | ||
|
|
48edbef643 | ||
|
|
5d79c16f51 | ||
|
|
40094e363b | ||
|
|
e2529a8494 | ||
|
|
50c4ced224 | ||
|
|
b7a5500d16 | ||
|
|
f6b24d0ae9 | ||
|
|
3c7dcf902d | ||
|
|
fbe743b9df | ||
|
|
f5924004ca | ||
|
|
b270d48686 | ||
|
|
3277f8c3b5 | ||
|
|
a57c8f3aba | ||
|
|
dcd312c5a9 | ||
|
|
a6d5b4de3f | ||
|
|
3968a60bf7 | ||
|
|
17fb3c0e17 | ||
|
|
b965e90307 | ||
|
|
38135e1f83 | ||
|
|
616062487a | ||
|
|
1950434a27 | ||
|
|
b3e8b7d9ce | ||
|
|
56eac665c3 | ||
|
|
8bc7161ab1 | ||
|
|
57e26feda4 | ||
|
|
9c42a76b1a | ||
|
|
ae374e79ab | ||
|
|
ef8d61bda7 | ||
|
|
caeaff7230 | ||
|
|
a3f47117cf | ||
|
|
9a524e7c21 | ||
|
|
adedbbf560 | ||
|
|
4eb63d6f41 | ||
|
|
b6d39e2d30 | ||
|
|
7920e6cf6b | ||
|
|
76c43094cd | ||
|
|
898701b05b | ||
|
|
f63111202f | ||
|
|
633eacaae6 | ||
|
|
1b0cdd5998 | ||
|
|
578eea403d | ||
|
|
7c38974070 | ||
|
|
d68ad7e97b | ||
|
|
0650b9cce0 | ||
|
|
df7b3a19c6 | ||
|
|
de42c9515d | ||
|
|
d26f914e3d | ||
|
|
8b5ea6d9e0 | ||
|
|
bfd4885026 | ||
|
|
3800bce812 | ||
|
|
aae8eb63d7 | ||
|
|
0b4c99f10d | ||
|
|
066480d8db | ||
|
|
b8780d8fee | ||
|
|
e47940eecd | ||
|
|
e3b2603d7b | ||
|
|
726a5829c8 | ||
|
|
54f1670791 | ||
|
|
b3e8634c83 | ||
|
|
1643a8598b | ||
|
|
9498088256 | ||
|
|
da3491fc87 | ||
|
|
d83f95b377 | ||
|
|
46761a60a9 | ||
|
|
ccf42e3213 | ||
|
|
94bdf85a8a | ||
|
|
29de1c63ae | ||
|
|
92f8eb51b3 | ||
|
|
146d7804af | ||
|
|
88de3cac20 | ||
|
|
04f4afe3dc | ||
|
|
6924027e93 | ||
|
|
ef183e632d | ||
|
|
ed1c1e561a | ||
|
|
a3a014422f | ||
|
|
f1fecf97e9 | ||
|
|
5da4d19e50 | ||
|
|
2019ea5593 | ||
|
|
4419fc4ba2 | ||
|
|
52aed7ac97 | ||
|
|
1d2c879fab | ||
|
|
bffe7d8392 | ||
|
|
d245b17b89 | ||
|
|
8977833ee2 | ||
|
|
acf5291d80 | ||
|
|
ebf078b64c | ||
|
|
9052f8ff3e | ||
|
|
e0fe04084e | ||
|
|
e859abaf18 | ||
|
|
a972eebe51 | ||
|
|
739c59e991 | ||
|
|
de090cb2a0 | ||
|
|
b97fe7c7d4 | ||
|
|
38e4aebfc8 | ||
|
|
d8a06666da | ||
|
|
60e0cb1e7b | ||
|
|
b09efb706c | ||
|
|
f090f53480 | ||
|
|
347385b07b | ||
|
|
bb45bca5ff | ||
|
|
3075b60576 | ||
|
|
4fcb628fca | ||
|
|
1ee379898b | ||
|
|
366ed6ed0e | ||
|
|
248c86355b | ||
|
|
e5866ac6c1 | ||
|
|
2bc3265e63 | ||
|
|
f6861fe494 | ||
|
|
632d8b9eb0 | ||
|
|
5549374241 | ||
|
|
cba16247b9 | ||
|
|
e528f948ce | ||
|
|
d3bc2754b4 | ||
|
|
ffda639b3f | ||
|
|
97e5ae56e4 | ||
|
|
bda9118168 | ||
|
|
96eff1efb9 | ||
|
|
eb66e1bde0 | ||
|
|
a8a028e2dc | ||
|
|
44c8a312c3 | ||
|
|
7bad46ee7e | ||
|
|
45eea1bc35 | ||
|
|
c8c8c3a81e | ||
|
|
e74f1c19da | ||
|
|
f16c1375b1 | ||
|
|
5146e3960f | ||
|
|
14c77900df | ||
|
|
bae368caca | ||
|
|
f3e2bd83b3 | ||
|
|
27fcb968fb | ||
|
|
2562744611 | ||
|
|
8d93e31d1d | ||
|
|
2596f3ac1c | ||
|
|
e2c4054cba | ||
|
|
2ca4e55502 | ||
|
|
cea974e770 | ||
|
|
ce20d1dea4 | ||
|
|
53d1fd5991 | ||
|
|
776f4e1944 | ||
|
|
3ff9c83bda | ||
|
|
89733cdb9a | ||
|
|
0209caafff | ||
|
|
0755255c35 | ||
|
|
36ae8f74ce | ||
|
|
160c126c6e | ||
|
|
d3deffbfa5 | ||
|
|
b542f66119 | ||
|
|
f0a38cfe58 | ||
|
|
27b43de85f | ||
|
|
7ceb84f06c | ||
|
|
2a51f38d55 | ||
|
|
e0958fe094 | ||
|
|
bc8b803471 | ||
|
|
26d93b601a | ||
|
|
c9cb3ee3ab | ||
|
|
22ff2a0636 | ||
|
|
7dae1b64de | ||
|
|
27aa660a74 | ||
|
|
6cd3539b1e | ||
|
|
97ca4c49e0 | ||
|
|
bd22105646 | ||
|
|
e16066c322 | ||
|
|
7cd9fcd3d1 | ||
|
|
2176819865 | ||
|
|
d038f89e91 | ||
|
|
3dd805d9ed | ||
|
|
16e29da52f | ||
|
|
b8b0387a31 | ||
|
|
8b6352a6fa | ||
|
|
839b841dda | ||
|
|
53a6019ad5 | ||
|
|
2a1fde3634 | ||
|
|
37799ad2f2 | ||
|
|
a1800105ef | ||
|
|
6b77613ee8 | ||
|
|
205c1c3ee5 | ||
|
|
65cc931018 | ||
|
|
3dd3efc42f | ||
|
|
ca9b10054c | ||
|
|
c9f54fab4d | ||
|
|
82d21934d3 | ||
|
|
1f27b98366 | ||
|
|
30d883e866 | ||
|
|
3ec89b24dc | ||
|
|
2e049bb3de | ||
|
|
8297b2e6d2 | ||
|
|
601304543d | ||
|
|
ceaa82cd4f | ||
|
|
444e4a9384 | ||
|
|
45938389b8 | ||
|
|
3301b2cc0f | ||
|
|
529b0d1c3b | ||
|
|
7424f0ca04 | ||
|
|
a33ecf7481 | ||
|
|
3085f45f71 | ||
|
|
ed4fbf4ed3 | ||
|
|
feed158ba2 | ||
|
|
9ff8d4fc73 | ||
|
|
5bc6021477 | ||
|
|
50c2ca0a27 | ||
|
|
d6370a0002 | ||
|
|
44a6370aba | ||
|
|
ef000908e7 | ||
|
|
cdf3787834 | ||
|
|
5e01906a1e | ||
|
|
f05b29abeb | ||
|
|
b33b0db040 | ||
|
|
d1422fb652 | ||
|
|
87c9b1c52a | ||
|
|
be402be262 | ||
|
|
cdc23c666f | ||
|
|
bbd35e281b | ||
|
|
55eb2ce442 | ||
|
|
4032797790 | ||
|
|
c7135451eb | ||
|
|
88a2531558 | ||
|
|
75de193d90 | ||
|
|
ee8a608a22 | ||
|
|
55def8cc2b | ||
|
|
85f690d3b9 | ||
|
|
c2e625bbb0 | ||
|
|
344dea5c77 | ||
|
|
b28dceb462 | ||
|
|
a7cba90a85 | ||
|
|
0d7df52a45 | ||
|
|
55200605dd | ||
|
|
cde7a9dbd9 | ||
|
|
ff7ef60841 | ||
|
|
c92141d141 | ||
|
|
9e1866736c | ||
|
|
22ba4e7bc5 | ||
|
|
033a9b512c | ||
|
|
e2b4fee0ec | ||
|
|
35078f1d04 | ||
|
|
a2ac242b91 | ||
|
|
e64dc46055 | ||
|
|
fd06e07efa | ||
|
|
21af799450 | ||
|
|
fd04f01e8e | ||
|
|
af98327a5e | ||
|
|
c21ed4f13a | ||
|
|
8b7065a876 | ||
|
|
8f9f673b86 | ||
|
|
a3391ba93c | ||
|
|
56e2802e67 | ||
|
|
aee0b23b8e | ||
|
|
d3173d3893 | ||
|
|
19337237ec | ||
|
|
d900743bce | ||
|
|
978f810bbe | ||
|
|
ae15984493 | ||
|
|
9f85b8379c | ||
|
|
24b9db41b3 | ||
|
|
503092ccca | ||
|
|
16a8db6675 | ||
|
|
bc0a9cbc11 | ||
|
|
c6f1e20d9e | ||
|
|
a249e20290 | ||
|
|
1c84af2e96 | ||
|
|
aca9033dca | ||
|
|
2efdd3e021 | ||
|
|
01da88958d | ||
|
|
6f5f79321b | ||
|
|
690fbb525a | ||
|
|
5480258fb5 | ||
|
|
bfebfd26c1 | ||
|
|
806a2aa94b | ||
|
|
13679fcb97 | ||
|
|
75a9cc7e12 | ||
|
|
03248c11bf | ||
|
|
23b6de7028 | ||
|
|
53d33b0f95 | ||
|
|
c4f84e2f78 | ||
|
|
3ba522910b | ||
|
|
6fc0aeda35 | ||
|
|
5b78e8b655 | ||
|
|
cef87cfcab | ||
|
|
a2c8142013 | ||
|
|
cabf0bf3b4 | ||
|
|
84d2fd6f3a | ||
|
|
ce1be847c8 | ||
|
|
fe26ba8d3a | ||
|
|
03f7c76acd | ||
|
|
e67e5a9ef1 | ||
|
|
3a37ec1dcc | ||
|
|
77a692a4bb | ||
|
|
6a41a5ed39 | ||
|
|
ea8417e29a | ||
|
|
5543023723 | ||
|
|
05a4abd700 | ||
|
|
740159411a | ||
|
|
346cbaa316 | ||
|
|
a83c438af4 | ||
|
|
49a3812bc3 | ||
|
|
9946cfe901 | ||
|
|
4c0902f6bf | ||
|
|
d7e9b00234 | ||
|
|
63dc285493 | ||
|
|
5a46327401 | ||
|
|
1ad3c54803 | ||
|
|
5d208c85ed | ||
|
|
209adf9a7e | ||
|
|
04e1e7fc47 | ||
|
|
2aa3fbf185 | ||
|
|
983f5ecea8 | ||
|
|
4e217aeac6 | ||
|
|
d904e7a247 | ||
|
|
f7e52a851e | ||
|
|
1e6da4e615 | ||
|
|
40c0525bed | ||
|
|
0f02b509be | ||
|
|
8ad9b5415e | ||
|
|
e11fe8afb0 | ||
|
|
fdc394819c | ||
|
|
74a8a37134 | ||
|
|
86877c557a | ||
|
|
b464dea23c | ||
|
|
d008d6cee9 | ||
|
|
96a8ff4d15 | ||
|
|
a195096733 | ||
|
|
aef216cb25 | ||
|
|
370b30f919 | ||
|
|
97c54f618c | ||
|
|
c133d6c893 | ||
|
|
a584136b6f | ||
|
|
986f6b49e3 | ||
|
|
1d63a8f3a1 | ||
|
|
f212d5b900 | ||
|
|
f4f7dcf13b | ||
|
|
6cdd1ffbbb | ||
|
|
81f34c2374 | ||
|
|
168a0ac860 | ||
|
|
727f2a74ea | ||
|
|
3b0337d894 | ||
|
|
3c33856997 | ||
|
|
6b8ac76d96 | ||
|
|
d138b8ff4b | ||
|
|
566a1dafc6 | ||
|
|
6bc5c93513 | ||
|
|
b28adf9789 | ||
|
|
7397073d86 | ||
|
|
1ea8397d69 | ||
|
|
86847d7f16 | ||
|
|
33cd4d5cc1 | ||
|
|
60ad29f242 | ||
|
|
9191cf3fb0 | ||
|
|
668e71d54a | ||
|
|
1e919c7c56 | ||
|
|
9d3eb0c637 | ||
|
|
bef5f3f97a | ||
|
|
c194d22b5c | ||
|
|
df8605e875 | ||
|
|
fbbbadc292 | ||
|
|
4b5b68ee29 | ||
|
|
09436b78f9 | ||
|
|
19e1f70ea6 | ||
|
|
0722ddbbd5 | ||
|
|
88b22abf15 | ||
|
|
e36093b11d | ||
|
|
2f4a3868fb | ||
|
|
3816c4afec | ||
|
|
48f05f92fe | ||
|
|
e2733c5b86 | ||
|
|
682d0920ba | ||
|
|
2863939580 | ||
|
|
35972133b0 | ||
|
|
0bbea438e1 | ||
|
|
6b4da93dcb | ||
|
|
e16566b313 | ||
|
|
1de45a1a9b | ||
|
|
1ef77564e3 | ||
|
|
4c8b76b6f0 | ||
|
|
00849c4dea | ||
|
|
a440967b12 | ||
|
|
1c63119756 | ||
|
|
022b23945b | ||
|
|
f31e1a83eb | ||
|
|
eac5d1d7b3 | ||
|
|
eaf8c25d1c | ||
|
|
741ba22943 | ||
|
|
e7d8533880 | ||
|
|
bb76c3c99b | ||
|
|
f26747bfc6 | ||
|
|
084f3b376a | ||
|
|
dc5c80f26a | ||
|
|
5ab2187416 | ||
|
|
dc33c29d1a | ||
|
|
4975ab82fe | ||
|
|
447e939b73 | ||
|
|
4e802ff11d | ||
|
|
0885af531a | ||
|
|
b1ec8e525c | ||
|
|
a694531a03 | ||
|
|
684b181101 | ||
|
|
df4af50125 | ||
|
|
02af7e7659 | ||
|
|
b7d7a18980 | ||
|
|
16dc6fe403 | ||
|
|
779ddcc517 | ||
|
|
e1025f35e4 | ||
|
|
3014c64264 | ||
|
|
4ae863717e | ||
|
|
3cff8dc4ec | ||
|
|
81cdc278c2 | ||
|
|
e500386a54 | ||
|
|
e882e8a483 | ||
|
|
66b382de52 | ||
|
|
3af98170d0 | ||
|
|
6ef70a7303 | ||
|
|
32d472fe76 | ||
|
|
6d1144cea3 | ||
|
|
0e1c754999 | ||
|
|
90b8205b62 | ||
|
|
883ade4b9d | ||
|
|
b36c2b506b | ||
|
|
ee1db0bc12 | ||
|
|
b0d1a30f50 | ||
|
|
77370af99a | ||
|
|
a13705aa8f | ||
|
|
76cab1e26d | ||
|
|
8059b63da8 | ||
|
|
e7f8fa38cf | ||
|
|
f72461d56d | ||
|
|
24dda0d435 | ||
|
|
8924a11fda | ||
|
|
0932e9572e | ||
|
|
830486a851 | ||
|
|
15d597c4ca | ||
|
|
2fe28ea0ee | ||
|
|
c2f6920794 | ||
|
|
2b685c33b6 | ||
|
|
62fd028089 | ||
|
|
fb11cc482e | ||
|
|
d5132d21e0 | ||
|
|
0bd0cd2873 | ||
|
|
e5c8a8bb62 | ||
|
|
611a6cccda | ||
|
|
38eea65993 | ||
|
|
c4112794fa | ||
|
|
3d8b3b99c6 | ||
|
|
fc1c26d867 | ||
|
|
5d3866775c | ||
|
|
c134edf473 | ||
|
|
3366d2b9b3 | ||
|
|
7c126fab32 | ||
|
|
72960562ce | ||
|
|
de72b7fa0d | ||
|
|
d767edfacd | ||
|
|
cf54433c14 | ||
|
|
a6f1e79394 | ||
|
|
3287ef1409 | ||
|
|
73dad06bea | ||
|
|
4cb98aea9f | ||
|
|
05a992d4f2 | ||
|
|
8b68b0fcdd | ||
|
|
958d23d81c | ||
|
|
3c18f58ee0 | ||
|
|
2d09135c6e | ||
|
|
b7960d7a85 | ||
|
|
71ffb4c8cc | ||
|
|
6ea9b1f3bb | ||
|
|
264a931de2 | ||
|
|
e82087bcc8 | ||
|
|
fe91413e5d | ||
|
|
f4c734e7e0 | ||
|
|
4b69c32541 | ||
|
|
39abed9124 | ||
|
|
f26d7fbfe7 | ||
|
|
9ee5d1ad7a | ||
|
|
7db7fa42c8 | ||
|
|
3f2144f062 | ||
|
|
a11777054a | ||
|
|
a448b8d2df | ||
|
|
b82ad639e8 | ||
|
|
439e8ba2e7 | ||
|
|
0280644e77 | ||
|
|
308ec2cc1c | ||
|
|
5c0aff8cbc | ||
|
|
314c430bfc | ||
|
|
f7cfb520bf | ||
|
|
a2fd4189f1 | ||
|
|
5f73c7cdcb | ||
|
|
02f08035b4 | ||
|
|
680f9582e1 | ||
|
|
f055d9cd11 | ||
|
|
b5fe251ba5 | ||
|
|
b7071c91c7 | ||
|
|
206fc6f317 | ||
|
|
01f072b520 | ||
|
|
548bd57802 | ||
|
|
4939dbc787 | ||
|
|
022f4a6477 | ||
|
|
8fa89ee02f | ||
|
|
8f13f2830a | ||
|
|
9183d0422c | ||
|
|
073239e6d5 | ||
|
|
7779f47a7d | ||
|
|
79924a5d93 | ||
|
|
1942447e90 | ||
|
|
44d9adfc7e | ||
|
|
fa8b8b243e | ||
|
|
c4c31a529c | ||
|
|
92ae1b336f | ||
|
|
e3aa15d7d7 | ||
|
|
360ec09cf2 | ||
|
|
1cac1e056d | ||
|
|
fe07e84d0f | ||
|
|
6610987057 | ||
|
|
94a49d08ce | ||
|
|
6a5aa7862e | ||
|
|
f47a2fe235 | ||
|
|
32d7c8ebf4 | ||
|
|
274639fb88 | ||
|
|
a9b0607111 | ||
|
|
b9695a7f6e | ||
|
|
b8cd29fde4 | ||
|
|
fb69206450 | ||
|
|
4ea06438db | ||
|
|
674fb57941 | ||
|
|
d931d6415a | ||
|
|
25831a8ac0 | ||
|
|
708e79264b | ||
|
|
5e28ff8c23 | ||
|
|
b0803dc450 | ||
|
|
72a4024161 | ||
|
|
e18afc84cc | ||
|
|
bec84a1dad | ||
|
|
e6bb9e0f3b | ||
|
|
d6a364c0c0 | ||
|
|
68ced97f81 | ||
|
|
a1370863e4 | ||
|
|
d6ec4c5546 | ||
|
|
be2ee6f356 | ||
|
|
388b4036d4 | ||
|
|
e77e30c7d9 | ||
|
|
e8b9821f4a | ||
|
|
ea9f451076 | ||
|
|
43fe82b10f | ||
|
|
f0ca6b24f4 | ||
|
|
944afe5c88 | ||
|
|
bae7656347 | ||
|
|
51c4770643 | ||
|
|
df27223a4e | ||
|
|
56d6e9da2c | ||
|
|
afd7cb4851 | ||
|
|
b8ff99f2cc | ||
|
|
3fe4612d93 | ||
|
|
dae811ddd9 | ||
|
|
ea38f3a3ee | ||
|
|
628ada76cd | ||
|
|
a30bb7d89d | ||
|
|
1043504c7a | ||
|
|
ea9021ed86 | ||
|
|
a3ae96e87b | ||
|
|
93c1291f6d | ||
|
|
6337b94d3c | ||
|
|
9675fc97fd | ||
|
|
1adc57b97b | ||
|
|
407cf23da7 | ||
|
|
1998011d90 | ||
|
|
e0c27dd34f | ||
|
|
03072faadc | ||
|
|
fd6c1af1a0 | ||
|
|
01d6a1019c | ||
|
|
387905148a | ||
|
|
51be26d7e2 | ||
|
|
064c3bf979 | ||
|
|
87ca4e8bf7 | ||
|
|
0ae0b6b3c2 | ||
|
|
69595f5821 | ||
|
|
aae8016c84 | ||
|
|
deaa9b1054 | ||
|
|
e79a2ba84b | ||
|
|
f6f71abef4 | ||
|
|
ff78115647 | ||
|
|
b22b6f4909 | ||
|
|
28bd124258 | ||
|
|
b4e5fa6eef | ||
|
|
0352a19654 | ||
|
|
89e88f87f2 | ||
|
|
900fe0c03a | ||
|
|
8e339d7051 | ||
|
|
9adaa38f22 | ||
|
|
9e5eb79cc4 | ||
|
|
37ed348665 | ||
|
|
c4d31db8d6 | ||
|
|
0a1ed4e09b | ||
|
|
93c0a5c0ff | ||
|
|
fc3954bfe2 | ||
|
|
d556c318f4 | ||
|
|
7576de5f26 | ||
|
|
c95e34f97a | ||
|
|
01e5ebf726 | ||
|
|
b9368f4db8 | ||
|
|
d262baceef | ||
|
|
f7e82abcba | ||
|
|
6438ead58d | ||
|
|
17fbae497b | ||
|
|
7d5730f688 | ||
|
|
1b294221b5 | ||
|
|
c5658ba4bf | ||
|
|
d09e6d65fc | ||
|
|
a969387ca9 | ||
|
|
fb083823bd | ||
|
|
925f3f54ac | ||
|
|
5bf4cae3b6 | ||
|
|
b48bb1f899 | ||
|
|
2d3e8140ce | ||
|
|
5763326a0a | ||
|
|
cbaea98521 | ||
|
|
093ae6f4db | ||
|
|
56aa3d8857 | ||
|
|
53b6a68e12 | ||
|
|
7675d2bcbd | ||
|
|
4a0865bb62 | ||
|
|
e144b337de | ||
|
|
33505d7faa | ||
|
|
b581d16a82 | ||
|
|
6769662ecf | ||
|
|
190d7c0dc0 | ||
|
|
88d6579459 | ||
|
|
efec7dc067 | ||
|
|
f82fa0c1af | ||
|
|
996dec3193 | ||
|
|
b41019a19a | ||
|
|
a70ff86667 | ||
|
|
f7c1bfa175 | ||
|
|
f2050832cf | ||
|
|
3d70eef3a3 | ||
|
|
9247a09214 | ||
|
|
59c6ab2057 | ||
|
|
d12cab0ed1 | ||
|
|
fa30358f84 | ||
|
|
d5937ed2b6 | ||
|
|
3faf54fea3 | ||
|
|
a4b87fccbe | ||
|
|
06b27d1461 | ||
|
|
58ddcd7bbb | ||
|
|
3745c835b2 | ||
|
|
5b499d512f | ||
|
|
1ee7233460 | ||
|
|
119e21752b | ||
|
|
0e3f6e996e | ||
|
|
f1d4017883 | ||
|
|
ee46834915 | ||
|
|
3a445ae2bf | ||
|
|
86969460f1 | ||
|
|
577df7a23b | ||
|
|
8dc0ac8011 | ||
|
|
681768e0bb | ||
|
|
1700702e52 | ||
|
|
f6a1224ee2 | ||
|
|
bdab0b1017 | ||
|
|
ae99934c21 | ||
|
|
ad6d94e568 | ||
|
|
0817b0e8cc | ||
|
|
fce9570711 | ||
|
|
3123fe20af | ||
|
|
d51c9a92ed | ||
|
|
e0b4928bc8 | ||
|
|
eeaf84f756 | ||
|
|
955c99dc26 | ||
|
|
6590765dc6 | ||
|
|
0976d925b9 | ||
|
|
1b1bb4698d | ||
|
|
e185c36c59 | ||
|
|
3248b75af8 | ||
|
|
5db0c39dde | ||
|
|
3f0c999440 | ||
|
|
0c8acfa274 | ||
|
|
54f634d5f9 | ||
|
|
cd1bfafe72 | ||
|
|
1ebd6265db | ||
|
|
0ddb89c007 | ||
|
|
a931dd8660 | ||
|
|
5e376b69ed | ||
|
|
45edf565df | ||
|
|
5576c785ce | ||
|
|
6da7f6b010 | ||
|
|
0f11660414 | ||
|
|
bd9e1d8947 | ||
|
|
902b039c4d | ||
|
|
5c1f8f7f6c | ||
|
|
202a52d206 | ||
|
|
b15ffa9542 | ||
|
|
256788294d | ||
|
|
2df4f3e1d0 | ||
|
|
9f514bc49d | ||
|
|
632a1f380e | ||
|
|
63c01bee24 | ||
|
|
cd9d1ac669 | ||
|
|
320cc2dda3 | ||
|
|
0c449919da | ||
|
|
4d931ca77e | ||
|
|
849ededd5b | ||
|
|
10e484f552 | ||
|
|
fa93a7f01f | ||
|
|
760e4e81d0 | ||
|
|
ad90c63923 | ||
|
|
9d11369db7 | ||
|
|
a19bf32e51 | ||
|
|
dcda65f7d9 | ||
|
|
3c1aaae176 | ||
|
|
9f0a8463a9 | ||
|
|
52db75973e | ||
|
|
99182c29be | ||
|
|
2f4bbfa7d2 | ||
|
|
506b99456f | ||
|
|
0732d03fa1 | ||
|
|
8f3574c48b | ||
|
|
41994bc6c7 | ||
|
|
0c4bf38033 | ||
|
|
57ef0d138f | ||
|
|
24178e51d3 | ||
|
|
799b92993a | ||
|
|
52dcb9252c | ||
|
|
d8dcc924b7 | ||
|
|
9a4a1dcf27 | ||
|
|
38f21c01f1 | ||
|
|
1694727548 | ||
|
|
fefb2da2b1 | ||
|
|
dafc10852f | ||
|
|
c83fee02fa | ||
|
|
191a22e759 | ||
|
|
c17c48043e | ||
|
|
31b6649d78 | ||
|
|
5d40d837f6 | ||
|
|
90e3385c15 | ||
|
|
373bf77f3a | ||
|
|
834202d124 | ||
|
|
6918fda018 | ||
|
|
f58f0ec1d0 | ||
|
|
d69b52e2c7 | ||
|
|
e13c480626 | ||
|
|
a5b00eee2d | ||
|
|
0cf969a05d | ||
|
|
04abd59fc6 | ||
|
|
7204df600d | ||
|
|
f9dbd2c8d6 | ||
|
|
ce295b6080 | ||
|
|
fc99aaf87b | ||
|
|
f0b70134a8 | ||
|
|
1153702d1a | ||
|
|
29c09c6f99 | ||
|
|
9ab649ff55 | ||
|
|
933113e375 | ||
|
|
3c896fcdea | ||
|
|
88d15ed051 | ||
|
|
adb42a31ff | ||
|
|
55fd9daa08 | ||
|
|
53b84d2baa | ||
|
|
05401103d2 | ||
|
|
995c101dba | ||
|
|
114a5737f5 | ||
|
|
55a87877b5 | ||
|
|
93b658c8db | ||
|
|
fe1bfdfd29 | ||
|
|
499d64181a | ||
|
|
bf6ba26a7f | ||
|
|
7e3a1037ff | ||
|
|
b09fd0dc69 | ||
|
|
7b12ec165d | ||
|
|
41a0ccaf16 | ||
|
|
6974abaea3 | ||
|
|
f3f660b745 | ||
|
|
d2b6e718d1 | ||
|
|
62cadddf28 | ||
|
|
1882cff76b | ||
|
|
8d4111dff8 | ||
|
|
0e5c9aaed6 | ||
|
|
a45162c5fa | ||
|
|
0811443909 | ||
|
|
54c351b92f | ||
|
|
0f63032916 | ||
|
|
188b03c486 | ||
|
|
df3945af8a | ||
|
|
212fbfe455 | ||
|
|
84da588c36 | ||
|
|
af2695407a | ||
|
|
ff98d9900f | ||
|
|
6cc8c0d448 | ||
|
|
c575f18c99 | ||
|
|
f931ea81d5 | ||
|
|
8c9733b5c6 | ||
|
|
f487613bfc | ||
|
|
d2eae41dd6 | ||
|
|
d92bc4d3ab | ||
|
|
84efbac6f1 | ||
|
|
599df45795 | ||
|
|
0f77ba5e7e | ||
|
|
682afd5aa9 | ||
|
|
529761ff03 | ||
|
|
3220953fb4 | ||
|
|
0cd66905e5 | ||
|
|
bab2978ccf | ||
|
|
a66221d24a | ||
|
|
c92f86ef71 | ||
|
|
0afda58804 | ||
|
|
46645c6d73 | ||
|
|
2bf9b94936 | ||
|
|
702bb42cbb | ||
|
|
b613184d64 | ||
|
|
dfd7a52224 |
|
|
@ -17,7 +17,7 @@ clear, when to stay silent, what never to say — lives once in
|
|||
| `backend/` | Backend | `cli`, `@n8n/db`, `core`, `workflow`, node packages |
|
||||
| `db-migrations/` | DB migrations | `@n8n/db` migrations + their tests in `cli` |
|
||||
| `frontend/` | Frontend | `packages/frontend` |
|
||||
| `qa-dx/` | QA & DX | `.github`, `docker`, `scripts`, `patches`, `packages/testing`, the lint/test/TS config packages, baselines |
|
||||
| `qa-dx/` | QA & DX | `.github`, `docker`, `scripts`, `patches`, `packages/quality`, the lint/test/TS config packages, baselines |
|
||||
| `testing/` | Backend + Frontend | any package with a test suite |
|
||||
|
||||
A directory maps to one agent unless, like `testing/`, the policy is identical
|
||||
|
|
@ -27,7 +27,7 @@ never a copy per directory. Security and QA & DX deliberately don't link
|
|||
agents shouldn't be able to produce.
|
||||
|
||||
All five slots are used, so a new domain now merges into an existing agent. QA &
|
||||
DX covers the build, test, and CI surface — the same paths `.github/OWNERS`
|
||||
DX covers the build, test, and CI surface — the same paths `OWNERS`
|
||||
assigns to `@n8n-io/qa-dx`. Code-quality rules that happen to apply broadly
|
||||
(error classes, `any`, lazy imports) are backend rules, not QA & DX ones. DB
|
||||
migrations is split out of Backend because a migration is permanent and runs
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ Flag a diff that **adds** entries to any of these, and ask for the fix instead:
|
|||
|------|---------|
|
||||
| `.code-health-baseline.json` | `@n8n/code-health` violations |
|
||||
| `.boundaries-baseline.json` | `turbo boundaries` issue count |
|
||||
| `packages/testing/playwright/.janitor-baseline.json` | Playwright janitor findings |
|
||||
| `packages/cli/eslint.config.mjs` | the `misplaced-n8n-typeorm-import` and public-API allowlists, each captioned "NEVER add to this list" |
|
||||
| `packages/quality/testing/playwright/.janitor-baseline.json` | Playwright janitor findings |
|
||||
| `.code-health-baseline.json` | the `typeorm-persistence-boundary` baseline, which only shrinks as business logic moves queries into repositories |
|
||||
| `packages/cli/eslint.config.mjs` | the public-API allowlists captioned "NEVER add to this list" |
|
||||
| `.code-health-baseline.json` (`lint-config-layering`) | package-wide rule downgrades left in package ESLint configs |
|
||||
|
||||
Removals are the healthy direction and need no comment.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fed from a `needs:` list. Flag:
|
|||
Jobs gated on a `ci-filter` filter only run when a matching file changed, so a
|
||||
filter that is too narrow means the test quietly stops covering new code. When
|
||||
a PR adds a directory that an existing filter was meant to cover — a new package
|
||||
under `packages/testing/`, a new script under `.github/scripts/` — check the
|
||||
under `packages/quality/`, a new script under `.github/scripts/` — check the
|
||||
filter still matches it.
|
||||
|
||||
## Least privilege
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ Run when `AutoReject` is `null`. Full rules for each in `reference/checks.md`:
|
|||
|
||||
## Step 4 — Identify the responsible team
|
||||
|
||||
Run `node .github/scripts/owners.mjs` against the changed file list and map the winning GitHub team to a Linear team. Full mapping table, sub-agent fallback procedure, and label rules: see `reference/teams.md`.
|
||||
Run `node .github/scripts/owners/owners.mjs` against the changed file list and map the winning GitHub team to a Linear team. Full mapping table, sub-agent fallback procedure, and label rules: see `reference/teams.md`.
|
||||
|
||||
## Step 5 — Extract the Linear ticket
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Owner resolution, the GitHub-team → Linear-team → label mapping, and the Lin
|
|||
|
||||
## Identifying the owning team
|
||||
|
||||
Use the canonical owners script at `.github/scripts/owners.mjs`. It parses `.github/OWNERS` with last-match-wins semantics and returns allocations sorted by file count with a `share` percentage. Using the script keeps this skill consistent with whatever CI uses.
|
||||
Use the canonical owners script at `.github/scripts/owners/owners.mjs`. It parses `OWNERS` with last-match-wins semantics and returns allocations sorted by file count with a `share` percentage. Using the script keeps this skill consistent with whatever CI uses.
|
||||
|
||||
1. Write the PR's changed file paths (from the `files` list) to a temp file, one per line:
|
||||
```bash
|
||||
|
|
@ -24,7 +24,7 @@ Use the canonical owners script at `.github/scripts/owners.mjs`. It parses `.git
|
|||
```
|
||||
2. Run the script:
|
||||
```bash
|
||||
node .github/scripts/owners.mjs /tmp/pr-<number>-files.txt
|
||||
node .github/scripts/owners/owners.mjs /tmp/pr-<number>-files.txt
|
||||
```
|
||||
3. The script prints JSON of the form:
|
||||
```json
|
||||
|
|
@ -38,10 +38,10 @@ Use the canonical owners script at `.github/scripts/owners.mjs`. It parses `.git
|
|||
```
|
||||
Allocations are already sorted by `fileCount` descending — take the first entry as the winning team.
|
||||
4. Clean up: `rm /tmp/pr-<number>-files.txt`.
|
||||
5. Strip the `@n8n-io/` prefix from `allocations[0].team` — the GitHub team slug is `nodes`, `iam`, `ai`, etc. If `allocations` is empty (no file matched any rule, which is possible only if `.github/OWNERS` lost its catch-all), fall back to `catalysts`.
|
||||
5. Strip the `@n8n-io/` prefix from `allocations[0].team` — the GitHub team slug is `nodes`, `iam`, `ai`, etc. If `allocations` is empty (no file matched any rule, which is possible only if `OWNERS` lost its catch-all), fall back to `catalysts`.
|
||||
6. Map the GitHub team slug to its Linear team name and PR label using the table below. The `team` field in the JSON output is the **Linear team name**. If the resolved GitHub team slug has no entry in the table, fall back to `Engineering`.
|
||||
|
||||
**Sub-agent fallback**: if `node` execution is denied by the sandbox, read `.github/OWNERS` directly and apply last-match-wins by hand. All the active rules fit on one screen.
|
||||
**Sub-agent fallback**: if `node` execution is denied by the sandbox, read `OWNERS` directly and apply last-match-wins by hand. All the active rules fit on one screen.
|
||||
|
||||
## GitHub team → Linear team → GitHub label
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Run this check before sourcing, drafting, or writing an eval. Run it from
|
|||
`packages/@n8n/instance-ai`:
|
||||
|
||||
```bash
|
||||
pnpm exec dotenvx run -f ../../../.env.local -- \
|
||||
pnpm exec dotenvx run -f ../../../.env.eval -- \
|
||||
sh -c 'test -n "${LANGTRACER_URL:-}" && test -n "${LANGTRACER_API_KEY:-}"'
|
||||
```
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ If the check fails, stop before creating an eval file. Ask the user to:
|
|||
|
||||
1. Generate a key on the
|
||||
[LangTracer API page](https://lang-tracer.n8n-maintenance.workers.dev/account?section=api).
|
||||
2. Add these variables to the repository root `.env.local` file:
|
||||
2. Add these variables to the repository root `.env.eval` file:
|
||||
|
||||
```env
|
||||
LANGTRACER_URL=https://lang-tracer.n8n-maintenance.workers.dev
|
||||
|
|
@ -45,9 +45,8 @@ value. Rerun the check after the user confirms. Continue only when it passes.
|
|||
- Author the case at
|
||||
`packages/@n8n/instance-ai/evaluations/data/agents/<slug>.json`.
|
||||
- Set `"datasets": ["agents"]`.
|
||||
- Push general Agent Builder cases to
|
||||
[Instance AI capabilities — agents](https://lang-tracer.n8n-maintenance.workers.dev/suites/10).
|
||||
Its suite slug is `agents`.
|
||||
- Ask the driver for the target suite before you do anything else. See
|
||||
"Ask for the target suite first" below. Do not assume `agents`.
|
||||
- Do not commit the case JSON. LangTracer is the durable source of truth.
|
||||
- Commit changes to this skill, the harness, and CI when applicable.
|
||||
|
||||
|
|
@ -70,9 +69,189 @@ or outcome expectations can prove the behavior.
|
|||
Use the substitution test for every expectation. A correct alternative build
|
||||
must pass. A build that misses the requested behavior must fail.
|
||||
|
||||
## Write expectations about the Agent, not the builder's internals
|
||||
|
||||
Agent Builder runs as a delegated sub-agent architecture today. That design
|
||||
can change. Do not write an expectation that depends on it. Follow
|
||||
"Keep expectations free of internal mechanics" in
|
||||
[create-instance-ai-eval](../create-instance-ai-eval/SKILL.md).
|
||||
|
||||
Do not write:
|
||||
|
||||
- "The sub-agent runs/tests the Agent before it responds."
|
||||
- "Instance AI delegates to `build-agent`" or "calls `build-agent` once."
|
||||
- "The builder calls `call_agent` to verify the Agent."
|
||||
- "The sub-agent reads the skill, then sets the model."
|
||||
|
||||
Write what a user or a reviewer can observe:
|
||||
|
||||
| ❌ mechanical | ✅ intent |
|
||||
|---|---|
|
||||
| "The sub-agent tests the Agent before it reports" | "The final response says the Agent was tested only if a test ran, and it reports the result of that test honestly" |
|
||||
| "`build-agent` is called with the Slack credential" | "The Agent uses the Slack credential the user has" |
|
||||
| "The sub-agent adds a tool for the lookup" | "The Agent can look up the order status when a user asks for it" |
|
||||
| "The builder sets `model` after the catalog lookup" | "The Agent uses a model that the connected credential supports" |
|
||||
|
||||
Put each one where it belongs:
|
||||
|
||||
- The Agent's configuration, tools, skills, and behavior go in
|
||||
`outcomeExpectations`.
|
||||
- What the user is told, asked, or shown goes in `processExpectations`.
|
||||
- The final response is the place to check claims ("says it is ready",
|
||||
"lists the tools it added"). Do not check which component made the claim.
|
||||
|
||||
The calibration steps below ask you to confirm that `build-agent` ran. That is
|
||||
a check for you, to be sure the case reached Agent Builder. Do not add it to the
|
||||
case.
|
||||
|
||||
For multi-turn, seeded, or capability-gap cases, follow the case-shape and
|
||||
calibration rules in [create-instance-ai-eval](../create-instance-ai-eval/SKILL.md).
|
||||
This skill overrides its workflow directory and suite guidance for Agent cases.
|
||||
The sections below add the Agent-specific parts. Do not skip the general rules
|
||||
they point to.
|
||||
|
||||
## Ask for the target suite first
|
||||
|
||||
Follow "Ask for the target suite first" in
|
||||
[create-instance-ai-eval](../create-instance-ai-eval/SKILL.md). Do it after the
|
||||
LangTracer preflight and before you source or draft anything. If the request
|
||||
already names a suite, state it and do not ask. Otherwise ask, in autonomous
|
||||
mode too, in the same message as the autonomy question.
|
||||
|
||||
For Agent cases, recommend
|
||||
[Instance AI capabilities — agents](https://lang-tracer.n8n-maintenance.workers.dev/suites/10)
|
||||
(slug `agents`). Offer `baseline` as well: the Instance AI (INS) team monitors
|
||||
it and it runs nightly. Get the full list from `list_suites`. Use the suite the
|
||||
driver picks in every push command, in place of `agents` in the examples below.
|
||||
Keep the `agents` dataset and the `data/agents/` directory. They do not depend
|
||||
on the suite.
|
||||
|
||||
## Set the autonomy level first
|
||||
|
||||
Follow "Set the autonomy level first" in
|
||||
[create-instance-ai-eval](../create-instance-ai-eval/SKILL.md). Ask the driver
|
||||
for *autonomous* or *checkpoint* mode before you source or draft anything. The
|
||||
four gates are the same: selection, shape and expectations, calibration, push.
|
||||
|
||||
## Source the case from a real thread
|
||||
|
||||
The best Agent cases come from a real conversation. Discover the conversation
|
||||
with LangTracer. Author a synthetic case from what you learn.
|
||||
|
||||
1. List candidates: `list_conversations` with `usedAgentBuilder: true`. Do not
|
||||
use `source: "agent-builder"`. That pool is a different product, has
|
||||
unscrubbed payloads, and has no Agent snapshots.
|
||||
2. List what the thread did to the Agent: `list_conversation_agent_snapshots`.
|
||||
`target-resolved` is the state that a turn opened on. `config-updated` is the
|
||||
state after the builder changed it.
|
||||
3. Pick the shape from the snapshot sequence (next section).
|
||||
4. Fetch the state to seed with `get_conversation_agent_snapshot`. Seed from the
|
||||
`target-resolved` row of the turn under test.
|
||||
5. Read the raw turn before you write any expectation. Follow
|
||||
"First reproduce, then reclassify" in the general skill.
|
||||
|
||||
Two snapshot patterns to look for:
|
||||
|
||||
- A `target-resolved` hash that differs from the previous `config-updated` hash.
|
||||
The user edited the Agent in the UI between turns. This is the best seed for a
|
||||
repair or update case.
|
||||
- A run of turns that each end in `config-updated`. The user added one rule per
|
||||
turn. Use it for a "new rule does not erase old rules" case.
|
||||
|
||||
Credentials arrive as `[redacted]`. Instructions and skill bodies do not. Scrub
|
||||
the prose before it enters a case. See the `agents` section in
|
||||
[case-shapes.md](../create-instance-ai-eval/case-shapes.md) (the `agents` seed section).
|
||||
|
||||
## Pick the shape: create or update
|
||||
|
||||
| Shape | Question it answers | How to write it |
|
||||
|---|---|---|
|
||||
| **Create** | Does the builder make the right Agent from a fresh request? | One user turn. No seed. Assert on the Agent artifact. |
|
||||
| **Update** | Does the builder change only what the user asked for? | `seed.agents` with the Agent before the change. One live turn with the change. Assert on the new behavior and that the untouched parts survive. |
|
||||
| **Repair** | Does the builder find the cause of a failure and fix it? | Seed the broken Agent. The live turn reports the failure. Assert on the cause found and the fix applied. |
|
||||
| **Incremental rules** | Do earlier rules survive when a new rule lands? | Seed the Agent after several rules. Add one conflicting or overlapping rule. Assert that the old rules are still in force. |
|
||||
|
||||
Rules that apply to all four:
|
||||
|
||||
- An update that rebuilds the Agent from scratch is a failure. Assert on a
|
||||
specific untouched instruction, tool, or skill that must still be there.
|
||||
- Write the conversation in English, even when the source thread is not.
|
||||
- Trim to the fewest turns that reproduce the behavior. Do not copy a long
|
||||
thread.
|
||||
- Put only the live turn in `conversation`. Put earlier turns in `seed.messages`.
|
||||
|
||||
## Confirm the precondition fired
|
||||
|
||||
A pass can mean the builder did the right thing. It can also mean the situation
|
||||
never happened. Check before you trust a green.
|
||||
|
||||
- For "the builder did not do X", confirm that it reached the turn where X was
|
||||
possible. A builder that never edited the Agent passes "did not delete the old
|
||||
rule".
|
||||
- For a repair case, confirm that the seeded Agent really has the defect. Read
|
||||
the seeded config in the rendered artifact.
|
||||
- For an update case, compare the seeded config with the final config. Confirm
|
||||
that the requested change is the only difference you expected.
|
||||
- For a "say you cannot" case, confirm that the tool or data was really missing
|
||||
in the run.
|
||||
|
||||
## A red is a finding
|
||||
|
||||
Follow "A red is signal" in the general skill. For Agent cases, add this:
|
||||
|
||||
1. Classify the red as a product gap, a harness limitation, or non-determinism.
|
||||
2. Keep a product-gap red. Start the case `description` with
|
||||
`Capability-gap finding:`.
|
||||
3. **Propose a Linear ticket for the Agent team.** The Linear team is named
|
||||
`Agent`. Propose the ticket. Do not create it unless the driver says go.
|
||||
Follow "Capability gap → propose a Linear ticket" in the general skill. Also
|
||||
check whether the gap already has a ticket.
|
||||
- Write the ticket in English. Use Simplified Technical English, as
|
||||
[AGENTS.md](../../../AGENTS.md) requires. Translate the user's words. Do
|
||||
not paste them.
|
||||
- This repository is public. Keep customer names, company names, and real
|
||||
URLs out of the ticket. Describe the use case in neutral words.
|
||||
- Link the LangTracer case and the source thread. Do not copy raw user
|
||||
messages or agent instructions from the thread.
|
||||
- Reproduce the gap first. This is a default, not a hard rule. Propose a
|
||||
ticket only after a case is red on a real build. Quote the failing
|
||||
expectation and the judge reason. Follow "Reproduce first" in the general
|
||||
skill.
|
||||
- The driver may approve an exception, for example when the harness cannot
|
||||
reach the mechanism after about three attempts. Then the ticket must say
|
||||
"Not reproduced in the eval. Based on trace analysis."
|
||||
4. **Check the builder's own guidance for the gap.** Read the files that steer
|
||||
Agent Builder:
|
||||
`packages/cli/src/modules/agents/builder/agents-builder-prompts.ts`,
|
||||
`packages/cli/src/modules/agents/builder/prompts/`, and
|
||||
`packages/cli/src/modules/agents/builder/skills/`. Also read the Instance AI
|
||||
orchestrator prompt in `packages/@n8n/instance-ai/src/agent/system-prompt.ts`.
|
||||
If none of them covers the failing situation, add that to the ticket as a
|
||||
proposed prompt or skill change.
|
||||
5. Propose the smallest added line first. Do not delete existing guidance to fix a
|
||||
gap. A measured case: adding one line fixed a routing gap, and deleting two
|
||||
lines did not.
|
||||
6. Every prompt or skill change needs a before and after measurement. The new
|
||||
case is the "before" run. Run it again after the change. Use `--iterations 5`.
|
||||
|
||||
When the failure is in the Agent's runtime (for example a channel that cannot
|
||||
report an error), the builder cannot see it. The right fix is often a builder
|
||||
instruction to say so and stop guessing. Write that as a `processExpectations`
|
||||
item on the final response.
|
||||
|
||||
## Publish and link
|
||||
|
||||
Follow "Share links, never bare ids" and "Link the pushed case to its source" in
|
||||
the general skill. For a sourced Agent case:
|
||||
|
||||
- Report the case, the suite, and the source thread as links. Use the
|
||||
`https://lang-tracer.n8n-maintenance.workers.dev/test-cases/<id>` form.
|
||||
- Call `update_test_case` with `sourceThreadId`, `expectedBehavior`, and
|
||||
`failurePattern`.
|
||||
- Add a capability tag with `add_case_tags`.
|
||||
- Push a case whose *build* is wrong with `--set-kind capability_gap` into a
|
||||
suite of that kind. If no such Agent suite exists, tell the driver. Do not put
|
||||
it in a `regression` suite.
|
||||
|
||||
## Draft the case
|
||||
|
||||
|
|
@ -161,14 +340,14 @@ credential such as `EVAL_OPENAI_API_KEY`. A build-only case does not need one.
|
|||
4. Classify each red as a product gap, harness limitation, or non-determinism.
|
||||
5. Use `--iterations 5` before adding a case to a gating tier.
|
||||
6. Preview the LangTracer change.
|
||||
7. Push it to `agents`.
|
||||
7. Push it to the suite the driver chose (`<suite>` below).
|
||||
|
||||
```bash
|
||||
pnpm exec dotenvx run -f ../../../.env.local -- \
|
||||
pnpm eval:langtracer-push --suite agents --dry-run --changed
|
||||
pnpm exec dotenvx run -f ../../../.env.eval -- \
|
||||
pnpm eval:langtracer-push --suite <suite> --dry-run --changed
|
||||
|
||||
pnpm exec dotenvx run -f ../../../.env.local -- \
|
||||
pnpm eval:langtracer-push --suite agents --changed
|
||||
pnpm exec dotenvx run -f ../../../.env.eval -- \
|
||||
pnpm eval:langtracer-push --suite <suite> --changed
|
||||
```
|
||||
|
||||
The push needs `LANGTRACER_URL` and `LANGTRACER_API_KEY`. Generate a key on the
|
||||
|
|
@ -179,12 +358,20 @@ after a successful push.
|
|||
## Completion checklist
|
||||
|
||||
- The LangTracer environment preflight passed before authoring.
|
||||
- The driver chose the target suite before any work, including in autonomous mode.
|
||||
- The autonomy level was set and stated before any work.
|
||||
- A sourced case links to its source thread, and the source turn was read raw.
|
||||
- Each conditional expectation has proof that its precondition fired.
|
||||
- A kept product-gap red has a proposed Linear ticket and a check of the builder guidance.
|
||||
- The case is in `data/agents/` and uses the `agents` dataset.
|
||||
- The strict loader accepts it.
|
||||
- A local run captures a standalone Agent.
|
||||
- The transcript proves each process expectation was exercised.
|
||||
- The rendered Agent artifact proves each outcome expectation.
|
||||
- No expectation names a sub-agent, an internal tool, or a delegation step.
|
||||
Each one passes for a correct build from a different architecture.
|
||||
- Repeated runs are stable enough for the selected tier.
|
||||
- The case is pushed to `Instance AI capabilities — agents` with suite slug `agents`.
|
||||
- Agent Builder PR changes select the `agents` suite in CI.
|
||||
- The case is pushed to the suite the driver chose, and the report links to it.
|
||||
- Agent Builder PR changes select the `agents` suite in CI. A case pushed to
|
||||
another suite does not run in that lane.
|
||||
- The local case JSON is not committed.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Generate a key on the
|
||||
[LangTracer API keys page](https://lang-tracer.n8n-maintenance.workers.dev/account?section=api).
|
||||
Add it to `.env.local` at the repository root:
|
||||
Add it to `.env.eval` at the repository root:
|
||||
|
||||
```env
|
||||
LANGTRACER_URL=https://lang-tracer.n8n-maintenance.workers.dev
|
||||
|
|
@ -16,7 +16,7 @@ that both variables exist before it creates an eval.
|
|||
|
||||
## Remaining environment
|
||||
|
||||
Create `.env.local` at the repository root. Use one Instance AI model provider
|
||||
Create `.env.eval` at the repository root. Use one Instance AI model provider
|
||||
and one sandbox provider.
|
||||
|
||||
```env
|
||||
|
|
@ -76,7 +76,7 @@ N8N_USER_FOLDER=<printed-temp-directory> \
|
|||
E2E_TESTS=true \
|
||||
N8N_ENABLED_MODULES=instance-ai,agents \
|
||||
N8N_AI_ENABLED=true \
|
||||
pnpm exec dotenvx run -f ../../.env.local -- pnpm start
|
||||
pnpm exec dotenvx run -f ../../.env.eval -- pnpm start
|
||||
```
|
||||
|
||||
Use different ports when 5680 or 5681 is busy.
|
||||
|
|
@ -96,7 +96,7 @@ curl -sf -X POST http://localhost:5680/rest/e2e/reset \
|
|||
From `packages/@n8n/instance-ai`:
|
||||
|
||||
```bash
|
||||
pnpm exec dotenvx run -f ../../../.env.local -- \
|
||||
pnpm exec dotenvx run -f ../../../.env.eval -- \
|
||||
env -u LANGSMITH_API_KEY -u LANGSMITH_TRACING \
|
||||
pnpm eval:instance-ai \
|
||||
--base-url http://localhost:5680 \
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ exhaustive field reference; this skill is the opinionated *how*.
|
|||
> approach.** Author the file locally (uncommitted), calibrate it against a real
|
||||
> build, then **push it to a lang-tracer suite** with `eval:langtracer-push`
|
||||
> (see [Push to a lang-tracer suite](#push-to-a-lang-tracer-suite)) —
|
||||
> `--suite baseline` for the consolidated corpus n8n CI runs, or a dedicated
|
||||
> the suite the driver chose (see
|
||||
> [Ask for the target suite first](#ask-for-the-target-suite-first)) —
|
||||
> `baseline` for the consolidated corpus n8n CI runs, or a dedicated
|
||||
> capability suite like `agents`.
|
||||
> The suite is the home for the case; the eval CLI reads it back via
|
||||
> `--source langtracer`. You still write the JSON file — it's just the input to
|
||||
|
|
@ -38,6 +40,34 @@ exhaustive field reference; this skill is the opinionated *how*.
|
|||
> pruned and has no durable home. Don't commit a replay case either — derive a
|
||||
> synthetic case from it. See [`case-shapes.md`](case-shapes.md).
|
||||
|
||||
## Ask for the target suite first
|
||||
|
||||
**Before you source, draft, or run anything, ask the driver which LangTracer
|
||||
suite receives every eval you create in this session.** Ask this in both
|
||||
autonomy levels, including autonomous mode. Never pick the suite yourself and
|
||||
never default silently. A case pushed to the wrong suite runs in the wrong CI
|
||||
lane and alerts the wrong team.
|
||||
|
||||
Ask the suite question and the autonomy question together, in one message.
|
||||
Recommend `baseline`:
|
||||
|
||||
- `baseline` (recommended): the general corpus. The Instance AI (INS) team
|
||||
monitors it, and it runs nightly.
|
||||
- Any other suite, such as `agents` or `node-gauntlet`: call `list_suites` and
|
||||
offer the live list. Do not use a hardcoded list, because suites are added
|
||||
and retired.
|
||||
|
||||
Rules:
|
||||
|
||||
- The driver's answer applies to all cases in the session. Do not ask again per
|
||||
case.
|
||||
- If the driver already named a suite in the request, state it back in one line
|
||||
and continue. Do not ask again.
|
||||
- Check that the suite kind matches the case. Push a case with a wrong build
|
||||
(`capability_gap`) only into a suite that allows that kind. If the suite does
|
||||
not allow it, tell the driver and ask again.
|
||||
- Put the chosen suite slug in the final decision log, as a link.
|
||||
|
||||
## Set the autonomy level first
|
||||
|
||||
**Before you source, draft, or run anything, decide how hands-on the driver
|
||||
|
|
@ -468,6 +498,16 @@ read the run assumes someone already owns it. So once a red is classified as a
|
|||
real gap (and the driver has confirmed it, per the autonomy level), **propose a
|
||||
Linear ticket for it.**
|
||||
|
||||
**Reproduce first. This is a default, not a hard rule.** Propose a ticket only
|
||||
after a case has reproduced the gap. That means the case is red on a real build,
|
||||
the precondition was confirmed to fire, and you re-read the raw thread (see
|
||||
[First reproduce, then reclassify](#first-reproduce-then-reclassify)). A gap that
|
||||
comes from trace analysis alone is a hypothesis. A ticket for it costs the owning
|
||||
team time. The driver may approve an exception, for example when the harness
|
||||
cannot reach the mechanism after about three attempts. In that case the ticket
|
||||
must say that the eval did not reproduce the gap. Never file an unreproduced gap
|
||||
without the driver's approval.
|
||||
|
||||
**Propose, don't create.** Per [AGENTS.md](../../../AGENTS.md), never open a
|
||||
Linear ticket unasked. Put the draft in front of the driver — interactively in
|
||||
checkpoint mode, in the decision log in autonomous mode — with a title, a team,
|
||||
|
|
@ -619,6 +659,12 @@ nodes get LLM-generated pin data). So:
|
|||
otherwise
|
||||
accept the static-data red as a harness limit and carry the logic in
|
||||
`outcomeExpectations`. Note the agent may *choose* static-data dedup on its own.
|
||||
- **Message an Agent runs the called Agent with its real model.** The harness
|
||||
mocks the Agent's tools, MCP servers and web search, and records those calls
|
||||
under the calling node. It turns off the Agent's memory, vector stores and chat
|
||||
integrations. Only OpenAI models are supported today, and only when
|
||||
`EVAL_OPENAI_API_KEY` is set (CI sets it): put the Agent on an `openai/*` model
|
||||
and declare an `openAiApi` credential.
|
||||
- Don't assert exact counts that depend on mock generation ("exactly 7 posts").
|
||||
Say "fewer than the original 10".
|
||||
|
||||
|
|
@ -712,8 +758,9 @@ Both are natural-language assertions graded by the same Sonnet judge, and each
|
|||
They run everywhere, including prebuilt/MCP runs (no transcript needed).
|
||||
- **`processExpectations`** — **how the agent behaved during the build**, judged
|
||||
from the transcript. Assert clarifying questions asked (or not re-asked),
|
||||
tool-call behaviour, plan/approval handling, batching, honouring a correction,
|
||||
ordering. They need a transcript, so they're **skipped in prebuilt/MCP runs**.
|
||||
user-visible behaviour, plan/approval handling, batching, honouring a
|
||||
correction, ordering. Do not assert internal mechanics such as sub-agents or
|
||||
tool names (see "Keep expectations free of internal mechanics"). They need a transcript, so they're **skipped in prebuilt/MCP runs**.
|
||||
|
||||
Rule of thumb: an assertion about *the artifact* is an outcome expectation; an
|
||||
assertion about *the conversation or the agent's choices along the way* is a
|
||||
|
|
@ -751,6 +798,54 @@ case, where the source and channel were **left unspecified**):
|
|||
Put intent the conversation only *implied* (a preferred but unstated channel) in
|
||||
`processExpectations`, not `outcomeExpectations`.
|
||||
|
||||
## Keep expectations free of internal mechanics
|
||||
|
||||
An expectation describes what the user or the artifact sees. It does not
|
||||
describe how Instance AI is built inside. The internal architecture changes:
|
||||
sub-agents, tool names, delegation, planning steps, and run ordering all move.
|
||||
An expectation that names them goes red when the design changes, although the
|
||||
behavior is still correct. It also stops proving the behavior: a build that
|
||||
solves the problem by another route fails it.
|
||||
|
||||
**Do not name these in an expectation:**
|
||||
|
||||
- Sub-agents, delegation, or hand-off ("the sub-agent runs ...", "the
|
||||
orchestrator delegates ...").
|
||||
- Internal tool names or call counts ("calls `build-agent`", "calls
|
||||
`search-nodes` twice").
|
||||
- Internal stages, task lists, or step order that the user cannot see.
|
||||
- Which component produced a result.
|
||||
|
||||
**Do name these:**
|
||||
|
||||
- What the user sees in the conversation: the question asked, the plan shown
|
||||
for approval, the claim in the final response.
|
||||
- What the artifact contains: the workflow or Agent, its configuration, and
|
||||
how it behaves.
|
||||
- A user-visible sequence, only when the user asked for it ("asks before it
|
||||
builds").
|
||||
|
||||
Rewrite test. Delete the internal term. State the result the term produced.
|
||||
|
||||
| ❌ mechanical | ✅ intent |
|
||||
|---|---|
|
||||
| "The sub-agent runs the workflow before it reports success" | "The final response reports only results that the build verified; it does not claim an untested step works" |
|
||||
| "The orchestrator calls `ask-user` before delegating" | "The agent asks for the missing Slack channel before it builds, and does not guess one" |
|
||||
| "The builder calls `search-nodes` for the trigger" | "The workflow starts from a trigger that matches the requested schedule" |
|
||||
|
||||
Second test. Imagine the internals are replaced by one agent with different
|
||||
tools. A correct build must still pass. If the expectation would fail or become
|
||||
meaningless, rewrite it.
|
||||
|
||||
Some mechanics are a legitimate subject: a case whose purpose is to guard a
|
||||
specific tool contract (for example, a regression in how a tool reports an
|
||||
error). Keep these rare. State the reason in `description`. Do not mix them with
|
||||
intent expectations in the same case.
|
||||
|
||||
**Calibration steps are not expectations.** Reading the transcript to confirm
|
||||
that a build ran, or that a sub-agent was reached, is a good calibration check.
|
||||
Keep it in your own run notes. Do not copy it into the case JSON.
|
||||
|
||||
## Robust design vs harness flakiness
|
||||
|
||||
Two different things — keep them apart:
|
||||
|
|
@ -837,10 +932,11 @@ drifted, leaves the rest unchanged, and never prunes. It's the inverse of
|
|||
|
||||
```bash
|
||||
cd packages/@n8n/instance-ai
|
||||
# <suite> is the slug the driver chose (e.g. baseline)
|
||||
# preview first — no writes:
|
||||
pnpm exec dotenvx run -f .env.eval -- pnpm eval:langtracer-push --suite baseline --dry-run --changed
|
||||
pnpm exec dotenvx run -f .env.eval -- pnpm eval:langtracer-push --suite <suite> --dry-run --changed
|
||||
# then push (drop --dry-run):
|
||||
pnpm exec dotenvx run -f .env.eval -- pnpm eval:langtracer-push --suite baseline --changed
|
||||
pnpm exec dotenvx run -f .env.eval -- pnpm eval:langtracer-push --suite <suite> --changed
|
||||
```
|
||||
|
||||
- **Selectors** (at least one required — no accidental push-all): positional
|
||||
|
|
|
|||
|
|
@ -47,9 +47,20 @@ ids. Full table in
|
|||
1. **Scan cluster themes** — `list_cluster_runs` / `get_latest_cluster_run`
|
||||
return capability-gap themes with a `label`, `summary`, and `mechanism`: the
|
||||
real, recurring failure modes.
|
||||
2. **Pull the conversations** — `list_conversations` (e.g. `verdict:"bad"`,
|
||||
`analyzed:"yes"`) → `get_conversation` (raw trace) + `get_conversation_analysis`
|
||||
(findings). `get_linear_ticket_context` links a thread to its ticket. To target
|
||||
2. **Pull the conversations.** Start from what LangTracer already detected.
|
||||
`list_signal_rates` lists every code check and label rule with its rate over a
|
||||
window: what fails most right now. Then `list_conversations` with `signals`,
|
||||
one token per signal, ANDed — `check:has_nodes`,
|
||||
`label:task_outcome=agent-gave-up`, `analysis:bad`, `keeper:*`, `review:*` — puts
|
||||
the matching entries on each row, each with its evidence (the verdict comment,
|
||||
the judge's words, Keeper's reason) and its version. On a candidate,
|
||||
`get_conversation_signals` shows what every source said and which ones agreed:
|
||||
a thread flagged by a code check, the judge and Keeper is a stronger source
|
||||
than one flagged by a single model pass. A source marked `not_run` never looked
|
||||
at the thread, so read its silence as unknown, not as clean. The older path
|
||||
still works: `list_conversations` (e.g. `verdict:"bad"`, `analyzed:"yes"`) →
|
||||
`get_conversation` (raw trace) + `get_conversation_analysis` (findings).
|
||||
`get_linear_ticket_context` links a thread to its ticket. To target
|
||||
**execution failures**, add `funnelDrop:"05"` (built + launched an execution
|
||||
that never *succeeded*). **Caveat: the funnel drop is a *conversion* signal, not
|
||||
a build-quality one** — most `funnelDrop:"05"` threads are healthy builds that
|
||||
|
|
@ -61,7 +72,8 @@ ids. Full table in
|
|||
(was `build-workflow` called on this turn?), which it reads reliably — but a
|
||||
content-dependent claim ("invented ID", "missing node") can be wrong when it
|
||||
couldn't see the built workflow. Confirm against the raw trace before building
|
||||
a case around it.
|
||||
a case around it. The same goes for a signal's `evidence`: it is what that
|
||||
source saw, not the trace.
|
||||
4. **Optionally record the selection as an observation.** Offer to save why this
|
||||
thread was selected and what the developer observed. This is a best-effort
|
||||
LangTracer note, not a gate: skip it when the developer declines or has not
|
||||
|
|
|
|||
|
|
@ -78,11 +78,24 @@ Creates GitHub PRs with titles that pass n8n's `check-pr-title` CI validation.
|
|||
body structure, then populate each section with actual content before
|
||||
creating the PR:
|
||||
- **Summary**: describe what the PR does
|
||||
- **Why**: explain why the change is needed (see Why below)
|
||||
- **How to test**: describe how to test the changes. Include an example workflow if appropriate
|
||||
- **Related tickets**: add the Linear ticket URL (`https://linear.app/n8n/issue/[TICKET-ID]`) and any GitHub issue links
|
||||
- **Checklist**: keep as-is from the template
|
||||
- Add a "🤖 PR Summary generated by AI" at the end of the body
|
||||
|
||||
Before you create the PR, proofread the body with these checks. Rewrite
|
||||
each sentence that fails a check:
|
||||
1. The Summary and the Why together are under the word budget. If not,
|
||||
first remove the details that the reviewer reads in the diff, then
|
||||
shorten the Why.
|
||||
2. Each sentence names who does what: a component, a process, or a person
|
||||
as the subject, with a plain technical verb.
|
||||
3. Each domain term has a plain definition where it first appears.
|
||||
4. Each sentence is true for the diff.
|
||||
|
||||
When a later commit changes a fact in the body, update the body.
|
||||
|
||||
```bash
|
||||
gh pr create --draft --title "<type>(<scope>): <summary>" --body "$(cat <<'EOF'
|
||||
<populated body based on pull_request_template.md>
|
||||
|
|
@ -94,12 +107,43 @@ Creates GitHub PRs with titles that pass n8n's `check-pr-title` CI validation.
|
|||
|
||||
Based on `.github/pull_request_template.md`:
|
||||
|
||||
Write the PR body in ASD-STE100 Simplified Technical English: use short
|
||||
sentences, the active voice, and one instruction for each sentence.
|
||||
Write for a reviewer who does not know this area of the code. The reviewer
|
||||
must understand the change before they open the diff. Use ASD-STE100
|
||||
Simplified Technical English: short sentences, active voice.
|
||||
1. Keep the Summary and the Why together under about 200 words. A small PR
|
||||
needs much less.
|
||||
2. Define each domain term in plain words where it first appears. Do not use
|
||||
a ticket ID or an internal name as an explanation.
|
||||
3. Leave out details that the reviewer reads in the diff: error cases, status
|
||||
codes, renamed fields, the order of calls, file lists, test names. Mention
|
||||
one only when it changes behaviour that a reviewer could miss. Also leave
|
||||
out what reviewers already know (framework behaviour).
|
||||
4. Use plain technical verbs, not metaphors ("passes the value", not
|
||||
"hands the value" or "lets the value through").
|
||||
|
||||
### Summary Section
|
||||
- Describe what the PR does
|
||||
- Include screenshots/videos for UI changes
|
||||
### Summary: what the PR does
|
||||
1. Start with one to three sentences of background for a reader who is new to
|
||||
the area. Then state what the PR does.
|
||||
2. Write **prose** about behaviour, not a list of code edits. Name a class or
|
||||
a function when it represents an essential/core concept and it helps
|
||||
the reader to navigate the change.
|
||||
3. If the PR changes no product code, say so in the first sentence
|
||||
(for example: "This PR only adds tests.").
|
||||
4. Do not justify the change here. The justification goes in the Why.
|
||||
5. Include screenshots or videos for UI changes.
|
||||
|
||||
### Why: the reasoning
|
||||
Always fill the `## Why` section. The Summary and the core of the
|
||||
Why are what the reviewer needs most. Keep the core short:
|
||||
1. Do not repeat the Summary.
|
||||
2. State the problem or the constraint that makes the pre-existing code unfit.
|
||||
3. If there are accepted gaps, or changes for existing code paths, state them
|
||||
in a separate short paragraph.
|
||||
|
||||
Add rejected alternatives only when a reviewer would otherwise ask about them,
|
||||
if any. Use one or two sentences for each alternative: state the concrete
|
||||
reason it fails. Write an alternative that was never built in the conditional.
|
||||
Together, the alternatives must be shorter than the core Why section.
|
||||
|
||||
### How to test Section
|
||||
- Explain how to test the changes
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ Run through this before requesting review. Each item is a real, recurring review
|
|||
- [ ] Identifiers go through **`escape.tableName(...)` / `escape.columnName(...)`**. Never hand-write `n8n_table` prefixes. — [Always escape identifiers](#always-escape-identifiers)
|
||||
- [ ] **Match column type to value semantics.** Native `uuid` for UUIDs, `timestampTimezone()` for timestamps, a numeric type for numbers, `bool` for booleans, `json` for structured data. Never `varchar` as a catch-all. — [Column types](#column-types)
|
||||
- [ ] **Pick the narrowest sane type within that category:** `int`/`smallint` not `bigint` when range allows; `text` not `varchar(255)` for unbounded strings; never `double` for version numbers. — [Column types](#column-types)
|
||||
- [ ] **A generated `id` key is an integer** (`int.primary.autoGenerate2`). A UUID key is a UUIDv7. For a UUID or string key, write the reason in the PR description. A join table uses its foreign-key columns as the key, not an `id` column. — [Primary Keys](#primary-keys)
|
||||
- [ ] **Default `notNull`**, relax only when justified. PK is implicitly NOT NULL. Migration's `notNull` matches the entity's nullability. — [NOT NULL and entity parity](#not-null-and-entity-parity)
|
||||
- [ ] **Enum-like columns** carry `.withEnumCheck([...])` AND `.comment('explains values')`. Opaque IDs / unix timestamps / JSON shapes also get `.comment()`. — [Constrain enum-like strings](#constrain-enum-like-strings), [Add comments on columns](#add-comments-on-columns)
|
||||
- [ ] **Every reference column has an explicit FK** with deliberate `onDelete`. Name FKs explicitly when SQLite recreate cycles risk duplicating them. Avoid polymorphic `(typeCol, idCol)` patterns. — [Foreign Key Constraints](#foreign-key-constraints), [General Design Guidance](#general-design-guidance)
|
||||
|
|
@ -439,11 +440,32 @@ Don't set the default of a `status` column to a terminal value — `"running"` m
|
|||
|
||||
### Primary Keys
|
||||
|
||||
Every table needs a primary key. Choose the type in this order:
|
||||
Every table needs a primary key. **Use an integer key by default.** Read the options below in order. Use the first option that fits. Do not skip an option only because you like a later one better.
|
||||
|
||||
1. **Integer** — `column('id').int.primary.autoGenerate2`. Preferred for new tables: compact, fast joins, no ordering surprises. `autoGenerate2` uses Postgres `IDENTITY` (preferred over the deprecated `serial`-based `autoGenerate`).
|
||||
2. **UUID** — `column('id').uuid.primary`. Use when IDs are generated client-side, exposed in URLs, or need to be unguessable. Generate UUIDs in application code via `randomUUID()` from `node:crypto`; do **not** chain `.autoGenerate2` on `.uuid` (the DSL throws — `DEFAULT uuid_generate_v4()` fails on managed Postgres like Supabase because it needs the `uuid-ossp` extension in `public`). Use `.uuid` instead of `.varchar(36)`.
|
||||
3. **String** — `column('id').varchar(36).primary` for IDs whose format isn't a UUID (e.g. nanoid-style IDs). Convention: nanoid length 16 for entity IDs.
|
||||
1. **Integer (default)** — `column('id').int.primary.autoGenerate2`. Use this for every new table. Use a different option only when one of its conditions is true. Integer keys are small and make joins fast. `autoGenerate2` uses Postgres `IDENTITY`. Do not use `autoGenerate`. It uses `serial`, which is deprecated.
|
||||
2. **UUID** — `column('id').uuid.primary`. Use this only when one of these conditions is true:
|
||||
- The application must know the ID before the insert. For example, the client makes the ID.
|
||||
- The ID is in a URL or a public API. Users must not see how many rows exist or guess other IDs.
|
||||
|
||||
**You must use UUIDv7.** Make the ID in application code with the `uuid` package: `import { v7 as uuidv7 } from 'uuid'`.
|
||||
|
||||
Why: a UUIDv7 starts with a timestamp. Thus, new rows go near the end of the index. A UUIDv4 is random, for example `randomUUID()` from `node:crypto`. New rows go to random positions in the index. This splits index pages and makes the index larger.
|
||||
|
||||
A UUIDv7 does not give an exact creation order. Two IDs made in the same millisecond can sort in the wrong order. Clock differences between hosts can also cause this. If you need the exact order, sort by a timestamp column.
|
||||
|
||||
Do **not** chain `.autoGenerate2` on `.uuid`. The DSL throws an error. The database default makes a UUIDv4. Also, `uuid_generate_v4()` needs the `uuid-ossp` extension. Some managed Postgres services, such as Supabase, do not have it. Use `.uuid`, not `.varchar(36)`.
|
||||
3. **String** — `column('id').varchar(36).primary`. Use this only when one of these conditions is true:
|
||||
- The entity already uses IDs in a format that is not a UUID. For example, the public API or the UI already shows nanoid IDs for this entity.
|
||||
- The UI shows the ID to users. A UUID is too long to read, copy, or type.
|
||||
|
||||
Entity IDs are usually nanoids with 16 characters.
|
||||
|
||||
**Give a reason for every key that is not an integer.** Write the reason in the PR description. Reviewers will ask for it. These reasons are not sufficient:
|
||||
- "Other tables use string IDs."
|
||||
- "This module already does it this way."
|
||||
- "We might need it later."
|
||||
|
||||
**These options apply to a generated `id` column only.** A join table that only links other tables does not need an `id` column. Use its foreign-key columns as a composite key (see below). A foreign-key column always has the same type as the key that it refers to.
|
||||
|
||||
**Keep ID-column types consistent across related tables.** Mixing `uuid` and `varchar(36)` for what is "the same kind of ID" creates JOIN footguns.
|
||||
|
||||
|
|
@ -696,7 +718,7 @@ If only Postgres needs the change, just put the file in `postgresdb/`; don't wri
|
|||
|
||||
### Postgres-version-aware UUID generation
|
||||
|
||||
`gen_random_uuid()` requires Postgres ≥ 13. n8n dropped Postgres 12 — prefer it over `uuid_generate_v4()` (which needs the `uuid-ossp` extension and breaks on managed services like Supabase). For UUID PKs, generate at the application level with `randomUUID()` — see [Primary Keys](#primary-keys).
|
||||
`gen_random_uuid()` requires Postgres ≥ 13. n8n dropped Postgres 12 — prefer it over `uuid_generate_v4()` (which needs the `uuid-ossp` extension and breaks on managed services like Supabase). For UUID PKs, make a UUIDv7 in application code — see [Primary Keys](#primary-keys).
|
||||
|
||||
### SQLite doesn't enforce `varchar(N)` length
|
||||
|
||||
|
|
|
|||
158
.agents/skills/env-var-docs/SKILL.md
Normal file
158
.agents/skills/env-var-docs/SKILL.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
name: n8n:env-var-docs
|
||||
description: >-
|
||||
Writes and reviews the JSDoc on `@Env(...)` config properties. The docs team
|
||||
extracts these comments from source into the public environment variable
|
||||
reference, so they are user documentation, not code comments. Use when adding
|
||||
or changing an `@Env` property, a config class, or reviewing a PR that touches
|
||||
one.
|
||||
---
|
||||
|
||||
# Environment variable docs
|
||||
|
||||
The JSDoc above every `@Env('...')` property is exported almost verbatim into
|
||||
the [environment variables reference](https://docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables)
|
||||
on docs.n8n.io. The generated table has four columns: variable name,
|
||||
description, data type, default. Your JSDoc is the description column. The
|
||||
reader is a self-hosting operator, not an n8n developer.
|
||||
|
||||
Follow the [n8n docs style guide](https://docs.n8n.io/contribute/contribution-guide-for-n8n-docs/style-guide-for-n8n-docs)
|
||||
for these comments. It overrides the ASD-STE100 rule in `AGENTS.md` for this
|
||||
one case. The rules below are the parts of that guide that matter for a
|
||||
one-paragraph description.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Every `@Env` property has a JSDoc.** No exceptions. A missing comment
|
||||
ships as an empty description cell.
|
||||
- **Write for the operator.** Say what the setting does, when to change it,
|
||||
and what happens when it's unset or wrong. Don't explain why the code is
|
||||
written the way it is.
|
||||
- **No code references.** No file names, class names, function names,
|
||||
constants, license keys, or other internals the reader can't open. Values
|
||||
the operator types into the variable are fine and often needed, for example
|
||||
a credential type ID like `googleOAuth2Api` or a mode like `postgresdb`.
|
||||
The test: would the operator paste this into a config file, or would they
|
||||
need the source to know what it means? Details for developers go in a
|
||||
regular `//` comment on the implementation, not in the JSDoc.
|
||||
- **No attack scenarios.** Say what the setting protects, not how an
|
||||
attacker would exploit it. This is a public page.
|
||||
- **Don't repeat the default.** The default renders in its own column. Don't
|
||||
write "Defaults to true" or "Default: 1 hour". Do say what the unset state
|
||||
means when it's not obvious, for example "When unset, n8n uses the Redis
|
||||
host".
|
||||
- **State units and formats.** Say "in milliseconds", "in bytes", "a
|
||||
comma-separated list", "a JSON object". The data type column only says
|
||||
`Number` or `String`.
|
||||
- **Use allowed values when the set is small.** "One of `sqlite`,
|
||||
`postgresdb`." For a list, show one inline example in the sentence.
|
||||
- **No JSDoc tags.** No `@example`, `@default`, `@see`, `@deprecated`. The
|
||||
exporter copies the tag name into the docs. Put the example or the
|
||||
deprecation notice in a sentence instead.
|
||||
- **Product names:** "Gateway credits", not "n8n Connect" or "AI Gateway".
|
||||
"Publish a workflow", not "activate". `n8n` is always lowercase.
|
||||
- **Style:** present tense, active voice, second person. Use contractions.
|
||||
No em dashes, use a comma or a new sentence. No "e.g." or "i.e.", write
|
||||
"for example" and "that is". Spell out zero to nine, numerals from 10.
|
||||
Values the operator types stay numerals, for example "Set to 0".
|
||||
Sentence case. One idea per sentence. End every sentence with a period.
|
||||
- **Plain words:** "use" not "utilize", "lets you" not "allows you to", cut
|
||||
"powerful", "seamless", "simply", "it's important to note".
|
||||
- **Length:** one to three sentences. If the setting needs more, the extra
|
||||
belongs in a docs page the comment can't link to yet; keep the comment to
|
||||
what fits a table cell.
|
||||
|
||||
## Examples
|
||||
|
||||
Bad, as found in the generated docs:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Instance-level enablement for n8n Connect (AI Gateway). Licensed instances
|
||||
* are enabled by default; false opts out. Budget / free allowance still comes
|
||||
* from the license (`quota:aiGatewayBudget`).
|
||||
*/
|
||||
```
|
||||
|
||||
Wrong product name, a restated default, and an internal license key. Good:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Whether Gateway credits are available on this instance. Set to false to
|
||||
* turn them off on a licensed instance. The credit allowance comes from your
|
||||
* license.
|
||||
*/
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Time to live in milliseconds for Redis cache entries. Set to 0 to disable
|
||||
* expiry. Default: 1 hour.
|
||||
*/
|
||||
```
|
||||
|
||||
The default is already in the table. Good:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Time to live in milliseconds for Redis cache entries. Set to 0 to keep
|
||||
* entries until Redis evicts them.
|
||||
*/
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Comma-separated list of credential types for which overwrites are skipped
|
||||
* when the credential has been customized.
|
||||
* @example `N8N_SKIP_CREDENTIAL_OVERWRITE=zohoOAuth2Api,salesforceOAuth2Api`
|
||||
*/
|
||||
```
|
||||
|
||||
The `@example` tag lands in the description as literal text. Good:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Comma-separated list of credential types to skip when applying credential
|
||||
* overwrites, for example `zohoOAuth2Api,salesforceOAuth2Api`. n8n skips an
|
||||
* overwrite only when a user has already changed one of its fields.
|
||||
*/
|
||||
```
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Bind OAuth credential callbacks to the browser that initiated the flow.
|
||||
* When enabled, a session-scoped `n8n-oauth-binding` cookie is set on flow
|
||||
* initiation and verified on callback. Defeats phishing attacks where an
|
||||
* attacker injects an OAuth authorize URL into a victim's browser to land
|
||||
* tokens on the attacker's credential.
|
||||
*/
|
||||
```
|
||||
|
||||
Attack narrative and an internal cookie name on a public page. Good:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Whether an OAuth credential connection must finish in the same browser
|
||||
* that started it. When enabled, n8n rejects the callback if it arrives
|
||||
* from a different browser.
|
||||
*/
|
||||
```
|
||||
|
||||
## Review checklist
|
||||
|
||||
When you review a PR that adds or changes an `@Env` property, check each
|
||||
comment against this list and quote the fix in the review:
|
||||
|
||||
1. JSDoc present.
|
||||
2. No code identifiers, file names, or internal keys.
|
||||
3. No default restated.
|
||||
4. Units, format, or allowed values stated.
|
||||
5. No JSDoc tags.
|
||||
6. No em dashes, Latin abbreviations, or filler words.
|
||||
7. Reads correctly as a sentence in a table cell with no surrounding context.
|
||||
|
|
@ -21,8 +21,7 @@ check the code, not license to drop a team default.
|
|||
## Non-negotiable rules
|
||||
|
||||
- New endpoints are `@PublicApiController` classes under `v1/controllers/`, one
|
||||
`*.public.controller.ts` per feature. A controller is a class — never
|
||||
`export =` (the legacy tuple style; `require-public-api-controller` flags it).
|
||||
`*.public.controller.ts` per feature.
|
||||
- Public API and internal REST are separate HTTP surfaces. A public controller
|
||||
never calls an internal controller/endpoint; both reuse the same service.
|
||||
- Controllers and handlers delegate to a service — never import a repository or
|
||||
|
|
@ -31,7 +30,6 @@ check the code, not license to drop a team default.
|
|||
`@ApiResponse(Dto)`.
|
||||
- Register each controller via a side-effect import in `v1/controllers/index.ts`
|
||||
(`public-api-controllers.test.ts` fails otherwise).
|
||||
- Don't add business logic to legacy `express-openapi-validator` (EOV) handlers.
|
||||
- Migrating a legacy endpoint must not change its public contract.
|
||||
|
||||
These are `n8n-local-rules` ESLint rules (see `packages/cli/eslint.config.mjs`)
|
||||
|
|
@ -109,10 +107,10 @@ model; reuse only what applies. Decorators, all from `@n8n/decorators`:
|
|||
| `@Get/@Post/@Put/@Patch/@Delete('/path')` | Route method. |
|
||||
| `@ApiKeyScope('res:action')` | API-key grant check. |
|
||||
| `@ProjectScope/@GlobalScope('res:action')` | User RBAC check. |
|
||||
| `@ApiResponse(status)` / `@ApiResponse(status, Dto)` | Success status + (optional) output DTO; registry `.parse()`s + strips the return value. Exactly one per route — a second `@ApiResponse` throws. `204` can't carry a DTO — throws. |
|
||||
| `@ApiResponse(status)` / `@ApiResponse(status, Dto)` | Success status + (optional) output DTO; registry `.parse()`s + strips the return value. Exactly one per route — a second `@ApiResponse` throws. `204` can't carry a DTO or a binary body — throws. For a binary response body (returned as `{ body, headers? }`), use `@ApiResponse(status, { mediaType, description?, headers? })` — see [Binary response bodies](reference.md#binary-response-bodies). |
|
||||
| `@ApiErrorResponse(status)` | Declares an additional documented non-2xx status (e.g. `404`, `409`). Stack multiple for more than one. `400`/`401`/`403` are added automatically (body/query present, always, and `@ApiKeyScope` present, respectively) — don't declare those yourself. |
|
||||
| `@ApiSummary(text)` / `@ApiDescription(text)` / `@ApiTags([...])` | OpenAPI summary/description/tags. `@ApiTags` sorts alphabetically regardless of the order you pass. All optional but expected on every real route. |
|
||||
| `@Query` / `@Body` / `@Param('name')` | Bind + validate via a `Z.class` DTO / path param. |
|
||||
| `@Query` / `@Body` / `@Param('name')` | Bind + validate via a `Z.class` DTO / path param. `@Body` is JSON by default; `@Body({ mediaType: 'multipart/form-data', uploadLimits })` takes a `multipart/form-data` body instead — see [Request body media types](reference.md#request-body-media-types). |
|
||||
| `@Licensed('feat')` | Gates the route on a single `BooleanLicenseFeature`; `PublicApiControllerRegistry` runs its own license middleware (after auth/`@ApiKeyScope`/`@ProjectScope`|`@GlobalScope`, before the handler) and 403s unlicensed requests. Only takes one feature — if the gate is an any-of/all-of combination (e.g. `LicenseState.isProvisioningLicensed()`, which is `feat:saml` OR `feat:oidc`), `@Licensed` can't express that; check manually in the handler instead, same as the internal `provisioning.controller.ee.ts`/`role-mapping-rule.controller.ee.ts` do today (throwing `ForbiddenError` on failure). |
|
||||
|
||||
## Authorization (easy to get wrong)
|
||||
|
|
@ -200,6 +198,5 @@ existing tests.
|
|||
- [Test-before-save endpoints](reference.md#test-before-save-endpoints)
|
||||
- [Errors](reference.md#errors)
|
||||
- [Testing matrix](reference.md#testing-matrix)
|
||||
- [Migrating legacy EOV endpoints](reference.md#migrating-legacy-eov-endpoints)
|
||||
- [Verifying a migration](reference.md#verifying-a-migration)
|
||||
- [CI and merging](reference.md#ci-and-merging)
|
||||
|
|
|
|||
|
|
@ -103,55 +103,76 @@ scope, RBAC denial. Add whichever apply, matching the nearest existing tests:
|
|||
unchanged. Tests alone cannot show this — see
|
||||
[Verifying a migration](#verifying-a-migration).
|
||||
|
||||
## Migrating legacy EOV endpoints
|
||||
## Request body media types
|
||||
|
||||
Legacy `express-openapi-validator` endpoints live under
|
||||
`v1/handlers/`, wired through `openapi.yml` with `x-eov-operation-*` and request
|
||||
types in `packages/cli/src/public-api/types.ts`. Treat these as migration targets,
|
||||
not templates.
|
||||
`@Body` defaults to `application/json`; the body is already parsed by the
|
||||
app-wide `bodyParser` before the registry sees it. Declaring
|
||||
`@Body({ mediaType: 'multipart/form-data', uploadLimits })` instead takes a
|
||||
`multipart/form-data` body:
|
||||
|
||||
- Prefer migrating to `@PublicApiController` over extending the handler.
|
||||
- Preserve the public contract: path, method, scopes, status codes, response
|
||||
shape, and pagination.
|
||||
- Move HTTP concerns into the controller and business orchestration into the
|
||||
shared service; keep the controller a thin HTTP boundary. If the legacy
|
||||
handler was itself already just a thin wrapper around an internal
|
||||
`@RestController` (calling its methods directly, e.g.
|
||||
`Container.get(SomeController).createThing(req, res, payload)`), the new
|
||||
public controller can call that same internal controller directly — no need
|
||||
to duplicate its validation/business logic.
|
||||
- A route must be served by either the EOV handler or a controller, not both —
|
||||
the build's `mergeDecoratorDocument` (`v1/openapi-gen/generate.ts`) throws on
|
||||
a path+method declared by both sides. Remove the legacy wiring (its path's
|
||||
`$ref` entry in `openapi.yml`, the `x-eov-operation-*` handler, and its
|
||||
`handler.ts`) only after the new controller is registered, `pnpm build`
|
||||
regenerates the spec cleanly, and tests are updated.
|
||||
- Fully delete the migrated legacy files: the handler's `.ts`, its
|
||||
`spec/paths/*.yml` and `spec/schemas/*.yml`, and any now-dead request type
|
||||
in `packages/cli/src/public-api/types.ts`. Then check
|
||||
`v1/shared/spec/schemas/_index.yml` and `v1/shared/spec/parameters/_index.yml`
|
||||
for entries that `$ref` one of the deleted schema/parameter files — those are
|
||||
separate from the path's own `$ref` in `openapi.yml` and are easy to miss;
|
||||
left dangling, the next bundle fails on a broken `$ref`.
|
||||
- If the legacy handler gated on a license (`isLicensed('feat:x')` middleware),
|
||||
`@Licensed('feat:x')` now replicates that for a controller route (see the
|
||||
decorator table in [SKILL.md](SKILL.md#declaring-a-controller)) — but only for
|
||||
a single feature. If the legacy check was an any-of/all-of over several flags
|
||||
(e.g. `LicenseState.isProvisioningLicensed()`), `@Licensed` can't express
|
||||
that; replicate it manually in the controller instead, don't drop it - this
|
||||
is exactly what the internal `provisioning.controller.ee.ts` and
|
||||
`role-mapping-rule.controller.ee.ts` already do, since neither uses
|
||||
`@Licensed` for that reason.
|
||||
- As a legacy file drops repository access / the `export =` tuple, remove its
|
||||
entry from the `off` allowlists for `no-repository-in-public-api-handler` and
|
||||
`require-public-api-controller` in `packages/cli/eslint.config.mjs` (shrink-only
|
||||
— never extend them).
|
||||
- For complex legacy-only, multipart, or non-standard endpoints, study the
|
||||
nearest existing handler first.
|
||||
- Keep each field in its original position when you extract a request shape shared
|
||||
by two routes, and destructure out the ones a route doesn't take. The generator
|
||||
emits properties in shape order, so a moved field rewrites the `*.generated.yml`
|
||||
of a route the PR wasn't changing.
|
||||
- `uploadLimits: () => MultipartUploadLimits` is a thunk, read once the route
|
||||
handles its first request (not at startup) — read live config inside it
|
||||
(e.g. `Container.get(GlobalConfig)`), don't inline a literal.
|
||||
- The route's `@Body` DTO validates text fields merged with uploaded files,
|
||||
not `req.body` alone. A text field and a file share the same flat object;
|
||||
a file field uses `publicApiUploadedFileSchema` (`@n8n/api-types`) — a
|
||||
multer-file-shaped schema that documents itself as `{ type: 'string',
|
||||
format: 'binary' }`. Several files under the same field name become an
|
||||
array.
|
||||
- An unknown field (one the DTO doesn't declare, on a `{ strict: true }` DTO)
|
||||
fails with `Unexpected form field "<name>"` — not the default "unrecognized
|
||||
keys" wording.
|
||||
- Multer parsing errors map to these statuses: `413` for a size or count limit,
|
||||
`500` for `LIMIT_UNEXPECTED_FILE`, `400` for any other multer error, and `500`
|
||||
for anything else (a malformed body). The `500` is not masked, because it is a
|
||||
`ResponseError`. A missing boundary gives `400` with `multipart file(s) required`.
|
||||
- The body is parsed **after** every auth/scope/license/quota gate and
|
||||
**before** controller/route middlewares — a caller those gates would reject
|
||||
never has their (possibly huge) body read off the socket, and a middleware
|
||||
that reads `req.body` sees it already parsed.
|
||||
- `/discover` shows no request schema for a multipart route (same as a legacy
|
||||
multipart route today) — a client can't assume a JSON schema it never gets.
|
||||
- The generator documents the body under the `multipart/form-data` content
|
||||
key (not `application/json`) and adds `413` alongside `415` to the route's
|
||||
documented responses automatically.
|
||||
|
||||
Everything above lives in `packages/cli/src/public-api/media-types/`, one
|
||||
handler per media type (`REQUEST_BODY_HANDLERS` in
|
||||
`media-types/request-body/index.ts`). Adding a further media type (e.g.
|
||||
`application/octet-stream`) means: add it to `RequestBodyMediaOptions` in
|
||||
`@n8n/decorators`'s `controller/types.ts`, write a handler implementing
|
||||
`RequestBodyHandler` in `media-types/request-body/`, and register it in
|
||||
`REQUEST_BODY_HANDLERS` — the registry, resolver, generator and `/discover`
|
||||
need no change, since they all read the handler, not the media type.
|
||||
|
||||
## Binary response bodies
|
||||
|
||||
`@ApiResponse(status, { mediaType, description?, headers? })` declares a
|
||||
success body that the controller method returns as `{ body, headers? }`, for
|
||||
example an `application/gzip` stream. The framework treats options whose
|
||||
`mediaType` is in `BINARY_RESPONSE_MEDIA_TYPES` as a binary response.
|
||||
`mediaType` accepts only those types.
|
||||
|
||||
- `body` is a `Buffer` or a `Readable`. The method returns the result and never
|
||||
writes to `res`.
|
||||
- `headers` holds the response headers. Every header declared in `headers` must
|
||||
be in the result. A missing header fails the request with a `500`. Matching is
|
||||
case-insensitive.
|
||||
- The registry checks the headers, then sets the declared status, the headers,
|
||||
and `Content-Type: <mediaType>`. A header in the result cannot replace the
|
||||
`Content-Type`.
|
||||
- A stream's first chunk is read before any header is set. So a stream that
|
||||
fails before its first chunk gives the normal JSON error response, with no
|
||||
binary `Content-Type`. A method that throws before it returns does the same.
|
||||
- After the first chunk is sent, an error ends the response. A client that
|
||||
disconnects is not an error, and the framework destroys the stream.
|
||||
- A method that returns no binary body fails with a `500`.
|
||||
- The generator documents the body as `{ type: string, format: binary }`
|
||||
under the `mediaType` content key, with the description and headers.
|
||||
The description defaults to `Operation successful.`.
|
||||
|
||||
The runtime part is `sendBinaryResponse` in
|
||||
`packages/cli/src/public-api/media-types/binary-response.ts`.
|
||||
|
||||
## Verifying a migration
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Based on the affected area, pick the test layer and pattern:
|
|||
| CLI / API | Vitest integration | setupTestServer + supertest | `packages/cli/test/integration/` |
|
||||
| Config | Vitest unit | GlobalConfig + Container | `packages/@n8n/config/src/configs/__tests__/` |
|
||||
| Editor UI | Vitest | Vue Test Utils + Pinia | `packages/frontend/editor-ui/src/**/__tests__/` |
|
||||
| E2E / Canvas | Playwright | Test containers + composables | `packages/testing/playwright/` |
|
||||
| E2E / Canvas | Playwright | Test containers + composables | `packages/quality/testing/playwright/` |
|
||||
|
||||
## Step 3: Locate Source Files
|
||||
|
||||
|
|
|
|||
2
.aikido
2
.aikido
|
|
@ -8,7 +8,7 @@ exclude:
|
|||
# Machine-generated instance-AI expectation traces. Contain ephemeral
|
||||
# credential reference IDs (nanoids) that re-trip the secrets scanner
|
||||
# on every regeneration. No secret material.
|
||||
- packages/testing/playwright/expectations
|
||||
- packages/quality/testing/playwright/expectations
|
||||
|
||||
# Test code repo-wide. Fixtures routinely contain synthetic PEM blocks,
|
||||
# basic-auth strings, and deliberately fake keys (including the secret
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"issues": 141
|
||||
"issues": 110
|
||||
}
|
||||
|
|
|
|||
1
.claude/plugins/n8n/skills/env-var-docs
Symbolic link
1
.claude/plugins/n8n/skills/env-var-docs
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../.agents/skills/env-var-docs
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Run long-lived, human-steered agent sessions (Claude Code / OpenCode) on a
|
||||
GitHub Codespace instead of your laptop: start a task, close the lid, steer it
|
||||
from anywhere with a terminal, resume tomorrow.
|
||||
from anywhere, resume tomorrow.
|
||||
|
||||
This is a separate devcontainer config from the laptop one in
|
||||
`.devcontainer/` — it ships both agent CLIs, `tmux` for session persistence,
|
||||
|
|
@ -11,17 +11,61 @@ Playwright system deps, and Docker-in-Docker (for testcontainers and
|
|||
|
||||
## One-time setup (~5 min)
|
||||
|
||||
1. Add provider keys at [github.com/settings/codespaces](https://github.com/settings/codespaces).
|
||||
Add `ANTHROPIC_API_KEY` for Claude Code. Add `OPENROUTER_API_KEY` for OpenCode.
|
||||
Give both secrets access to `n8n-io/n8n`.
|
||||
(Alternative for Max subscriptions: `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`.)
|
||||
2. Give the GitHub CLI the codespace scope:
|
||||
1. Give the GitHub CLI the codespace scope:
|
||||
|
||||
```bash
|
||||
gh auth refresh -h github.com -s codespace
|
||||
```
|
||||
|
||||
## Daily flow
|
||||
2. Add provider keys at [github.com/settings/codespaces](https://github.com/settings/codespaces)
|
||||
if your client needs them. Give each secret access to `n8n-io/n8n`.
|
||||
- `ANTHROPIC_API_KEY` for Claude Code in the terminal (`pnpm session`).
|
||||
Alternative for Max subscriptions: `CLAUDE_CODE_OAUTH_TOKEN` from
|
||||
`claude setup-token`. The desktop app does not need either secret.
|
||||
- `OPENROUTER_API_KEY` for OpenCode.
|
||||
|
||||
## Choose a client
|
||||
|
||||
All clients run the agent, its tools, and builds in the Codespace. Your laptop
|
||||
shows the conversation only.
|
||||
|
||||
| Client | Command | Use it when |
|
||||
|---|---|---|
|
||||
| Claude Code desktop app | `pnpm session ssh-config` once | You want the full desktop experience. |
|
||||
| Claude Code in VS Code | Open the codespace in VS Code or the browser | You already work in VS Code. |
|
||||
| Claude Code in a terminal | `pnpm session` | You want a plain terminal, or you are on a remote machine. |
|
||||
| OpenCode | `pnpm session:opencode` | You use OpenCode. See [Local OpenCode clients](#local-opencode-clients). |
|
||||
|
||||
### Claude Code desktop app
|
||||
|
||||
1. From a local checkout, add an SSH host for your codespace:
|
||||
|
||||
```bash
|
||||
pnpm session ssh-config
|
||||
```
|
||||
|
||||
The command creates or starts the codespace. It writes the host
|
||||
`n8n-codespace` to `~/.ssh/n8n-codespace.conf` and includes that file from
|
||||
`~/.ssh/config`.
|
||||
2. In the desktop app, add an SSH connection to the host `n8n-codespace`.
|
||||
3. Open the folder `/workspaces/n8n`, or a worktree under `/workspaces`.
|
||||
|
||||
The desktop app installs its own Claude Code on the codespace and signs in
|
||||
with your desktop account. Sessions continue when you close the app. They stop
|
||||
when the codespace stops.
|
||||
|
||||
- **Run `pnpm session ssh-config` again after you recreate the codespace.**
|
||||
- **If the connection times out**, the codespace is probably starting. Run
|
||||
`pnpm session ssh-config`, then connect again.
|
||||
- **Test the connection** with `ssh n8n-codespace`.
|
||||
|
||||
### Claude Code in VS Code
|
||||
|
||||
The dev container installs the Claude Code extension. Open the codespace in
|
||||
VS Code or the browser, then open the Claude Code panel. Sign in from the
|
||||
extension if you did not add `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN`.
|
||||
|
||||
### Claude Code in a terminal
|
||||
|
||||
```bash
|
||||
pnpm session # attach Claude Code (creates everything on first run)
|
||||
|
|
@ -33,6 +77,7 @@ pnpm session:opencode --legacy # use the remote TUI in tmux
|
|||
pnpm session fix-flaky # Claude Code in a separate worktree
|
||||
pnpm session ls # what's running
|
||||
pnpm session tunnel # forward n8n ports (default 5678, 8080); Ctrl-C to stop
|
||||
pnpm session ssh-config # add the n8n-codespace SSH host for the desktop app
|
||||
pnpm session stop # end of day: billing stops, disk survives
|
||||
pnpm session rm # delete the codespace
|
||||
```
|
||||
|
|
@ -332,11 +377,15 @@ session rarely needs a cold `pnpm install` or a full `pnpm build`. Both are slow
|
|||
Claude sessions and the headless OpenCode worker get the `flaky` MCP server automatically: Currents
|
||||
flaky/quarantine data, the `qa_*` BigQuery dataset, Sentry RCA, live Linear,
|
||||
and repo investigation. The worker keeps the token in its environment and puts
|
||||
only an environment reference in the OpenCode config. Claude login registers
|
||||
the same server without writing the token to disk. Forks have no secrets and
|
||||
only an environment reference in the OpenCode config. Forks have no secrets and
|
||||
skip it. Tell the agent to call `get_flaky_context` first — it returns the rules
|
||||
the tools assume.
|
||||
|
||||
`post-start.mjs` registers the server for Claude Code on each container start.
|
||||
Its `headersHelper` reads the token from the secrets file on each connect, so
|
||||
the server works in every client. The token is not copied into the Claude Code
|
||||
MCP configuration.
|
||||
|
||||
## Quality and security skills (Claude plugins)
|
||||
|
||||
Claude sessions can also load the private skills from the
|
||||
|
|
@ -398,7 +447,8 @@ to pull the skills into context.
|
|||
|
||||
## Viewing the dev UI locally
|
||||
|
||||
Two terminal windows:
|
||||
`pnpm session tunnel` works with every client. With the terminal client, use
|
||||
two terminal windows:
|
||||
|
||||
```bash
|
||||
pnpm session # window 1: attach the agent session
|
||||
|
|
@ -448,10 +498,10 @@ After a stop, `pnpm session <name>` restarts the codespace (~30–60 s); run
|
|||
agent injects them into VS Code sessions only; they're delivered
|
||||
base64-encoded to `/workspaces/.codespaces/shared/.env-secrets`. The image
|
||||
sources `/usr/local/lib/codespaces-env.sh` in login shells (profile.d), in
|
||||
interactive shells (bashrc), and in the `pnpm session` prelude. If Claude
|
||||
Code shows `Missing environment variables: FLAKY_MCP_TOKEN`, the shell that
|
||||
started Claude did not source the file. Run
|
||||
`. /usr/local/lib/codespaces-env.sh` and start Claude again.
|
||||
interactive shells (bashrc), and in the `pnpm session` prelude. Desktop app
|
||||
sessions do not source it. `git`, `gh`, `scripts/codespace-env.mjs`, and the
|
||||
`flaky` MCP entry read the file when they run, so they work everywhere. New
|
||||
code that needs a secret must do the same.
|
||||
- **Do not read `CODESPACE_NAME` or `GITHUB_USER` from the process env** — use
|
||||
`scripts/codespace-env.mjs`. Codespaces gives these variables to VS Code
|
||||
sessions only. Other processes read them from `codespaces-env.sh`, and a
|
||||
|
|
@ -460,7 +510,7 @@ After a stop, `pnpm session <name>` restarts the codespace (~30–60 s); run
|
|||
polled correctly as its owner while `dev:up` in the same session saw an empty
|
||||
box name, printed the localhost URL, and did not share the port. The helper
|
||||
reads `/workspaces/.codespaces/shared`, which is always correct.
|
||||
- **You cannot paste images into a remote Claude session.** Image paste reads
|
||||
- **You cannot paste images into a terminal Claude session.** Image paste reads
|
||||
the clipboard of the machine where `claude` runs — the codespace, not your
|
||||
laptop. Drag the file into the VS Code explorer (or
|
||||
`gh codespace cp shot.png remote:/workspaces/n8n/`) and give Claude the
|
||||
|
|
|
|||
|
|
@ -12,14 +12,6 @@
|
|||
'{"hasCompletedOnboarding":true,"theme":"dark","projects":{"/workspaces/n8n":{"hasTrustDialogAccepted":true}}}' \
|
||||
>"$HOME/.claude.json"
|
||||
|
||||
# Register the Flaky MCP server for Claude Code. The config keeps a literal
|
||||
# ${FLAKY_MCP_TOKEN}; Claude Code expands it at connect time, so the token is
|
||||
# not written to disk. Forks have no repo secrets and skip this.
|
||||
if [ -n "$FLAKY_MCP_URL" ] && ! grep -qs '"flaky"' "$HOME/.claude.json" && command -v claude >/dev/null 2>&1; then
|
||||
claude mcp add --scope user --transport http flaky "$FLAKY_MCP_URL" \
|
||||
--header 'Authorization: Bearer ${FLAKY_MCP_TOKEN}' >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Register the credential helper in the user config, because Codespaces
|
||||
# regenerates the managed /etc/gitconfig. When the env token is missing, the
|
||||
# system helper exits 0 without output, and git falls through to ours.
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@
|
|||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
},
|
||||
"forwardPorts": [8080, 5678],
|
||||
// Grant the Codespace token read access to the private skills and harness repositories.
|
||||
// Each user authorizes this access once when they create the Codespace.
|
||||
"customizations": {
|
||||
// Claude Code in VS Code or the browser editor. It runs in the codespace.
|
||||
"vscode": {
|
||||
"extensions": ["anthropic.claude-code"]
|
||||
},
|
||||
// Grant the Codespace token read access to the private skills and harness repositories.
|
||||
// Each user authorizes this access once when they create the Codespace.
|
||||
"codespaces": {
|
||||
"repositories": {
|
||||
"n8n-io/n8n-agent-skills": { "permissions": { "contents": "read" } },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
// Runs on each Codespace start. Installs the skills and harness, then starts the worker.
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { rmSync, writeFileSync } from 'node:fs';
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { installAgentHarness } from '../../scripts/agent-harness.mjs';
|
||||
import { codespaceSecret } from '../../scripts/codespace-env.mjs';
|
||||
import { MARKETPLACE, PLUGINS } from './plugins.mjs';
|
||||
|
||||
const STATUS_FILE = '/tmp/post-start-status.json';
|
||||
|
|
@ -49,6 +50,42 @@ function addMarketplace() {
|
|||
return add('marketplace add (retry)');
|
||||
}
|
||||
|
||||
// Claude Code reads the Flaky token when it connects. An env reference such as
|
||||
// ${FLAKY_MCP_TOKEN} fails in processes that did not load the secrets file, for
|
||||
// example the desktop app's SSH server. The helper reads the file on each connect,
|
||||
// so it also gets a rotated token. The token is not copied into ~/.claude.json.
|
||||
const FLAKY_HEADERS_HELPER =
|
||||
'. /usr/local/lib/codespaces-env.sh; printf %s "{\\"Authorization\\":\\"Bearer $FLAKY_MCP_TOKEN\\"}"';
|
||||
|
||||
function registerFlakyMcp() {
|
||||
// Forks have no repository secrets.
|
||||
const url = codespaceSecret('FLAKY_MCP_URL');
|
||||
if (!url || !codespaceSecret('FLAKY_MCP_TOKEN')) return;
|
||||
|
||||
let current;
|
||||
try {
|
||||
current = JSON.parse(readFileSync(join(homedir(), '.claude.json'), 'utf8')).mcpServers?.flaky;
|
||||
} catch {
|
||||
// No readable config yet: register the server below.
|
||||
}
|
||||
if (current?.url === url && current.headersHelper === FLAKY_HEADERS_HELPER && !current.headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) tryRun('flaky mcp remove', 'claude', ['mcp', 'remove', '--scope', 'user', 'flaky']);
|
||||
const config = { type: 'http', url, headersHelper: FLAKY_HEADERS_HELPER };
|
||||
tryRun('flaky mcp add', 'claude', [
|
||||
'mcp',
|
||||
'add-json',
|
||||
'--scope',
|
||||
'user',
|
||||
'flaky',
|
||||
JSON.stringify(config),
|
||||
]);
|
||||
}
|
||||
|
||||
registerFlakyMcp();
|
||||
|
||||
tryRun('skills repo reachable', 'git', ['ls-remote', `https://github.com/${MARKETPLACE}`, 'HEAD']);
|
||||
|
||||
const installed = [];
|
||||
|
|
|
|||
32
.devcontainer/codespaces/ssh-config.test.mjs
Normal file
32
.devcontainer/codespaces/ssh-config.test.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import { aliasHostConfig, withInclude } from '../../scripts/cloud-session-ssh-config.mjs';
|
||||
|
||||
const GH_CONFIG = `Host cs.ominous-doodle.master
|
||||
User node
|
||||
ProxyCommand gh cs ssh -c ominous-doodle --stdio
|
||||
UserKnownHostsFile=/dev/null
|
||||
`;
|
||||
|
||||
test('replaces the gh host name with the fixed alias', () => {
|
||||
assert.equal(
|
||||
aliasHostConfig(GH_CONFIG),
|
||||
GH_CONFIG.replace('cs.ominous-doodle.master', 'n8n-codespace'),
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects gh output without exactly one proxied host', () => {
|
||||
assert.throws(() => aliasHostConfig(''));
|
||||
assert.throws(() => aliasHostConfig(`${GH_CONFIG}Host other\n`));
|
||||
assert.throws(() => aliasHostConfig('Host cs.x\n\tUser node\n'));
|
||||
});
|
||||
|
||||
test('adds the Include line at the top once', () => {
|
||||
const include = 'Include ~/.ssh/n8n-codespace.conf';
|
||||
assert.equal(withInclude(''), `${include}\n`);
|
||||
|
||||
const updated = withInclude('Host github.com\n\tUser git\n');
|
||||
assert.equal(updated, `${include}\n\nHost github.com\n\tUser git\n`);
|
||||
assert.equal(withInclude(updated), undefined);
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
!docker/images/runners
|
||||
!docker/images/runners/n8n-task-runners.json
|
||||
|
||||
# === benchmark image (packages/@n8n/benchmark/Dockerfile) ===
|
||||
# === benchmark image (packages/quality/efficiency/scale/benchmark/Dockerfile) ===
|
||||
!package.json
|
||||
!pnpm-lock.yaml
|
||||
!pnpm-workspace.yaml
|
||||
|
|
@ -34,8 +34,10 @@
|
|||
!patches/**
|
||||
!scripts
|
||||
!scripts/**
|
||||
!packages/@n8n/benchmark
|
||||
!packages/@n8n/benchmark/**
|
||||
!packages/quality
|
||||
!packages/quality/efficiency
|
||||
!packages/quality/efficiency/scale
|
||||
!packages/quality/efficiency/scale/benchmark
|
||||
!packages/quality/efficiency/scale/benchmark/**
|
||||
!packages/@n8n/typescript-config
|
||||
!packages/@n8n/typescript-config/**
|
||||
|
||||
|
|
|
|||
36
.githooks/check-pnpm.mjs
Normal file
36
.githooks/check-pnpm.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env node
|
||||
// Git hook helper (post-merge, post-rewrite, post-checkout): warn when the
|
||||
// installed pnpm major differs from the one pinned in package.json.
|
||||
// Usage: node .githooks/check-pnpm.mjs [checkout-type]
|
||||
// For post-checkout, pass lefthook's {3}: "0" is a file checkout, which we skip.
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
if (process.argv[2] === '0') process.exit(0);
|
||||
|
||||
const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
||||
const { packageManager = '' } = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
const wanted = /^pnpm@(\d+)/.exec(packageManager)?.[1];
|
||||
|
||||
if (!wanted) process.exit(0);
|
||||
|
||||
// Run from the home dir so pnpm reports the installed version,
|
||||
// not the version pinned by this repo.
|
||||
// `shell` lets Windows resolve pnpm.cmd. The command has no user input.
|
||||
const result = spawnSync('pnpm --version', { cwd: homedir(), shell: true, encoding: 'utf8' });
|
||||
const current = result.status === 0 ? result.stdout.trim().split('.')[0] : '';
|
||||
if (wanted === current) process.exit(0);
|
||||
|
||||
const installCommand =
|
||||
process.platform === 'win32'
|
||||
? 'powershell -c "irm https://get.pnpm.io/install.ps1 | iex"'
|
||||
: 'curl -fsSL https://get.pnpm.io/install.sh | sh -';
|
||||
|
||||
console.log(`
|
||||
⚠ This repo now pins pnpm ${wanted} (you have ${current || 'none'}).
|
||||
pnpm should auto-download it on the next install command.
|
||||
If that fails (some pnpm 10 versions can't install v12), reinstall:
|
||||
${installCommand}
|
||||
`);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/sh
|
||||
# .githooks/check-pnpm — symlink or call from post-merge, post-rewrite, post-checkout
|
||||
|
||||
WANTED=$(sed -n 's/.*"packageManager": *"pnpm@\([0-9]*\).*/\1/p' package.json)
|
||||
CURRENT=$(cd ~ && pnpm --version 2>/dev/null | cut -d. -f1)
|
||||
[ -z "$WANTED" ] || [ "$WANTED" = "$CURRENT" ] && exit 0
|
||||
echo ""
|
||||
echo "⚠ This repo now pins pnpm $WANTED (you have ${CURRENT:-none})."
|
||||
echo " pnpm should auto-download it on the next install command."
|
||||
echo " If that fails (some pnpm 10 versions can't install v12), reinstall:"
|
||||
echo " curl -fsSL https://get.pnpm.io/install.sh | sh -"
|
||||
echo ""
|
||||
exit 0
|
||||
7
.github/CI-TELEMETRY.md
vendored
7
.github/CI-TELEMETRY.md
vendored
|
|
@ -81,11 +81,12 @@ return 'blacksmith';
|
|||
|
||||
| Telemetry | Source | Metrics |
|
||||
|-----------|--------|---------|
|
||||
| Playwright perf/benchmark | `packages/testing/playwright/reporters/metrics-reporter.ts` | Any metric attached via `attachMetric()` |
|
||||
| Accessibility buckets | `packages/testing/playwright/reporters/a11y-reporter.ts` | Per-bucket axe score, violated rules, violating elements |
|
||||
| Playwright perf/benchmark | `packages/quality/testing/playwright/reporters/metrics-reporter.ts` | Any metric attached via `attachMetric()` |
|
||||
| Accessibility buckets | `packages/quality/testing/playwright/reporters/a11y-reporter.ts` | Per-bucket axe score, violated rules, violating elements |
|
||||
| Build stats | `.github/scripts/send-build-stats.mjs` | Per-package build duration, cache hit/miss, run total |
|
||||
| Docker stats | `.github/scripts/send-docker-stats.mjs` | Image size per platform, docker build duration |
|
||||
| Container stack | `packages/testing/containers/telemetry.ts` | E2E stack startup times per service |
|
||||
| Dive image efficiency | `packages/quality/efficiency/images/image-efficiency.spec.ts` | Layer content size, estimated waste, efficiency score, and layer count through the shared Playwright reporter |
|
||||
| Container stack | `packages/quality/environments/containers/telemetry.ts` | E2E stack startup times per service |
|
||||
|
||||
## Secrets
|
||||
|
||||
|
|
|
|||
11
.github/CODEOWNERS
vendored
11
.github/CODEOWNERS
vendored
|
|
@ -1,11 +0,0 @@
|
|||
# Temporary: kept side by side with the top-level OWNERS file while the
|
||||
# "Required Reviews" enforcement is on trial. Do not add entries here;
|
||||
# add them to OWNERS. See "Transition from CODEOWNERS" in WORKFLOWS.md.
|
||||
packages/@n8n/db/src/migrations/ @n8n-io/migrations-review
|
||||
/OWNERS @n8n-io/qa-dx
|
||||
.github/workflows @n8n-io/qa-dx
|
||||
.github/scripts @n8n-io/qa-dx
|
||||
.github/actions @n8n-io/qa-dx
|
||||
.github/poutine-rules @n8n-io/qa-dx
|
||||
docker/get-n8n.sh @n8n-io/qa-dx
|
||||
.code-health-baseline.json @n8n-io/qa-dx
|
||||
110
.github/WORKFLOWS.md
vendored
110
.github/WORKFLOWS.md
vendored
|
|
@ -10,7 +10,6 @@ Complete reference for n8n's `.github/` folder.
|
|||
.github/
|
||||
├── WORKFLOWS.md # This document
|
||||
├── CI-TELEMETRY.md # Telemetry & metrics guide
|
||||
├── CODEOWNERS # Temporary, side by side with OWNERS during the trial
|
||||
├── pull_request_template.md # PR description template
|
||||
├── pull_request_title_conventions.md # Title format rules (Angular)
|
||||
├── actionlint.yml # Workflow linter config
|
||||
|
|
@ -178,7 +177,7 @@ These only run if specific files changed:
|
|||
| `docker/images/n8n-base/Dockerfile` | `build-base-image.yml` | any |
|
||||
| `**/package.json`, `**/turbo.json` | `build-windows.yml` | master |
|
||||
| `packages/@n8n/ai-workflow-builder.ee/evaluations/programmatic/python/**` | `test-evals-python.yml` | any |
|
||||
| `packages/@n8n/benchmark/**` | `build-benchmark-image.yml` | master |
|
||||
| `packages/quality/efficiency/scale/benchmark/**` | `build-benchmark-image.yml` | master |
|
||||
| `packages/cli/src/public-api/**/*.yml`, `packages/cli/src/public-api/**/*.yaml`, `packages/cli/src/public-api/**/*.css`, `packages/cli/src/public-api/v1/openapi-gen/**/*.ts`, `packages/cli/scripts/build.mjs`, `packages/cli/package.json` | `util-publish-api-schema.yml` | master |
|
||||
| `packages/@n8n/instance-ai/src/**`, `packages/@n8n/instance-ai/skills/**`, `packages/@n8n/instance-ai/knowledge-base/**`, `packages/@n8n/instance-ai/evaluations/**`, `packages/cli/src/modules/instance-ai/**`, `packages/core/src/execution-engine/eval-mock-helpers.ts`, `packages/@n8n/agents/src/**` | `ci-instance-ai-evals.yml` | on PR `opened` / `reopened` / `ready_for_review` |
|
||||
| `docker/get-n8n.sh`, `docker/get-n8n-compose.yml`, `docker/test-get-n8n.sh` | `test-get-n8n.yml` | any |
|
||||
|
|
@ -237,14 +236,6 @@ parallelism). See the `--build-via-mcp` section in
|
|||
| PR merged to `release/*` | `release-publish.yml` |
|
||||
| PR closed with `codespace-preview` | `util-codespace-preview.yml` |
|
||||
|
||||
### Manual Triggers (PR Comments)
|
||||
|
||||
| Command | Workflow | Permissions |
|
||||
|--------------------|------------------------------|---------------------|
|
||||
| `/test-workflows` | `test-workflows-callable.yml`| admin/write/maintain|
|
||||
|
||||
**Why:** Re-run tests without pushing commits. Useful for flaky test investigation.
|
||||
|
||||
### Label Triggers
|
||||
|
||||
| Label | Workflow | Effect |
|
||||
|
|
@ -420,7 +411,9 @@ Run-workflow dropdown, which only a user with write access can pick.
|
|||
|-----------------------------|---------------------------------------------------------|
|
||||
| `util-codespace-preview.yml`| Wake, re-serve or delete a PR preview instance by hand |
|
||||
| `util-data-tooling.yml` | SQLite/PostgreSQL export/import validation (manual) |
|
||||
| `test-image-efficiency.yml` | Measure n8n and runners image size and layer waste with Dive |
|
||||
| `util-probe-registry.yml` | Diagnose slow npm metadata fetches (temporary) |
|
||||
| `sec-sync-bundle-branches.yml` | Sync both bundle branches on demand, including before a bundle cut |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -454,10 +447,6 @@ test-sbom-nightly.yml
|
|||
|
||||
test-workflows-nightly.yml (manual dispatch only — nightly schedule disabled, DEVP-544)
|
||||
└──────────────────────────▶ test-workflows-callable.yml
|
||||
|
||||
PR Comment Dispatchers (triggered by /command in PR comments):
|
||||
test-workflows-pr-comment.yml
|
||||
└──────────────────────────▶ test-workflows-callable.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -598,7 +587,7 @@ Push to master
|
|||
| Daily 03:00 | `sec-sync-bundle-branches.yml` | Merge the base into `bundle/*` |
|
||||
| Daily 00:00 | `docker-build-push.yml` | Nightly Docker images |
|
||||
| Daily 00:00 | `test-db.yml` | Database compatibility |
|
||||
| Daily 00:00 | `test-e2e-performance-reusable.yml`| Performance E2E |
|
||||
| Daily 00:00 | `test-e2e-performance-reusable.yml`| Browser performance and idle memory baselines |
|
||||
| Daily 00:00 | `release-storybook.yml` | Storybook deploy |
|
||||
| Daily 00:00 | `release-chromatic.yml` | Visual regression |
|
||||
| Daily 00:00 | `util-check-docs-urls.yml` | Doc link validation |
|
||||
|
|
@ -611,6 +600,7 @@ Push to master
|
|||
| Daily 08:00 | `build-v3-nightly.yml` | Nightly v3 Docker images |
|
||||
| Monday 00:00 | `util-update-node-popularity.yml` | Node usage stats |
|
||||
| Monday 02:00 | `test-e2e-coverage-weekly.yml` | Weekly E2E coverage |
|
||||
| Wednesday 04:00 | `util-update-get-n8n-versions.yml` | get-n8n stable versions |
|
||||
| Saturday 22:00 | `test-evals-ai.yml` | AI workflow evals |
|
||||
| 1st of month 04:00 | `util-refresh-cubic-schema.yml` | Refresh vendored cubic schema |
|
||||
|
||||
|
|
@ -850,9 +840,8 @@ See **[CI-TELEMETRY.md](CI-TELEMETRY.md)** for:
|
|||
|
||||
## OWNERS
|
||||
|
||||
Team ownership lives in the top-level `OWNERS` file (this replaces the
|
||||
GitHub-native `CODEOWNERS` file; see the transition note below). The scripts
|
||||
that consume it live in `.github/scripts/owners/`. Line format:
|
||||
Team ownership lives in the top-level `OWNERS` file. The scripts that consume
|
||||
it live in `.github/scripts/owners/`. Line format:
|
||||
|
||||
```
|
||||
<pattern> <@org/team> [required]
|
||||
|
|
@ -884,8 +873,10 @@ The file drives four workflows:
|
|||
### Required reviews
|
||||
|
||||
An entry with the `required` option makes team approval mandatory: when a PR
|
||||
changes a file whose winning entry carries `required`, a member of each listed
|
||||
team must approve the PR. `ci-owners-required-reviews.yml` evaluates this on
|
||||
changes a non-test file whose winning entry carries `required`, a member of each
|
||||
listed team must approve the PR. Test files match the shared patterns in
|
||||
`test-files.mjs` and do not trigger required reviews.
|
||||
`ci-owners-required-reviews.yml` evaluates this on
|
||||
PR changes and review events, and reports a commit status
|
||||
named **Required Reviews** on the head SHA. A missing approval reports
|
||||
`pending` ("Waiting for approval from: …"), not `failure`, so an unreviewed PR
|
||||
|
|
@ -920,8 +911,7 @@ Reviews** status succeed without required OWNERS team approvals. A team member
|
|||
must apply the `large-scale-change` label. The gate checks the actor from the
|
||||
label event and verifies their current team membership. The label alone is not
|
||||
sufficient. Label changes re-evaluate the status. Removing the label removes
|
||||
the exemption. During the CODEOWNERS trial, GitHub still enforces its separate
|
||||
code-owner review requirement.
|
||||
the exemption.
|
||||
|
||||
Every path that writes the status runs in the base repository context, because
|
||||
a fork-context run has no secrets and a read-only token. PR changes arrive
|
||||
|
|
@ -935,16 +925,6 @@ contribution whose runs still wait for approval gets no review-event
|
|||
re-evaluation until a maintainer approves the runs; `workflow_dispatch` with
|
||||
the PR number is the manual fallback.
|
||||
|
||||
### Transition from CODEOWNERS
|
||||
|
||||
During a trial period, `.github/CODEOWNERS` stays in place next to OWNERS:
|
||||
GitHub's native code-owner enforcement keeps gating merges while the
|
||||
"Required Reviews" status runs side by side. The two must agree — CODEOWNERS
|
||||
holds exactly the `required` entries of OWNERS (plus the OWNERS file itself)
|
||||
and must not gain new entries; new ownership goes into OWNERS. After the
|
||||
trial, delete `.github/CODEOWNERS`, remove "Require review from Code Owners"
|
||||
from the master ruleset, and delete this section (tracked in DEVP-887).
|
||||
|
||||
---
|
||||
|
||||
## Runner Selection
|
||||
|
|
@ -1161,40 +1141,28 @@ open the PR there. That PR **must stay a single-parent squash** — the publish
|
|||
posts to `#alerts-build` when that gate fails on a PR opened *from* `bundle/2.x` or
|
||||
`bundle/1.x` (link only, no PR title, since the branch is embargoed).
|
||||
|
||||
`sec-sync-bundle-branches.yml` keeps those branches current, daily plus whenever a PR is
|
||||
merged into one (and on `workflow_dispatch`). It **merges the base into** the bundle branch
|
||||
via [`scripts/sync-bundle-branch.mjs`](scripts/sync-bundle-branch.mjs) and pushes without
|
||||
forcing. Every push is verified to carry exactly the tree a merge of the two sides would
|
||||
produce (`git merge-tree`); a mismatch, or a conflict marker, fails the run instead of pushing.
|
||||
`sec-sync-bundle-branches.yml` keeps those branches current nightly at 03:00 UTC and on
|
||||
`workflow_dispatch`, including before a bundle cut. It **merges the public base into** the
|
||||
bundle branch via
|
||||
[`scripts/sync-bundle-branch.mjs`](scripts/sync-bundle-branch.mjs) and pushes without forcing.
|
||||
Every push is verified to carry exactly the tree a merge of the two sides would produce (`git
|
||||
merge-tree`); a mismatch, or a conflict marker, fails the run instead of pushing.
|
||||
|
||||
**A bundle branch is only ever built on published history.** Before it creates or merges
|
||||
anything, the sync fetches the same-named branch from `https://github.com/n8n-io/n8n.git`
|
||||
(anonymously — its token is scoped to the private repo) and checks that the private base tip is
|
||||
contained in it. This matters because the `chore: Bundle/*` squash on private `master` is
|
||||
*private-only*: the mirror above discards it in favour of the public cherry-pick of the same
|
||||
changes. A sync in that window would root the branch on a commit that is about to disappear,
|
||||
leaving it carrying two commits for one set of fixes — and every fix PR cut from it inherits the
|
||||
dead one. The window is real: this workflow's daily cron and the mirror's hourly cron both fire
|
||||
at `:00` with nothing ordering them, which is why the check lives in the script rather than in
|
||||
step ordering.
|
||||
|
||||
An unpublished cut is a **skip**, not a failure: the mirror discards that commit every hour
|
||||
regardless, so the run exits green with the reason in the log and the next one proceeds. A
|
||||
missing branch also self-heals, because the mirror re-dispatches this workflow while one is
|
||||
absent. A base ahead of public for **any other reason** fails the run — the mirror is stuck and
|
||||
will not clear it on its own, so fix that first (remove the commit, or dispatch **Security: Sync
|
||||
from Public** with `force`) and re-run. To sync a bundle branch immediately, dispatch the mirror
|
||||
and then this workflow. The blocking commits are listed in the run log; the failure annotation
|
||||
carries only a count, since a subject hints at the fix.
|
||||
**A bundle branch is only ever built on published history.** The sync fetches `master` or `1.x`
|
||||
directly from `https://github.com/n8n-io/n8n.git`. It creates a missing bundle branch at that
|
||||
exact public SHA and merges that public SHA into an existing bundle branch. It never reads the
|
||||
private base branch. This matters because the `chore: Bundle/*` squash on private `master` is
|
||||
private-only until publication. The mirror can replace that commit with the public copy after
|
||||
the bundle branch is deleted. The next bundle branch must not inherit the replaced commit.
|
||||
|
||||
Deleting a bundle branch takes its open PRs with it: GitHub moves each one onto the deleted
|
||||
branch's own base, and re-creating the branch does not move them back. So the sync then calls
|
||||
branch's own base, and re-creating the branch does not move them back. The next nightly or manual
|
||||
sync re-creates the branch and then calls
|
||||
[`sec-sync-retarget-prs.yml`](workflows/sec-sync-retarget-prs.yml), which moves every open PR
|
||||
on `master` onto `bundle/2.x` and every one on `1.x` onto `bundle/1.x`. It skips a bundle
|
||||
branch that does not exist, and skips PRs whose *head* is `bundle/*` — those are the
|
||||
`chore: Bundle/*` cut PRs, which target the base on purpose. It is also dispatchable on its
|
||||
own. `sec-sync-public-to-private.yml` only *dispatches* the bundle sync when it finds a branch
|
||||
missing; the retarget runs inside that dispatched run, once the branch exists.
|
||||
own. The retarget runs inside the bundle sync after a missing branch is recreated.
|
||||
|
||||
**`bundle/*` is append-only — never rebase it, never force-push it.** These branches receive
|
||||
PRs, and rewriting a branch that receives PRs orphans the copies of its commits that the open
|
||||
|
|
@ -1207,21 +1175,19 @@ itself. To refresh a fix branch, use GitHub's **Update branch** button or
|
|||
sibling PR's merge base untouched, which is why only a rewrite breaks this.
|
||||
|
||||
The costs of merging are deliberate and paid for: a merge commit per run, and fixes that have
|
||||
already been published staying in the branch's log (the old rebase dropped them as empty
|
||||
commits). Neither reaches anything downstream, because a bundle publishes as one squashed
|
||||
commit taken from the tree rather than the history — the `chore: Bundle/*` PR's **diff** stays
|
||||
already been published staying in the branch's log. Neither reaches anything downstream,
|
||||
because a bundle publishes as one squashed commit taken from the tree rather than the history —
|
||||
the `chore: Bundle/*` PR's **diff** stays
|
||||
exactly the pending fixes even when its commit list does not. For a list of what a bundle
|
||||
actually carries, read the fix PRs merged into the branch since the last cut, not
|
||||
`base..bundle`. A lower cadence than the base's is fine too: a base push never re-triggered
|
||||
CI on the fix PRs, so syncing more often bought them nothing.
|
||||
actually carries, read the fix PRs merged into the branch since the last cut, not `base..bundle`.
|
||||
|
||||
There is **one job per bundle branch**. A conflict is detected from the merge tree before the
|
||||
working tree is touched, so the branch is left exactly as it was, that job **fails** (no
|
||||
green runs hiding a stalled branch) and `#alerts-security` gets a run link — while the other
|
||||
branch still syncs. Recovery is deliberate: merge the base into the branch locally, resolve,
|
||||
push, then re-run the workflow — and that resolution then lives in the merge commit instead of
|
||||
being re-litigated on every later run. The sync never resolves a conflict itself, unlike
|
||||
`util-sync-master-to-3x.yml`.
|
||||
branch still syncs. Recovery is deliberate: merge the public base into the branch locally,
|
||||
resolve, push, then re-run the workflow — and that resolution then lives in the merge commit
|
||||
instead of being re-litigated on every later run. The sync never resolves a conflict itself,
|
||||
unlike `util-sync-master-to-3x.yml`.
|
||||
|
||||
See **[`../AGENTS.md`](../AGENTS.md)** ("Security Fix Hygiene") for the naming rules that
|
||||
keep the vulnerability out of public branch names, commits, and test descriptions.
|
||||
|
|
@ -1291,12 +1257,6 @@ Adding a new channel requires inviting the bot first; the first run otherwise fa
|
|||
|
||||
## Future Vision
|
||||
|
||||
### Redundancy Review
|
||||
|
||||
Comment trigger (`/test-workflows`) is a workaround.
|
||||
|
||||
Long-term: Main CI should be reliable enough to not need these.
|
||||
|
||||
### Workflow Testability
|
||||
|
||||
- Tools like `act` for local testing
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ describe('matchGlob', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('backend module pattern matches packaged module files', () => {
|
||||
assert.ok(
|
||||
matchGlob(
|
||||
'packages/modules/insights/backend/src/insights.service.ts',
|
||||
'packages/modules/*/backend/**',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('* matches single-level only', () => {
|
||||
assert.ok(matchGlob('README.md', '*.md'));
|
||||
assert.ok(!matchGlob('docs/README.md', '*.md'));
|
||||
|
|
@ -242,7 +251,7 @@ describe('runtime filter', () => {
|
|||
'!**/CHANGELOG.md',
|
||||
'!**/*.test.ts',
|
||||
'!**/*.spec.ts',
|
||||
'!packages/testing/playwright/**',
|
||||
'!packages/quality/testing/playwright/**',
|
||||
'!packages/frontend/@n8n/storybook/**',
|
||||
'!scripts/agent-setup.mjs',
|
||||
'!scripts/backend-module/**',
|
||||
|
|
@ -265,7 +274,7 @@ describe('runtime filter', () => {
|
|||
|
||||
it('does not trigger on playwright tests', () => {
|
||||
assert.equal(
|
||||
evaluateFilter(['packages/testing/playwright/tests/x.spec.ts'], runtimePatterns),
|
||||
evaluateFilter(['packages/quality/testing/playwright/tests/x.spec.ts'], runtimePatterns),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
2
.github/docker-compose.yml
vendored
2
.github/docker-compose.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
postgres:
|
||||
image: ${TEST_IMAGE_POSTGRES:?set it from packages/testing/containers/postgres-versions.json}
|
||||
image: ${TEST_IMAGE_POSTGRES:?set it from packages/quality/environments/containers/postgres-versions.json}
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
|
|
|
|||
11
.github/pull_request_template.md
vendored
11
.github/pull_request_template.md
vendored
|
|
@ -1,8 +1,15 @@
|
|||
## Summary
|
||||
|
||||
<!--
|
||||
Describe what the PR does.
|
||||
Photos and videos are recommended.
|
||||
Start with 1 to 3 sentences of background for a new reader. Then describe what the PR does, in prose, not as a list of edits.
|
||||
Add photos or videos for UI changes.
|
||||
-->
|
||||
|
||||
## Why
|
||||
|
||||
<!--
|
||||
State the problem or the constraint that makes the current code unfit. Do not repeat the Summary.
|
||||
Add a rejected alternative only when a reviewer would ask about it, in one or two sentences.
|
||||
-->
|
||||
|
||||
## How to test
|
||||
|
|
|
|||
2
.github/scripts/db-test-matrix.mjs
vendored
2
.github/scripts/db-test-matrix.mjs
vendored
|
|
@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
export const POSTGRES_VERSIONS_PATH = 'packages/testing/containers/postgres-versions.json';
|
||||
export const POSTGRES_VERSIONS_PATH = 'packages/quality/environments/containers/postgres-versions.json';
|
||||
|
||||
/** The matrix wall is bounded by its slowest leg, so a larger runner buys nothing. */
|
||||
const RUNNER = 'blacksmith-4vcpu-ubuntu-2204';
|
||||
|
|
|
|||
10
.github/scripts/glob.test.mjs
vendored
10
.github/scripts/glob.test.mjs
vendored
|
|
@ -30,9 +30,9 @@ describe('matchesGlob', () => {
|
|||
});
|
||||
|
||||
it('matches a prefix pattern', () => {
|
||||
assert.equal(matchesGlob('packages/testing/playwright/a.ts', 'packages/testing/**'), true);
|
||||
assert.equal(matchesGlob('packages/testing', 'packages/testing/**'), false);
|
||||
assert.equal(matchesGlob('packages/cli/a.ts', 'packages/testing/**'), false);
|
||||
assert.equal(matchesGlob('packages/quality/testing/playwright/a.ts', 'packages/quality/**'), true);
|
||||
assert.equal(matchesGlob('packages/quality', 'packages/quality/**'), false);
|
||||
assert.equal(matchesGlob('packages/cli/a.ts', 'packages/quality/**'), false);
|
||||
});
|
||||
|
||||
it('matches a literal path only exactly', () => {
|
||||
|
|
@ -55,8 +55,8 @@ describe('matchesGlob', () => {
|
|||
'packages/cli/__snapshots__/a.ts.snap',
|
||||
'packages/nodes-base/nodes/Foo/test/fixtures/a.json',
|
||||
'packages/frontend/editor-ui/src/__mocks__/a.ts',
|
||||
'packages/testing/playwright/a.ts',
|
||||
'packages/testing',
|
||||
'packages/quality/testing/playwright/a.ts',
|
||||
'packages/quality',
|
||||
'.github/scripts/glob.test.mjs',
|
||||
'.github/workflows/ci.yml',
|
||||
'a/.hidden/b.spec.js',
|
||||
|
|
|
|||
6
.github/scripts/owners/owners.mjs
vendored
6
.github/scripts/owners/owners.mjs
vendored
|
|
@ -1,6 +1,8 @@
|
|||
import { readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { isTestFile } from "../test-files.mjs";
|
||||
|
||||
/**
|
||||
* @typedef OwnersEntry
|
||||
* @property { string } pattern
|
||||
|
|
@ -236,7 +238,7 @@ export function assignOwnership(files, entries) {
|
|||
|
||||
/**
|
||||
* Determine which teams must approve the changeset: a team is required when
|
||||
* its `required` entry wins (last-match) for a changed file.
|
||||
* its `required` entry wins (last-match) for a changed non-test file.
|
||||
*
|
||||
* @param { Set<string> } files
|
||||
* @param { OwnersEntry[] } entries
|
||||
|
|
@ -247,6 +249,8 @@ export function resolveRequiredTeams(files, entries) {
|
|||
const teamToFiles = new Map();
|
||||
|
||||
for (const file of [...files].sort()) {
|
||||
if (isTestFile(file)) continue;
|
||||
|
||||
const entry = findOwningEntry(file, entries);
|
||||
if (!entry?.required) continue;
|
||||
|
||||
|
|
|
|||
16
.github/scripts/owners/owners.test.mjs
vendored
16
.github/scripts/owners/owners.test.mjs
vendored
|
|
@ -293,6 +293,22 @@ describe('resolveRequiredTeams', () => {
|
|||
assert.equal(result.size, 0);
|
||||
});
|
||||
|
||||
it('ignores test files whose winning entry is required', () => {
|
||||
const result = resolveRequiredTeams(
|
||||
new Set([
|
||||
'.github/scripts/check.test.mjs',
|
||||
'.github/workflows/__tests__/check.ts',
|
||||
'.github/workflows/ci.yml',
|
||||
]),
|
||||
[
|
||||
entry({ pattern: '.github/scripts/', team: '@n8n-io/qa-dx', required: true }),
|
||||
entry({ pattern: '.github/workflows/', team: '@n8n-io/qa-dx', required: true }),
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepEqual(result.get('@n8n-io/qa-dx'), ['.github/workflows/ci.yml']);
|
||||
});
|
||||
|
||||
it('a later non-required entry overrides an earlier required one', () => {
|
||||
const result = resolveRequiredTeams(new Set(['db/migrations/1-init.ts']), [
|
||||
entry({ pattern: 'db/', team: '@n8n-io/migrations-review', required: true }),
|
||||
|
|
|
|||
2
.github/scripts/owners/required-reviews.mjs
vendored
2
.github/scripts/owners/required-reviews.mjs
vendored
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Required-review enforcement for OWNERS entries marked `required`.
|
||||
*
|
||||
* Computes the teams whose approval the changeset needs (per OWNERS)
|
||||
* Computes the teams whose approval the non-test changes need (per OWNERS)
|
||||
* and reports the verdict as a "Required Reviews" commit status on the PR
|
||||
* head SHA. A ruleset that lists this status as a required check blocks the
|
||||
* merge until a member of each required team has approved the PR. Merge-queue
|
||||
|
|
|
|||
|
|
@ -31,6 +31,38 @@ import {
|
|||
*/
|
||||
|
||||
const BOT_MARKER = '<!-- pr-recommendations -->';
|
||||
const REQUIRED_FILES_DISPLAY_LIMIT = 5;
|
||||
|
||||
/**
|
||||
* @param { string } value
|
||||
* @returns { string }
|
||||
*/
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('|', '|');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { string[] } files
|
||||
* @returns { string }
|
||||
*/
|
||||
function formatRequiredFiles(files) {
|
||||
const displayedFiles = files
|
||||
.slice(0, REQUIRED_FILES_DISPLAY_LIMIT)
|
||||
.map((file) => `<code>${escapeHtml(file)}</code>`);
|
||||
const omittedFileCount = files.length - displayedFiles.length;
|
||||
|
||||
if (omittedFileCount > 0) {
|
||||
displayedFiles.push(
|
||||
`<em>${omittedFileCount} more file${omittedFileCount === 1 ? '' : 's'}</em>`,
|
||||
);
|
||||
}
|
||||
|
||||
return displayedFiles.join('<br>');
|
||||
}
|
||||
|
||||
function createEmptyLineStats() {
|
||||
return {
|
||||
|
|
@ -210,9 +242,11 @@ export function buildRequiredReviewsSection(requiredTeamFiles) {
|
|||
'',
|
||||
'Some changed files have a `required` owner in `OWNERS`. A member of each of these teams must approve this PR before it can merge:',
|
||||
'',
|
||||
'| Team | Files |',
|
||||
'| --- | ---: |',
|
||||
...[...requiredTeamFiles].map(([team, files]) => `| ${team} | ${files.length} |`),
|
||||
'| Team | Files owned | Owned files |',
|
||||
'| --- | ---: | --- |',
|
||||
...[...requiredTeamFiles].map(
|
||||
([team, files]) => `| ${team} | ${files.length} | ${formatRequiredFiles(files)} |`,
|
||||
),
|
||||
'',
|
||||
`Request a review from the team${plural ? 's' : ''} — GitHub assigns reviewers according to the team's review settings. The \`Auto-assign reviewers\` label does this for all owning teams.`,
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ describe('computeLineStats', () => {
|
|||
['*.snap extension', 'src/foo.snap'],
|
||||
['fixtures/ directory', 'src/fixtures/data.json'],
|
||||
['__mocks__/ directory', 'src/__mocks__/axios.ts'],
|
||||
['packages/testing/**', 'packages/testing/playwright/spec.ts'],
|
||||
['packages/quality/**', 'packages/quality/testing/playwright/spec.ts'],
|
||||
['dot-directory *.test.mjs', '.github/scripts/owners/owners.test.mjs'],
|
||||
]) {
|
||||
it(`classifies ${label} as testFiles`, () => {
|
||||
|
|
@ -322,7 +322,7 @@ describe('buildRequiredReviewsSection', () => {
|
|||
assert.equal(buildRequiredReviewsSection(new Map()), null);
|
||||
});
|
||||
|
||||
it('lists each required team with its file count', () => {
|
||||
it('lists each required team with its file count and owned files', () => {
|
||||
const section = buildRequiredReviewsSection(
|
||||
new Map([
|
||||
['@n8n-io/qa-dx', ['a.yml', 'b.yml']],
|
||||
|
|
@ -331,8 +331,30 @@ describe('buildRequiredReviewsSection', () => {
|
|||
);
|
||||
|
||||
assert.match(section, /### Required reviews/);
|
||||
assert.match(section, /\| @n8n-io\/qa-dx \| 2 \|/);
|
||||
assert.match(section, /\| @n8n-io\/migrations-review \| 1 \|/);
|
||||
assert.match(
|
||||
section,
|
||||
/\| @n8n-io\/qa-dx \| 2 \| <code>a\.yml<\/code><br><code>b\.yml<\/code> \|/,
|
||||
);
|
||||
assert.match(section, /\| @n8n-io\/migrations-review \| 1 \| <code>m\.ts<\/code> \|/);
|
||||
});
|
||||
|
||||
it('shows no more than five files for each team', () => {
|
||||
const section = buildRequiredReviewsSection(
|
||||
new Map([['@n8n-io/qa-dx', ['a.yml', 'b.yml', 'c.yml', 'd.yml', 'e.yml', 'f.yml', 'g.yml']]]),
|
||||
);
|
||||
|
||||
assert.match(section, /<code>a\.yml<\/code>/);
|
||||
assert.match(section, /<code>e\.yml<\/code>/);
|
||||
assert.doesNotMatch(section, /<code>f\.yml<\/code>/);
|
||||
assert.match(section, /<em>2 more files<\/em>/);
|
||||
});
|
||||
|
||||
it('escapes file paths before adding them to the table', () => {
|
||||
const section = buildRequiredReviewsSection(
|
||||
new Map([['@n8n-io/qa-dx', ['packages/a|b/<script>.ts']]]),
|
||||
);
|
||||
|
||||
assert.match(section, /<code>packages\/a|b\/<script>\.ts<\/code>/);
|
||||
});
|
||||
|
||||
it('prompts to request review from the team, plural when several teams are required', () => {
|
||||
|
|
@ -447,7 +469,7 @@ describe('run', () => {
|
|||
|
||||
const body = postOrUpdateComment.mock.calls[0].arguments[1];
|
||||
assert.match(body, /### Required reviews/);
|
||||
assert.match(body, /\| @n8n-io\/qa-dx \| 1 \|/);
|
||||
assert.match(body, /\| @n8n-io\/qa-dx \| 1 \| <code>\.github\/workflows\/ci\.yml<\/code> \|/);
|
||||
assert.match(body, /Request a review from the team —/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
8
.github/scripts/pnpm-lock.yaml
vendored
8
.github/scripts/pnpm-lock.yaml
vendored
|
|
@ -747,8 +747,8 @@ packages:
|
|||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-uri@3.1.5:
|
||||
resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==}
|
||||
fast-uri@3.1.8:
|
||||
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
|
||||
|
||||
fd-package-json@2.0.0:
|
||||
resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==}
|
||||
|
|
@ -1694,7 +1694,7 @@ snapshots:
|
|||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.5
|
||||
fast-uri: 3.1.8
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
|
|
@ -1994,7 +1994,7 @@ snapshots:
|
|||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-uri@3.1.5: {}
|
||||
fast-uri@3.1.8: {}
|
||||
|
||||
fd-package-json@2.0.0:
|
||||
dependencies:
|
||||
|
|
|
|||
27
.github/scripts/quality/check-pr-size.mjs
vendored
27
.github/scripts/quality/check-pr-size.mjs
vendored
|
|
@ -14,32 +14,13 @@
|
|||
|
||||
import { initGithub, getEventFromGithubEventPath } from '../github-helpers.mjs';
|
||||
import { matchesGlob } from '../glob.mjs';
|
||||
import { isTestFile, TEST_PATTERNS } from '../test-files.mjs';
|
||||
|
||||
export { isTestFile, TEST_PATTERNS };
|
||||
|
||||
export const SIZE_LIMIT = 1000;
|
||||
export const OVERRIDE_COMMAND = '/size-limit-override';
|
||||
|
||||
export const TEST_PATTERNS = [
|
||||
// Test files (by extension)
|
||||
'**/*.test.ts',
|
||||
'**/*.test.js',
|
||||
'**/*.test.mjs',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.mjs',
|
||||
// Test directories
|
||||
'**/test/**',
|
||||
'**/tests/**',
|
||||
'**/__tests__/**',
|
||||
// Snapshots
|
||||
'**/__snapshots__/**',
|
||||
'**/*.snap',
|
||||
// Fixtures and mocks
|
||||
'**/fixtures/**',
|
||||
'**/__mocks__/**',
|
||||
// Dedicated testing package
|
||||
'packages/testing/**',
|
||||
];
|
||||
|
||||
export const MISC_PATTERNS = [
|
||||
// Lock file (can produce massive diffs on dependency changes)
|
||||
'pnpm-lock.yaml',
|
||||
|
|
@ -56,7 +37,7 @@ export const EXCLUDE_PATTERNS = [...TEST_PATTERNS, ...MISC_PATTERNS];
|
|||
* @returns { 'testFiles' | 'misc' | 'sourceCode' }
|
||||
*/
|
||||
export function categorizeFile(filename) {
|
||||
if (TEST_PATTERNS.some((pattern) => matchesGlob(filename, pattern))) return 'testFiles';
|
||||
if (isTestFile(filename)) return 'testFiles';
|
||||
if (MISC_PATTERNS.some((pattern) => matchesGlob(filename, pattern))) return 'misc';
|
||||
return 'sourceCode';
|
||||
}
|
||||
|
|
|
|||
15
.github/scripts/quality/check-pr-size.test.mjs
vendored
15
.github/scripts/quality/check-pr-size.test.mjs
vendored
|
|
@ -203,14 +203,21 @@ describe('countFilteredAdditions', () => {
|
|||
assert.equal(countFilteredAdditions(files, EXCLUDE_PATTERNS), 50);
|
||||
});
|
||||
|
||||
it('applies EXCLUDE_PATTERNS to packages/testing and pnpm-lock.yaml', () => {
|
||||
it('excludes former testing workspaces but counts benchmark CLI source', () => {
|
||||
const files = [
|
||||
{ filename: 'packages/cli/src/service.ts', additions: 50 },
|
||||
{ filename: 'packages/testing/playwright/tests/workflow.spec.ts', additions: 100 },
|
||||
{ filename: 'packages/testing/playwright/pages/CanvasPage.ts', additions: 100 },
|
||||
{ filename: 'packages/quality/testing/playwright/tests/workflow.spec.ts', additions: 100 },
|
||||
{ filename: 'packages/quality/testing/playwright/pages/CanvasPage.ts', additions: 100 },
|
||||
{ filename: 'packages/quality/policy/code-health/src/cli.ts', additions: 100 },
|
||||
{ filename: 'packages/quality/environments/containers/service-stack.ts', additions: 100 },
|
||||
{
|
||||
filename: 'packages/quality/efficiency/microbenchmarks/benchmarks/bench-options.ts',
|
||||
additions: 100,
|
||||
},
|
||||
{ filename: 'packages/quality/efficiency/scale/benchmark/src/commands/list.ts', additions: 100 },
|
||||
{ filename: 'pnpm-lock.yaml', additions: 500 },
|
||||
];
|
||||
assert.equal(countFilteredAdditions(files, EXCLUDE_PATTERNS), 50);
|
||||
assert.equal(countFilteredAdditions(files, EXCLUDE_PATTERNS), 150);
|
||||
});
|
||||
|
||||
it('applies EXCLUDE_PATTERNS to markdown files', () => {
|
||||
|
|
|
|||
77
.github/scripts/quality/cubic-config.schema.json
vendored
77
.github/scripts/quality/cubic-config.schema.json
vendored
|
|
@ -5,7 +5,9 @@
|
|||
"description": "JSON schema for cubic.yaml (and the legacy .cubic.yaml) so editors can validate and autocomplete repository settings.",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": ["version"],
|
||||
"required": [
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"const": 1,
|
||||
|
|
@ -16,13 +18,26 @@
|
|||
"description": "AI review behavior and optional YAML-defined custom agents.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"senior_reviewers": {
|
||||
"type": "array",
|
||||
"maxItems": 5,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$"
|
||||
},
|
||||
"description": "GitHub usernames to learn review patterns from. Overrides dashboard selections; an empty list disables senior reviewer learning."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Master toggle for AI reviews on this repository."
|
||||
},
|
||||
"sensitivity": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
],
|
||||
"description": "Controls how strictly Cubic flags issues."
|
||||
},
|
||||
"incremental_commits": {
|
||||
|
|
@ -63,12 +78,21 @@
|
|||
},
|
||||
"auto_approve_behavior": {
|
||||
"type": "string",
|
||||
"enum": ["disabled", "shadow", "live"],
|
||||
"enum": [
|
||||
"disabled",
|
||||
"shadow",
|
||||
"live"
|
||||
],
|
||||
"description": "Controls whether auto-approval is disabled, simulated in shadow mode, or submitted live to GitHub."
|
||||
},
|
||||
"auto_approve": {
|
||||
"type": "string",
|
||||
"enum": ["disabled", "always", "low_risk_only", "custom"],
|
||||
"enum": [
|
||||
"disabled",
|
||||
"always",
|
||||
"low_risk_only",
|
||||
"custom"
|
||||
],
|
||||
"description": "Auto-approve mode: disabled, always approve clean runs, use low-risk heuristics, or use a custom prompt."
|
||||
},
|
||||
"auto_approve_custom_prompt": {
|
||||
|
|
@ -80,12 +104,20 @@
|
|||
},
|
||||
"ultrareview": {
|
||||
"type": "string",
|
||||
"enum": ["disabled", "manual", "automatic"],
|
||||
"enum": [
|
||||
"disabled",
|
||||
"manual",
|
||||
"automatic"
|
||||
],
|
||||
"description": "Master switch for ultrareviews. disabled blocks all ultrareviews (manual and automatic), manual allows only manual triggers (the default), automatic also enables the automatic triggers."
|
||||
},
|
||||
"auto_ultrareview": {
|
||||
"type": "string",
|
||||
"enum": ["disabled", "high_risk_only", "custom"],
|
||||
"enum": [
|
||||
"disabled",
|
||||
"high_risk_only",
|
||||
"custom"
|
||||
],
|
||||
"description": "Auto-trigger ultrareview mode: disabled, let cubic decide per PR using built-in high-risk criteria, or use a custom prompt."
|
||||
},
|
||||
"auto_ultrareview_custom_prompt": {
|
||||
|
|
@ -159,7 +191,10 @@
|
|||
},
|
||||
"coding_agent_provider": {
|
||||
"type": "string",
|
||||
"enum": ["cubic", "cursor_cloud_agent"],
|
||||
"enum": [
|
||||
"cubic",
|
||||
"cursor_cloud_agent"
|
||||
],
|
||||
"description": "Which coding agent applies automatic fixes. 'cubic' uses cubic's built-in agent; 'cursor_cloud_agent' dispatches a Cursor cloud agent (requires Cursor configuration)."
|
||||
}
|
||||
}
|
||||
|
|
@ -222,8 +257,20 @@
|
|||
},
|
||||
"exclude_external_contributors": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "If true, public-repository PRs from external contributors are reviewed but never auto-approved."
|
||||
"default": true,
|
||||
"description": "If true, public-repository PRs from external contributors, or from authors cubic cannot verify, are reviewed but never auto-approved."
|
||||
},
|
||||
"allow_open_issues": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"p3",
|
||||
"p2_and_p3",
|
||||
"p1_to_p3",
|
||||
"all"
|
||||
],
|
||||
"default": "none",
|
||||
"description": "Open cubic findings that don't block auto-approval, by priority. none blocks on any open finding; p3, p2_and_p3, and p1_to_p3 allow that range; all allows every open finding. A finding without a recorded priority blocks unless this is all. Every other auto-approval rule still applies."
|
||||
},
|
||||
"exclude_authors": {
|
||||
"$ref": "#/$defs/globList",
|
||||
|
|
@ -271,13 +318,19 @@
|
|||
"type": "object",
|
||||
"description": "Definition of a YAML-managed custom agent.",
|
||||
"additionalProperties": false,
|
||||
"required": ["name"],
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"anyOf": [
|
||||
{
|
||||
"required": ["description"]
|
||||
"required": [
|
||||
"description"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": ["file_paths"]
|
||||
"required": [
|
||||
"file_paths"
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
|
|
|
|||
68
.github/scripts/split-jscpd-report.mjs
vendored
Normal file
68
.github/scripts/split-jscpd-report.mjs
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env node
|
||||
// Splits a jscpd JSON report's `duplicates` array into N bucket files so a
|
||||
// caller can download and sample one small file instead of the whole report
|
||||
// (which can run into tens of MB on a large monorepo). Round-robin, not
|
||||
// contiguous chunks, so a run of similarly-sized clusters in scan order
|
||||
// doesn't pile up in one bucket.
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function splitDuplicates(duplicates, bucketCount) {
|
||||
const buckets = Array.from({ length: bucketCount }, () => []);
|
||||
duplicates.forEach((dup, i) => buckets[i % bucketCount].push(dup));
|
||||
return buckets;
|
||||
}
|
||||
|
||||
export function writeBuckets(buckets, outputDir, deps = {}) {
|
||||
const mkdirImpl = deps.mkdir ?? mkdirSync;
|
||||
const writeImpl = deps.write ?? writeFileSync;
|
||||
mkdirImpl(outputDir, { recursive: true });
|
||||
return buckets.map((duplicates, i) => {
|
||||
const path = join(outputDir, `bucket-${i}.json`);
|
||||
writeImpl(path, JSON.stringify({ duplicates }));
|
||||
return path;
|
||||
});
|
||||
}
|
||||
|
||||
export function parseBucketCount(raw) {
|
||||
const count = Number(raw ?? 20);
|
||||
if (!Number.isInteger(count) || count <= 0) {
|
||||
throw new Error(`BUCKET_COUNT must be a positive integer, got '${raw}'`);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function loadDuplicates(reportPath, deps = {}) {
|
||||
const readImpl = deps.read ?? readFileSync;
|
||||
const report = JSON.parse(readImpl(reportPath, 'utf8'));
|
||||
if (!Array.isArray(report.duplicates)) {
|
||||
throw new Error('report.duplicates must be an array');
|
||||
}
|
||||
return report.duplicates;
|
||||
}
|
||||
|
||||
// Exits non-zero on any failure. The workflow step already runs with
|
||||
// continue-on-error, so this can't fail the job — it only stops a broken
|
||||
// split (zero buckets written) from reporting success.
|
||||
function main() {
|
||||
const reportPath = process.env.REPORT_PATH;
|
||||
const outputDir = process.env.OUTPUT_DIR;
|
||||
if (!reportPath || !outputDir) {
|
||||
process.stderr.write('split-jscpd-report: REPORT_PATH and OUTPUT_DIR are required\n');
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
const bucketCount = parseBucketCount(process.env.BUCKET_COUNT);
|
||||
const duplicates = loadDuplicates(reportPath);
|
||||
const buckets = splitDuplicates(duplicates, bucketCount);
|
||||
writeBuckets(buckets, outputDir);
|
||||
} catch (err) {
|
||||
process.stderr.write(`split-jscpd-report: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
111
.github/scripts/split-jscpd-report.test.mjs
vendored
Normal file
111
.github/scripts/split-jscpd-report.test.mjs
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Run these tests by running
|
||||
*
|
||||
* node --test .github/scripts/split-jscpd-report.test.mjs
|
||||
* */
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { splitDuplicates, writeBuckets, parseBucketCount, loadDuplicates } from './split-jscpd-report.mjs';
|
||||
|
||||
describe('splitDuplicates', () => {
|
||||
it('distributes entries round-robin across buckets', () => {
|
||||
const duplicates = [1, 2, 3, 4, 5, 6];
|
||||
const buckets = splitDuplicates(duplicates, 3);
|
||||
assert.deepEqual(buckets, [
|
||||
[1, 4],
|
||||
[2, 5],
|
||||
[3, 6],
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves later buckets empty when there are fewer entries than buckets', () => {
|
||||
const buckets = splitDuplicates([1, 2], 5);
|
||||
assert.equal(buckets.length, 5);
|
||||
assert.deepEqual(buckets[0], [1]);
|
||||
assert.deepEqual(buckets[1], [2]);
|
||||
assert.deepEqual(buckets[2], []);
|
||||
});
|
||||
|
||||
it('returns bucketCount empty arrays for an empty report', () => {
|
||||
const buckets = splitDuplicates([], 4);
|
||||
assert.deepEqual(buckets, [[], [], [], []]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeBuckets', () => {
|
||||
it('writes one JSON file per bucket, named by index', () => {
|
||||
const written = [];
|
||||
const paths = writeBuckets([[{ a: 1 }], [{ b: 2 }]], '/tmp/buckets', {
|
||||
mkdir: () => {},
|
||||
write: (path, contents) => written.push({ path, contents }),
|
||||
});
|
||||
assert.deepEqual(paths, ['/tmp/buckets/bucket-0.json', '/tmp/buckets/bucket-1.json']);
|
||||
assert.deepEqual(JSON.parse(written[0].contents), { duplicates: [{ a: 1 }] });
|
||||
assert.deepEqual(JSON.parse(written[1].contents), { duplicates: [{ b: 2 }] });
|
||||
});
|
||||
|
||||
it('creates the output directory before writing', () => {
|
||||
let mkdirCalledWith = null;
|
||||
writeBuckets([[]], '/tmp/buckets', {
|
||||
mkdir: (dir, opts) => {
|
||||
mkdirCalledWith = { dir, opts };
|
||||
},
|
||||
write: () => {},
|
||||
});
|
||||
assert.deepEqual(mkdirCalledWith, { dir: '/tmp/buckets', opts: { recursive: true } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBucketCount', () => {
|
||||
it('parses a numeric string', () => {
|
||||
assert.equal(parseBucketCount('20'), 20);
|
||||
});
|
||||
|
||||
it('defaults to 20 when unset', () => {
|
||||
assert.equal(parseBucketCount(undefined), 20);
|
||||
});
|
||||
|
||||
it('rejects zero', () => {
|
||||
assert.throws(() => parseBucketCount('0'), /positive integer/);
|
||||
});
|
||||
|
||||
it('rejects a negative count', () => {
|
||||
assert.throws(() => parseBucketCount('-1'), /positive integer/);
|
||||
});
|
||||
|
||||
it('rejects a non-integer', () => {
|
||||
assert.throws(() => parseBucketCount('1.5'), /positive integer/);
|
||||
});
|
||||
|
||||
it('rejects a non-numeric value', () => {
|
||||
assert.throws(() => parseBucketCount('abc'), /positive integer/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadDuplicates', () => {
|
||||
it('returns the duplicates array from a valid report', () => {
|
||||
const duplicates = loadDuplicates('/tmp/report.json', {
|
||||
read: () => JSON.stringify({ duplicates: [{ a: 1 }] }),
|
||||
});
|
||||
assert.deepEqual(duplicates, [{ a: 1 }]);
|
||||
});
|
||||
|
||||
it('rejects a report missing the duplicates array', () => {
|
||||
assert.throws(
|
||||
() => loadDuplicates('/tmp/report.json', { read: () => JSON.stringify({}) }),
|
||||
/duplicates must be an array/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a report whose duplicates field is not an array', () => {
|
||||
assert.throws(
|
||||
() => loadDuplicates('/tmp/report.json', { read: () => JSON.stringify({ duplicates: 'x' }) }),
|
||||
/duplicates must be an array/,
|
||||
);
|
||||
});
|
||||
|
||||
it('propagates a JSON parse failure', () => {
|
||||
assert.throws(() => loadDuplicates('/tmp/report.json', { read: () => 'not json' }), SyntaxError);
|
||||
});
|
||||
});
|
||||
88
.github/scripts/sync-bundle-branch.mjs
vendored
88
.github/scripts/sync-bundle-branch.mjs
vendored
|
|
@ -12,7 +12,7 @@
|
|||
* why only a rewrite breaks this.
|
||||
*
|
||||
* The cost of merging is a merge commit per run, plus already-published fixes staying in the
|
||||
* branch's log (the rebase used to drop them as empty). Neither reaches anything downstream:
|
||||
* branch's log. Neither reaches anything downstream:
|
||||
* a bundle is published as one squashed, deliberately obfuscated commit, and the squash is
|
||||
* taken from the tree, not the history. For a list of what a bundle actually carries, read
|
||||
* the fix PRs merged into the branch since the last cut, not `base..bundle`.
|
||||
|
|
@ -26,17 +26,13 @@
|
|||
* of being re-litigated on every later run. Contrast `sync-master-to-3x.mjs`, which
|
||||
* auto-resolves mechanical files and opens a conflict PR.
|
||||
*
|
||||
* PUBLISHED HISTORY ONLY: the base is synced in only once the public repo already has it. A cut
|
||||
* squash-merges the bundle branch into private master as `chore: Bundle …`, and the hourly
|
||||
* public→private mirror then DISCARDS that commit in favour of the public cherry-pick of the same
|
||||
* changes. Building on it would strand the discarded commit on a branch that fix PRs are cut
|
||||
* from, and the replacement gets merged in on top of it later. An unpublished cut is a SKIP (the
|
||||
* mirror clears it within the hour, whatever the cut is, so waiting cannot deadlock); a base
|
||||
* ahead of public for any other reason is a FAILURE, because the mirror will not clear it.
|
||||
* PUBLIC HISTORY ONLY: the base is fetched directly from the public repo. Private master can
|
||||
* temporarily contain a bundle cut that public master does not have yet. Using the private base
|
||||
* would strand that commit on a recreated bundle branch after the private mirror replaces it.
|
||||
*
|
||||
* Runs from a full checkout (fetch-depth 0) of any branch. Assumes credentials are NOT
|
||||
* persisted by checkout — every private fetch and the push go through an explicit token URL,
|
||||
* which matters because the repository is private. The public fetch is anonymous.
|
||||
* persisted by checkout. The bundle fetch and push use an explicit token URL. The public base
|
||||
* fetch is anonymous.
|
||||
*
|
||||
* Env: BUNDLE_BRANCH (e.g. bundle/2.x), BASE_BRANCH (e.g. master),
|
||||
* GH_TOKEN (installation token with contents:write),
|
||||
|
|
@ -60,10 +56,6 @@ const BOT_EMAIL = 'n8n-assistant[bot]@users.noreply.github.com';
|
|||
// installation token this script holds is scoped to the private repo.
|
||||
const PUBLIC_REMOTE = 'https://github.com/n8n-io/n8n.git';
|
||||
|
||||
// What a cut lands on private base before it is published. Anything else ahead of public means
|
||||
// the mirror is stuck, not that a cut is in flight.
|
||||
const BUNDLE_SUBJECT = /^chore: Bundle/;
|
||||
|
||||
function required(env, name) {
|
||||
const value = env[name];
|
||||
if (!value) throw new Error(`${name} env var is required`);
|
||||
|
|
@ -76,49 +68,8 @@ export function annotation(title, message) {
|
|||
return `::error title=${title}::${message.replace(/\n/g, '%0A')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the base tip is still private-only, and what to do about it.
|
||||
*
|
||||
* `null` means the public repo already has it, so the sync may proceed. The predicate is
|
||||
* containment in public rather than "is not a bundle commit", so a direct push that bypassed
|
||||
* ci-restrict-private-merges.yml is caught too; the subjects only decide how to react.
|
||||
*/
|
||||
function unconvergedBase({ git, log, bundle, base, baseSha, publicSha }) {
|
||||
if (isAncestor(git, baseSha, publicSha)) return null;
|
||||
|
||||
const ahead = git(['log', '--format=%h %s', `${publicSha}..${baseSha}`])
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
const subjects = ahead.map((line) => line.slice(line.indexOf(' ') + 1));
|
||||
|
||||
// All-or-nothing: one unexplained commit means the mirror will not clear the base on its own.
|
||||
// An empty range here means unrelated histories, which `every` treats as a skip — the safe
|
||||
// direction, since forcing past an ancestry check we could not evaluate is never right.
|
||||
if (subjects.every((subject) => BUNDLE_SUBJECT.test(subject))) {
|
||||
log(
|
||||
`${base} ${baseSha} has not reached public ${base} ${publicSha} yet; leaving ${bundle} ` +
|
||||
'untouched. The hourly public → private sync clears this.',
|
||||
);
|
||||
return { status: 'unconverged' };
|
||||
}
|
||||
|
||||
// Subjects go to the run log, which is private. The annotation stays a count: it is the
|
||||
// headline of a failed run, and a subject hints at the fix.
|
||||
log(ahead.join('\n'));
|
||||
log(
|
||||
annotation(
|
||||
`${base} is ahead of public`,
|
||||
`${base} carries ${ahead.length} commit(s) that public does not have and that are not ` +
|
||||
`bundle cuts, so the mirror cannot clear them. Fix the mirror, then re-run this workflow.`,
|
||||
),
|
||||
);
|
||||
throw new Error(`${base} is ahead of public by commits that are not bundle cuts.`);
|
||||
}
|
||||
|
||||
// Takes `baseSha` explicitly. This path never fetches the bundle branch, so FETCH_HEAD still
|
||||
// holds the PUBLIC tip here, not the base — never swap the argument for FETCH_HEAD.
|
||||
function createBundleBranch({ git, log, bundle, base, baseSha, remote }) {
|
||||
log(`${bundle} does not exist yet; creating it at ${base} ${baseSha}.`);
|
||||
log(`${bundle} does not exist yet; creating it at public ${base} ${baseSha}.`);
|
||||
git(['checkout', '--force', '-B', bundle, baseSha]);
|
||||
|
||||
const push = attempt(git, ['push', remote, `HEAD:refs/heads/${bundle}`]);
|
||||
|
|
@ -132,28 +83,15 @@ function createBundleBranch({ git, log, bundle, base, baseSha, remote }) {
|
|||
|
||||
/**
|
||||
* One fetch-merge-push cycle. `status: 'rejected'` means the branch moved under us — the
|
||||
* caller re-runs from a fresh fetch rather than forcing anything. `status: 'unconverged'` means
|
||||
* the base is not published yet and nothing was touched.
|
||||
* caller re-runs from a fresh fetch rather than forcing anything.
|
||||
*/
|
||||
function mergeBaseIntoBundle({ git, log, bundle, base, remote }) {
|
||||
// Pin every side to the SHA fetched for it — fetching by URL never updates the origin/*
|
||||
// tracking refs, so FETCH_HEAD is the only handle, and each of the three fetches below
|
||||
// overwrites the last. Read FETCH_HEAD on the line after its own fetch; never separate a
|
||||
// fetch from the read that consumes it.
|
||||
git(['fetch', remote, base]);
|
||||
const baseSha = git(['rev-parse', 'FETCH_HEAD']);
|
||||
|
||||
// The base branch is named the same on both sides (master, 1.x). If that ever stops being
|
||||
// true, map it here rather than at the call site.
|
||||
// Pin the base to the public SHA. Private master can temporarily contain an unpublished cut.
|
||||
const publicFetch = attempt(git, ['fetch', PUBLIC_REMOTE, base]);
|
||||
if (!publicFetch.ok) {
|
||||
// A guard that cannot be evaluated must stop the run, not be assumed satisfied.
|
||||
throw new Error(`Could not fetch ${base} from the public repo:\n${publicFetch.out}`);
|
||||
}
|
||||
const publicSha = git(['rev-parse', 'FETCH_HEAD']);
|
||||
|
||||
const unconverged = unconvergedBase({ git, log, bundle, base, baseSha, publicSha });
|
||||
if (unconverged) return unconverged;
|
||||
const baseSha = git(['rev-parse', 'FETCH_HEAD']);
|
||||
|
||||
const listed = attempt(git, ['ls-remote', '--heads', remote, `refs/heads/${bundle}`]);
|
||||
if (!listed.ok) {
|
||||
|
|
@ -179,7 +117,9 @@ function mergeBaseIntoBundle({ git, log, bundle, base, remote }) {
|
|||
'--format=%h %s',
|
||||
`${baseSha}..${preHead}`,
|
||||
]);
|
||||
log(`Merging ${base} ${baseSha} into ${bundle}. Commits on ${bundle}:\n${pending || '(none)'}`);
|
||||
log(
|
||||
`Merging public ${base} ${baseSha} into ${bundle}. Commits on ${bundle}:\n${pending || '(none)'}`,
|
||||
);
|
||||
|
||||
// The content the merge must produce — and whether the two sides conflict at all. Computed
|
||||
// without touching the working tree, so a conflict costs the branch nothing.
|
||||
|
|
@ -211,7 +151,7 @@ function mergeBaseIntoBundle({ git, log, bundle, base, remote }) {
|
|||
if (push.out) log(push.out);
|
||||
return { status: 'rejected' };
|
||||
}
|
||||
log(`Merged ${base} into ${bundle} — nothing rewritten.`);
|
||||
log(`Merged public ${base} into ${bundle}; nothing was rewritten.`);
|
||||
return { status: 'merged' };
|
||||
}
|
||||
|
||||
|
|
|
|||
136
.github/scripts/sync-bundle-branch.test.mjs
vendored
136
.github/scripts/sync-bundle-branch.test.mjs
vendored
|
|
@ -28,7 +28,6 @@ const fail =
|
|||
};
|
||||
|
||||
const PRE_HEAD = 'PREHEAD';
|
||||
const BASE = 'BASESHA';
|
||||
const PUBLIC = 'PUBLICSHA';
|
||||
const MERGE_TREE = 'MERGETREEOID';
|
||||
const REMOTE = 'https://x-access-token:tok@github.com/n8n-io/n8n-private.git';
|
||||
|
|
@ -52,37 +51,27 @@ const conflictedMergeTree = (...paths) =>
|
|||
fail(`${MERGE_TREE}\n${paths.join('\n')}\n\nCONFLICT (content): Merge conflict in ${paths[0]}`);
|
||||
|
||||
// FETCH_HEAD is one slot holding whatever the last fetch brought down, so the stub has to answer
|
||||
// by looking back at that fetch. Resolving both fetches to the same SHA would let the
|
||||
// convergence guard pass by accident and assert nothing.
|
||||
// by looking back at that fetch.
|
||||
const fetchHead = (_args, calls) => {
|
||||
const [, url, ref] = [...calls].reverse().find((a) => a[0] === 'fetch') ?? [];
|
||||
if (url === PUBLIC_REMOTE) return PUBLIC;
|
||||
if (ref?.startsWith('bundle/')) return PRE_HEAD;
|
||||
return BASE;
|
||||
return '';
|
||||
};
|
||||
|
||||
// Routes shared by every path: base fetched and already published, the bundle branch hasn't
|
||||
// absorbed it yet, the merge tree is computable, and the merge lands on exactly that tree. git
|
||||
// grep exits non-zero => no markers.
|
||||
// Routes shared by every path: the bundle branch has not absorbed the public base yet, the
|
||||
// merge tree is computable, and the merge lands on exactly that tree. git grep exits non-zero
|
||||
// => no markers.
|
||||
const baseGitRoutes = [
|
||||
[(a) => a[0] === 'ls-remote', `${PRE_HEAD}\trefs/heads/bundle/2.x`],
|
||||
[(a) => a[0] === 'rev-parse' && a[1] === 'FETCH_HEAD', fetchHead],
|
||||
[(a) => a[0] === 'rev-parse' && a[1] === 'HEAD', PRE_HEAD],
|
||||
// Split by descendant: the base has reached public, but the bundle branch has not absorbed
|
||||
// it. One `merge-base` matcher would answer both and decide convergence by accident.
|
||||
[(a) => a[0] === 'merge-base' && a[3] === PUBLIC, ''],
|
||||
[(a) => a[0] === 'merge-base' && a[3] === PRE_HEAD, fail()],
|
||||
[(a) => a[0] === 'merge-tree', MERGE_TREE],
|
||||
[(a) => a[0] === 'rev-parse' && a[1] === 'HEAD^{tree}', MERGE_TREE],
|
||||
[(a) => a[0] === 'grep', fail()],
|
||||
];
|
||||
|
||||
// A base that is ahead of public by the commits given, so the guard trips.
|
||||
const unpublishedRoutes = (...lines) => [
|
||||
[(a) => a[0] === 'merge-base' && a[3] === PUBLIC, fail()],
|
||||
[(a) => a[0] === 'log' && a[1] === '--format=%h %s', lines.join('\n')],
|
||||
];
|
||||
|
||||
const silent = () => {};
|
||||
|
||||
test('a missing branch env var fails before any git command runs', () => {
|
||||
|
|
@ -107,14 +96,14 @@ test('a clean sync merges the base in and pushes without forcing', () => {
|
|||
'--no-edit',
|
||||
'-m',
|
||||
'Merge master into bundle/2.x',
|
||||
BASE,
|
||||
PUBLIC,
|
||||
]);
|
||||
// No lease and no --force: the branch is append-only, so a non-fast-forward must be
|
||||
// refused by git rather than resolved by overwriting whatever landed.
|
||||
assert.deepEqual(git.calls.find(isPush), ['push', REMOTE, 'HEAD:refs/heads/bundle/2.x']);
|
||||
});
|
||||
|
||||
test('the private fetches are authenticated and the public one is not', () => {
|
||||
test('the bundle fetch is authenticated and the public base fetch is not', () => {
|
||||
const git = makeStub(baseGitRoutes);
|
||||
syncBundleBranch({ git, env, log: silent });
|
||||
|
||||
|
|
@ -122,7 +111,6 @@ test('the private fetches are authenticated and the public one is not', () => {
|
|||
assert.deepEqual(
|
||||
git.calls.filter((a) => a[0] === 'fetch'),
|
||||
[
|
||||
['fetch', REMOTE, 'master'],
|
||||
['fetch', PUBLIC_REMOTE, 'master'],
|
||||
['fetch', REMOTE, 'bundle/2.x'],
|
||||
],
|
||||
|
|
@ -145,8 +133,6 @@ test('the public tip is read straight after its own fetch', () => {
|
|||
assert.deepEqual(
|
||||
git.calls.filter((a) => a[0] === 'fetch' || (a[0] === 'rev-parse' && a[1] === 'FETCH_HEAD')),
|
||||
[
|
||||
['fetch', REMOTE, 'master'],
|
||||
['rev-parse', 'FETCH_HEAD'],
|
||||
['fetch', PUBLIC_REMOTE, 'master'],
|
||||
['rev-parse', 'FETCH_HEAD'],
|
||||
['fetch', REMOTE, 'bundle/2.x'],
|
||||
|
|
@ -156,8 +142,6 @@ test('the public tip is read straight after its own fetch', () => {
|
|||
|
||||
test('a bundle branch that already contains its base is left alone', () => {
|
||||
const git = makeStub([
|
||||
// Only the bundle-branch ancestry is overridden; the convergence probe keeps its own
|
||||
// answer from baseGitRoutes, or this would pass without the guard being exercised.
|
||||
[(a) => a[0] === 'merge-base' && a[3] === PRE_HEAD, ''], // --is-ancestor succeeds
|
||||
...baseGitRoutes,
|
||||
]);
|
||||
|
|
@ -226,9 +210,8 @@ test('a fix landing mid-run is retried from a fresh fetch, never forced', () =>
|
|||
|
||||
assert.deepEqual(result, { status: 'merged' });
|
||||
assert.equal(pushes, 2);
|
||||
// The retry re-fetches rather than reusing the tip it already merged onto — including the
|
||||
// public tip, so the convergence guard is re-evaluated instead of carried over.
|
||||
assert.equal(git.calls.filter((a) => a[0] === 'fetch').length, 6);
|
||||
// The retry re-fetches both tips instead of reusing the merge that lost the push race.
|
||||
assert.equal(git.calls.filter((a) => a[0] === 'fetch').length, 4);
|
||||
assert.equal(git.calls.filter((a) => a[1] === PUBLIC_REMOTE).length, 2);
|
||||
assert.equal(
|
||||
git.calls.every((a) => !a.some((arg) => String(arg).includes('force-with-lease'))),
|
||||
|
|
@ -244,22 +227,19 @@ test('a branch that keeps moving fails instead of forcing', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('a bundle branch that does not exist yet is created at its base', () => {
|
||||
test('a bundle branch that does not exist yet is created at the public base', () => {
|
||||
const git = makeStub([[(a) => a[0] === 'ls-remote', ''], ...baseGitRoutes]);
|
||||
const result = syncBundleBranch({ git, env, log: silent });
|
||||
|
||||
assert.deepEqual(result, { status: 'created' });
|
||||
assert.deepEqual(
|
||||
git.calls.find((a) => a[0] === 'checkout'),
|
||||
['checkout', '--force', '-B', 'bundle/2.x', BASE],
|
||||
['checkout', '--force', '-B', 'bundle/2.x', PUBLIC],
|
||||
);
|
||||
assert.equal(git.calls.some(isMerge), false);
|
||||
assert.deepEqual(
|
||||
git.calls.filter((a) => a[0] === 'fetch'),
|
||||
[
|
||||
['fetch', REMOTE, 'master'],
|
||||
['fetch', PUBLIC_REMOTE, 'master'],
|
||||
],
|
||||
[['fetch', PUBLIC_REMOTE, 'master']],
|
||||
);
|
||||
assert.deepEqual(git.calls.find(isPush), ['push', REMOTE, 'HEAD:refs/heads/bundle/2.x']);
|
||||
});
|
||||
|
|
@ -301,97 +281,6 @@ test('annotation keeps a multi-line message on one line', () => {
|
|||
assert.equal(annotation('t', 'a\nb'), '::error title=t::a%0Ab');
|
||||
});
|
||||
|
||||
test('a base that has not reached the public repo is left alone', () => {
|
||||
const git = makeStub([
|
||||
...unpublishedRoutes('abc1234 chore: Bundle 2.x (#123)'),
|
||||
...baseGitRoutes,
|
||||
]);
|
||||
const logs = [];
|
||||
|
||||
const result = syncBundleBranch({ git, env, log: (m) => logs.push(m) });
|
||||
|
||||
assert.deepEqual(result, { status: 'unconverged' });
|
||||
// Nothing about the bundle branch is even asked: no existence probe, no fetch, no checkout.
|
||||
assert.equal(
|
||||
git.calls.some((a) => a[0] === 'ls-remote'),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
git.calls.some((a) => a[0] === 'checkout'),
|
||||
false,
|
||||
);
|
||||
assert.equal(git.calls.some(isMerge), false);
|
||||
assert.equal(git.calls.some(isPush), false);
|
||||
assert.equal(git.calls.filter((a) => a[0] === 'fetch').length, 2);
|
||||
assert.match(logs.join('\n'), /has not reached public master/);
|
||||
});
|
||||
|
||||
test('a missing bundle branch is not created from an unpublished base', () => {
|
||||
const git = makeStub([
|
||||
[(a) => a[0] === 'ls-remote', ''],
|
||||
...unpublishedRoutes('abc1234 chore: Bundle/1.x'),
|
||||
...baseGitRoutes,
|
||||
]);
|
||||
|
||||
// One guard covers both paths: the branch must not be conjured onto a doomed commit either.
|
||||
assert.deepEqual(syncBundleBranch({ git, env, log: silent }), { status: 'unconverged' });
|
||||
assert.equal(
|
||||
git.calls.some((a) => a[0] === 'checkout'),
|
||||
false,
|
||||
);
|
||||
assert.equal(git.calls.some(isPush), false);
|
||||
});
|
||||
|
||||
test('a base ahead of public by a non-bundle commit fails loudly', () => {
|
||||
const git = makeStub([
|
||||
...unpublishedRoutes('abc1234 fix(core): Something a human pushed directly'),
|
||||
...baseGitRoutes,
|
||||
]);
|
||||
const logs = [];
|
||||
|
||||
assert.throws(() => syncBundleBranch({ git, env, log: (m) => logs.push(m) }), {
|
||||
message: /master is ahead of public by commits that are not bundle cuts/,
|
||||
});
|
||||
assert.equal(git.calls.some(isPush), false);
|
||||
|
||||
// The annotation is the headline of a failed run: a count, never a subject.
|
||||
const annotations = logs.filter((m) => m.startsWith('::error'));
|
||||
assert.match(annotations.join('\n'), /carries 1 commit\(s\)/);
|
||||
assert.doesNotMatch(annotations.join('\n'), /Something a human pushed directly/);
|
||||
});
|
||||
|
||||
test('one stray commit among bundle cuts still fails', () => {
|
||||
const git = makeStub([
|
||||
...unpublishedRoutes('abc1234 chore: Bundle 2.x (#123)', 'def5678 fix(core): Stray'),
|
||||
...baseGitRoutes,
|
||||
]);
|
||||
|
||||
// All-or-nothing: a single unexplained commit means the mirror cannot clear the base.
|
||||
assert.throws(() => syncBundleBranch({ git, env, log: silent }), {
|
||||
message: /not bundle cuts/,
|
||||
});
|
||||
});
|
||||
|
||||
test('an unrelated history skips rather than forcing anything', () => {
|
||||
const git = makeStub([...unpublishedRoutes(), ...baseGitRoutes]);
|
||||
|
||||
// Ancestry fails but the range is empty, so there is nothing to classify. Skipping is the
|
||||
// only safe default here.
|
||||
assert.deepEqual(syncBundleBranch({ git, env, log: silent }), { status: 'unconverged' });
|
||||
assert.equal(git.calls.some(isPush), false);
|
||||
});
|
||||
|
||||
test('an unconverged base is not retried as a push race', () => {
|
||||
const git = makeStub([...unpublishedRoutes('abc1234 chore: Bundle 2.x'), ...baseGitRoutes]);
|
||||
const logs = [];
|
||||
|
||||
syncBundleBranch({ git, env, log: (m) => logs.push(m) });
|
||||
|
||||
// Only 'rejected' earns a second attempt; the mirror runs hourly, not within this run.
|
||||
assert.equal(git.calls.filter((a) => a[0] === 'fetch').length, 2);
|
||||
assert.doesNotMatch(logs.join('\n'), /retrying/);
|
||||
});
|
||||
|
||||
test('a public fetch that fails stops the run instead of guessing', () => {
|
||||
const git = makeStub([
|
||||
[(a) => a[0] === 'fetch' && a[1] === PUBLIC_REMOTE, fail('could not read from remote')],
|
||||
|
|
@ -423,7 +312,6 @@ test('the 1.x bundle branch is checked against public 1.x', () => {
|
|||
assert.deepEqual(
|
||||
git.calls.filter((a) => a[0] === 'fetch'),
|
||||
[
|
||||
['fetch', REMOTE, '1.x'],
|
||||
['fetch', PUBLIC_REMOTE, '1.x'],
|
||||
['fetch', REMOTE, 'bundle/1.x'],
|
||||
],
|
||||
|
|
|
|||
34
.github/scripts/test-files.mjs
vendored
Normal file
34
.github/scripts/test-files.mjs
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { matchesGlob } from './glob.mjs';
|
||||
|
||||
export const TEST_PATTERNS = [
|
||||
// Test files (by extension)
|
||||
'**/*.test.ts',
|
||||
'**/*.test.js',
|
||||
'**/*.test.mjs',
|
||||
'**/*.spec.ts',
|
||||
'**/*.spec.js',
|
||||
'**/*.spec.mjs',
|
||||
// Test directories
|
||||
'**/test/**',
|
||||
'**/tests/**',
|
||||
'**/__tests__/**',
|
||||
// Snapshots
|
||||
'**/__snapshots__/**',
|
||||
'**/*.snap',
|
||||
// Fixtures and mocks
|
||||
'**/fixtures/**',
|
||||
'**/__mocks__/**',
|
||||
// Former packages/testing workspaces
|
||||
'packages/quality/policy/**',
|
||||
'packages/quality/testing/**',
|
||||
'packages/quality/environments/**',
|
||||
'packages/quality/efficiency/microbenchmarks/**',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param { string } filename
|
||||
* @returns { boolean }
|
||||
*/
|
||||
export function isTestFile(filename) {
|
||||
return TEST_PATTERNS.some((pattern) => matchesGlob(filename, pattern));
|
||||
}
|
||||
2
.github/test-metrics/e2e-impact-map.json
vendored
2
.github/test-metrics/e2e-impact-map.json
vendored
File diff suppressed because one or more lines are too long
10
.github/workflows/build-base-image.yml
vendored
10
.github/workflows/build-base-image.yml
vendored
|
|
@ -32,11 +32,11 @@ jobs:
|
|||
# when updating.
|
||||
include:
|
||||
- node_version: '22'
|
||||
dhi_ref: dhi.io/node:22.23.2-alpine3.24-dev@sha256:155a95d59244a71e9479e191ff718f03a7197d6829612e0707608be92988cb87
|
||||
- node_version: '24.19.0'
|
||||
dhi_ref: dhi.io/node:24.19.0-alpine3.24-dev@sha256:dc2989ad23938772abaf549ed7bde5d61d70377005229bb55bef94bff40f8bbd
|
||||
- node_version: '26.7.0'
|
||||
dhi_ref: dhi.io/node:26.7.0-alpine3.24-dev@sha256:4b494d89fb26c950ce97865acf45b480dc7a6868fdc2b81c2d66599702eeac3f
|
||||
dhi_ref: dhi.io/node:22.23.3-alpine3.24-dev@sha256:23ee07ace431ea7d14e22c0e52fb52b0bd1853dabdacd89ba75f937f6dff8a8d
|
||||
- node_version: '24.21.0'
|
||||
dhi_ref: dhi.io/node:24.21.0-alpine3.24-dev@sha256:d22ba0e9ab2a54609ae533df4ebfd6f36c1f233b662a4c547f5a3195e8c21937
|
||||
- node_version: '26.10.0'
|
||||
dhi_ref: dhi.io/node:26.10.0-alpine3.24-dev@sha256:8da859df5dc853c923ace53c4cfe994a909a28917f7fcfc8508869beab8cf132
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
|
|
|
|||
4
.github/workflows/build-benchmark-image.yml
vendored
4
.github/workflows/build-benchmark-image.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/@n8n/benchmark/**'
|
||||
- 'packages/quality/efficiency/scale/benchmark/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- '.github/workflows/build-benchmark-image.yml'
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
DOCKER_BUILD_SUMMARY: false
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/@n8n/benchmark/Dockerfile
|
||||
file: ./packages/quality/efficiency/scale/benchmark/Dockerfile
|
||||
platforms: linux/amd64
|
||||
provenance: false
|
||||
push: true
|
||||
|
|
|
|||
2
.github/workflows/build-node-pc-image.yml
vendored
2
.github/workflows/build-node-pc-image.yml
vendored
|
|
@ -25,7 +25,7 @@ on:
|
|||
type: boolean
|
||||
|
||||
env:
|
||||
NODE_VERSION: '26.7.0'
|
||||
NODE_VERSION: '26.10.0'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
1
.github/workflows/build-windows.yml
vendored
1
.github/workflows/build-windows.yml
vendored
|
|
@ -23,6 +23,7 @@ on:
|
|||
paths:
|
||||
- '**/package.json'
|
||||
- '**/turbo.json'
|
||||
- 'packages/cli/scripts/build.mjs'
|
||||
- '.github/workflows/build-windows.yml'
|
||||
- '.github/actions/setup-nodejs/**'
|
||||
|
||||
|
|
|
|||
4
.github/workflows/ci-cla-check.yml
vendored
4
.github/workflows/ci-cla-check.yml
vendored
|
|
@ -69,8 +69,8 @@ jobs:
|
|||
steps:
|
||||
# Defaults for the CLA endpoints, status context, comment marker and label
|
||||
# are already n8n's production values.
|
||||
- uses: n8n-io/github-actions/cla-check@ea082925743265a61d9fd0a69a32b211551f88f0 # v1.0.2
|
||||
with:
|
||||
- uses: n8n-io/github-actions/cla-check@ea082925743265a61d9fd0a69a32b211551f88f0 # v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
pr-number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number || github.event.issue.number }}
|
||||
|
|
|
|||
4
.github/workflows/ci-pr-quality.yml
vendored
4
.github/workflows/ci-pr-quality.yml
vendored
|
|
@ -99,8 +99,8 @@ jobs:
|
|||
mode: filter
|
||||
filters: |
|
||||
janitor:
|
||||
packages/testing/playwright/**
|
||||
packages/testing/janitor/**
|
||||
packages/quality/testing/playwright/**
|
||||
packages/quality/testing/janitor/**
|
||||
|
||||
check-static-analysis:
|
||||
name: Static Analysis
|
||||
|
|
|
|||
63
.github/workflows/ci-pull-requests.yml
vendored
63
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -79,7 +79,8 @@ jobs:
|
|||
!**/CHANGELOG.md
|
||||
!**/*.test.ts
|
||||
!**/*.spec.ts
|
||||
!packages/testing/playwright/**
|
||||
!packages/quality/testing/playwright/**
|
||||
!packages/quality/efficiency/**
|
||||
!packages/frontend/@n8n/storybook/**
|
||||
!.devcontainer/**
|
||||
!scripts/agent-setup.mjs
|
||||
|
|
@ -92,7 +93,7 @@ jobs:
|
|||
unit:
|
||||
**
|
||||
!packages/@n8n/task-runner-python/**
|
||||
!packages/testing/playwright/**
|
||||
!packages/quality/testing/playwright/**
|
||||
!.github/**
|
||||
.github/workflows/test-unit-reusable.yml
|
||||
!**/*.md
|
||||
|
|
@ -111,8 +112,8 @@ jobs:
|
|||
.github/workflows/prepare-docker-reusable.yml
|
||||
.github/actions/build-n8n-docker/**
|
||||
.github/actions/load-n8n-docker/**
|
||||
packages/testing/playwright/**
|
||||
packages/testing/containers/**
|
||||
packages/quality/testing/playwright/**
|
||||
packages/quality/environments/containers/**
|
||||
dev-server-smoke:
|
||||
packages/frontend/editor-ui/vite.config.mts
|
||||
# If this job becomes too slow, use a smaller glob. Do not delete the assertion.
|
||||
|
|
@ -120,10 +121,10 @@ jobs:
|
|||
pnpm-workspace.yaml
|
||||
packages/@n8n/*/package.json
|
||||
packages/frontend/@n8n/frontend-vite-config/**
|
||||
packages/testing/playwright/tests/dev-server-smoke/**
|
||||
packages/testing/playwright/playwright.config.ts
|
||||
packages/testing/playwright/playwright-projects.ts
|
||||
packages/testing/playwright/package.json
|
||||
packages/quality/testing/playwright/tests/dev-server-smoke/**
|
||||
packages/quality/testing/playwright/playwright.config.ts
|
||||
packages/quality/testing/playwright/playwright-projects.ts
|
||||
packages/quality/testing/playwright/package.json
|
||||
.github/workflows/test-dev-server-smoke-reusable.yml
|
||||
frontend-declarations:
|
||||
packages/frontend/@n8n/design-system/**
|
||||
|
|
@ -146,10 +147,17 @@ jobs:
|
|||
cubic.yaml
|
||||
.agents/review-rules/**
|
||||
e2e-performance:
|
||||
packages/testing/playwright/tests/performance/**
|
||||
packages/testing/playwright/utils/performance-helper.ts
|
||||
packages/testing/containers/**
|
||||
packages/quality/efficiency/memory/idle/**
|
||||
packages/quality/efficiency/playwright.config.ts
|
||||
packages/quality/efficiency/package.json
|
||||
packages/quality/testing/playwright/package.json
|
||||
packages/quality/testing/playwright/quality-*.ts
|
||||
packages/quality/testing/playwright/fixtures/**
|
||||
packages/quality/testing/playwright/tests/performance/**
|
||||
packages/quality/testing/playwright/utils/performance-helper.ts
|
||||
packages/quality/environments/containers/**
|
||||
.github/workflows/test-e2e-performance-reusable.yml
|
||||
.github/workflows/test-e2e-reusable.yml
|
||||
# Whole directories, not the `engine-*` files alone. Engine v2
|
||||
# routing lives in ordinary control plane code: `workflow-runner.ts`
|
||||
# and the webhook and trigger paths decide whether a run reaches the
|
||||
|
|
@ -169,8 +177,8 @@ jobs:
|
|||
packages/cli/src/webhooks/**
|
||||
packages/cli/src/workflows/triggers/**
|
||||
packages/cli/src/workflow-runner.ts
|
||||
packages/testing/containers/**
|
||||
packages/testing/playwright/**
|
||||
packages/quality/environments/containers/**
|
||||
packages/quality/testing/playwright/**
|
||||
instance-ai-workflow-eval:
|
||||
packages/@n8n/instance-ai/src/**
|
||||
packages/@n8n/instance-ai/evaluations/**
|
||||
|
|
@ -178,6 +186,7 @@ jobs:
|
|||
packages/core/src/execution-engine/eval-mock-helpers.ts
|
||||
.github/workflows/test-evals-discovery.yml
|
||||
db:
|
||||
packages/modules/*/backend/**
|
||||
packages/cli/src/databases/**
|
||||
packages/cli/src/modules/*/database/**
|
||||
packages/cli/src/modules/**/*.entity.ts
|
||||
|
|
@ -185,18 +194,19 @@ jobs:
|
|||
packages/cli/test/integration/**
|
||||
packages/cli/test/migration/**
|
||||
packages/cli/test/shared/db/**
|
||||
packages/cli/test/setup-testcontainers.js
|
||||
packages/cli/test/teardown-testcontainers.js
|
||||
packages/cli/test/setup-testcontainers.ts
|
||||
packages/@n8n/db/**
|
||||
packages/@n8n/backend-test-utils/**
|
||||
packages/@n8n/scheduler/src/**
|
||||
packages/cli/src/scheduling/**
|
||||
packages/cli/src/**/*.integration.test.ts
|
||||
packages/cli/src/modules/instance-registry/**
|
||||
packages/cli/vitest.*
|
||||
packages/testing/containers/services/postgres.ts
|
||||
packages/testing/containers/services/redis.ts
|
||||
packages/testing/containers/postgres-versions.json
|
||||
packages/testing/containers/test-containers.ts
|
||||
packages/quality/environments/containers/services/postgres.ts
|
||||
packages/quality/environments/containers/services/redis.ts
|
||||
packages/quality/environments/containers/test-db-global-setup.ts
|
||||
packages/quality/environments/containers/postgres-versions.json
|
||||
packages/quality/environments/containers/test-containers.ts
|
||||
.github/workflows/test-db-reusable.yml
|
||||
.github/scripts/db-test-matrix.mjs
|
||||
docs/generated/**
|
||||
|
|
@ -209,15 +219,14 @@ jobs:
|
|||
packages/@n8n/backend-test-utils/**
|
||||
packages/cli/test/migration/**
|
||||
packages/cli/test/shared/db/**
|
||||
packages/cli/test/setup-testcontainers.js
|
||||
packages/cli/test/teardown-testcontainers.js
|
||||
packages/cli/test/setup-testcontainers.ts
|
||||
packages/cli/vitest.config.base.ts
|
||||
packages/cli/vitest.config.migration.ts
|
||||
packages/cli/vitest.config.migration.testcontainers.ts
|
||||
packages/testing/containers/services/postgres.ts
|
||||
packages/testing/containers/services/redis.ts
|
||||
packages/testing/containers/postgres-versions.json
|
||||
packages/testing/containers/test-containers.ts
|
||||
packages/quality/environments/containers/services/postgres.ts
|
||||
packages/quality/environments/containers/services/redis.ts
|
||||
packages/quality/environments/containers/postgres-versions.json
|
||||
packages/quality/environments/containers/test-containers.ts
|
||||
.github/workflows/test-db-reusable.yml
|
||||
.github/scripts/db-test-matrix.mjs
|
||||
|
||||
|
|
@ -247,7 +256,7 @@ jobs:
|
|||
env:
|
||||
CHANGED_FILES: ${{ steps.ci-filter.outputs.changed-files }}
|
||||
run: |
|
||||
PACKAGES=$(node packages/testing/janitor/dist/cli.js affected-packages | tr '\n' ' ' | sed 's/ *$//')
|
||||
PACKAGES=$(node packages/quality/testing/janitor/dist/cli.js affected-packages | tr '\n' ' ' | sed 's/ *$//')
|
||||
echo "Affected packages: $PACKAGES"
|
||||
echo "list=$PACKAGES" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
|
@ -393,7 +402,7 @@ jobs:
|
|||
PLAYWRIGHT_PROJECT: ${{ github.event.pull_request.head.repo.fork == true && 'sqlite:e2e' || 'multi-main:e2e' }}
|
||||
run: |
|
||||
FILES_CSV=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//')
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --impact "--project=$PLAYWRIGHT_PROJECT" "--files=$FILES_CSV" "--base=$MERGE_BASE")
|
||||
MATRIX=$(node packages/quality/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --impact "--project=$PLAYWRIGHT_PROJECT" "--files=$FILES_CSV" "--base=$MERGE_BASE")
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
echo "skip-tests=$(node -e "process.stdout.write(JSON.parse(process.argv[1])[0]?.skip === true ? 'true' : 'false')" "$MATRIX")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
|
|
|||
4
.github/workflows/ci-terraform.yml
vendored
4
.github/workflows/ci-terraform.yml
vendored
|
|
@ -3,7 +3,7 @@ name: 'CI: Terraform'
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/@n8n/benchmark/infra/**
|
||||
- packages/quality/efficiency/scale/benchmark/infra/**
|
||||
- .github/workflows/ci-terraform.yml
|
||||
|
||||
# The job only reads the repo and runs terraform. It writes nothing.
|
||||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/@n8n/benchmark/infra
|
||||
working-directory: packages/quality/efficiency/scale/benchmark/infra
|
||||
steps:
|
||||
- name: Check out project
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
|
|
|||
2
.github/workflows/docker-build-push.yml
vendored
2
.github/workflows/docker-build-push.yml
vendored
|
|
@ -8,7 +8,7 @@ run-name: "${{ github.event_name == 'workflow_dispatch' && format('Docker: Build
|
|||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=7168'
|
||||
NODE_VERSION: '26.7.0'
|
||||
NODE_VERSION: '26.10.0'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
|
|||
2
.github/workflows/docker-build-smoke.yml
vendored
2
.github/workflows/docker-build-smoke.yml
vendored
|
|
@ -12,7 +12,7 @@ env:
|
|||
# The images copy native bindings from the host build, so both have to use the same
|
||||
# Node. dockerize-n8n.mjs reads this env for the image builds. Keep in sync with
|
||||
# NODE_VERSION in docker-build-push.yml, which is what the published images use.
|
||||
NODE_VERSION: '26.7.0'
|
||||
NODE_VERSION: '26.10.0'
|
||||
# Also build the distroless runners image and exec-check
|
||||
# both runners images' interpreters, so a broken runtime assembly (e.g. a
|
||||
# missing shared library after a Node bump) fails at PR time. The smoke
|
||||
|
|
|
|||
2
.github/workflows/docker-sbom-probe.yml
vendored
2
.github/workflows/docker-sbom-probe.yml
vendored
|
|
@ -19,7 +19,7 @@ on:
|
|||
default: false
|
||||
|
||||
env:
|
||||
NODE_VERSION: '26.7.0'
|
||||
NODE_VERSION: '26.10.0'
|
||||
SCRATCH_TAG: sbom-probe-${{ github.run_id }}
|
||||
# A separate package. These are throwaway fixtures (one is a two-layer
|
||||
# alpine), and ghcr.io/<repo> is where the real release tags live - they
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ on:
|
|||
# verify themselves (both modes) before the merge.
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/testing/playwright/tests/infrastructure/encryption/**'
|
||||
- 'packages/testing/containers/services/n8n.ts'
|
||||
- 'packages/testing/containers/services/types.ts'
|
||||
- 'packages/testing/containers/stack.ts'
|
||||
- 'packages/quality/testing/playwright/tests/infrastructure/encryption/**'
|
||||
- 'packages/quality/environments/containers/services/n8n.ts'
|
||||
- 'packages/quality/environments/containers/services/types.ts'
|
||||
- 'packages/quality/environments/containers/stack.ts'
|
||||
- '.github/workflows/encryption-upgrade-test.yml'
|
||||
|
||||
permissions:
|
||||
|
|
|
|||
309
.github/workflows/jscpd.yml
vendored
Normal file
309
.github/workflows/jscpd.yml
vendored
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
# 🍝 jscpd — deterministic copy-paste/duplication scan.
|
||||
#
|
||||
# Runs jscpd against this repo and uploads the full JSON+HTML report, plus
|
||||
# the report split into BUCKET_COUNT small JSON files, so a caller can
|
||||
# download one small bucket at random per invocation instead of the full
|
||||
# report (which can run into tens of MB on a monorepo this size).
|
||||
#
|
||||
# Reporters/ignores/threshold come from this repo's own .jscpd.json; this
|
||||
# workflow passes none of those flags itself.
|
||||
|
||||
name: 🍝 jscpd
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Number of small sample files the report is split into (split-jscpd-report.mjs).
|
||||
# Fixed, not an input: changing it means adding/removing the matching
|
||||
# per-bucket upload steps below, so it's a code change, not a dispatch option.
|
||||
BUCKET_COUNT: '20'
|
||||
|
||||
jobs:
|
||||
jscpd:
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js and install dependencies
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: ''
|
||||
|
||||
- name: Run jscpd
|
||||
id: jscpd
|
||||
continue-on-error: true
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/jscpd-report"
|
||||
pnpm jscpd -- . --output "$RUNNER_TEMP/jscpd-report" --silent
|
||||
|
||||
- name: Upload jscpd report
|
||||
id: report-artifact
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-report-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/jscpd-report
|
||||
retention-days: 31
|
||||
if-no-files-found: warn
|
||||
|
||||
# jscpd exits non-zero once duplication crosses .jscpd.json's threshold,
|
||||
# but still writes the report first. Gating the split on the report
|
||||
# file existing (not on steps.jscpd.outcome) means an over-threshold
|
||||
# run still gets its buckets instead of silently dropping all of them.
|
||||
- name: Check jscpd report exists
|
||||
id: report-exists
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f "$RUNNER_TEMP/jscpd-report/jscpd-report.json" ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Splits `duplicates` into BUCKET_COUNT files so a caller can download
|
||||
# one small sample instead of the full report (tens of MB on a large
|
||||
# monorepo). Logic + tests live in split-jscpd-report.mjs.
|
||||
- name: Split report into buckets
|
||||
id: split
|
||||
if: always() && steps.report-exists.outputs.exists == 'true'
|
||||
continue-on-error: true
|
||||
env:
|
||||
REPORT_PATH: ${{ runner.temp }}/jscpd-report/jscpd-report.json
|
||||
OUTPUT_DIR: ${{ runner.temp }}/jscpd-buckets
|
||||
run: node .github/scripts/split-jscpd-report.mjs
|
||||
|
||||
# BUCKET_COUNT static upload steps, one per bucket file: GitHub Actions
|
||||
# has no per-job step loop, and each bucket needs its own small
|
||||
# artifact (not one artifact holding all of them) so a caller can
|
||||
# fetch a single sample without pulling the rest. Keep this block's
|
||||
# step count in sync with BUCKET_COUNT above.
|
||||
- name: Upload jscpd bucket 0
|
||||
id: bucket-artifact-0
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-0
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-0.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 1
|
||||
id: bucket-artifact-1
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-1
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-1.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 2
|
||||
id: bucket-artifact-2
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-2
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-2.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 3
|
||||
id: bucket-artifact-3
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-3
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-3.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 4
|
||||
id: bucket-artifact-4
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-4
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-4.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 5
|
||||
id: bucket-artifact-5
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-5
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-5.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 6
|
||||
id: bucket-artifact-6
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-6
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-6.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 7
|
||||
id: bucket-artifact-7
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-7
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-7.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 8
|
||||
id: bucket-artifact-8
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-8
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-8.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 9
|
||||
id: bucket-artifact-9
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-9
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-9.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 10
|
||||
id: bucket-artifact-10
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-10
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-10.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 11
|
||||
id: bucket-artifact-11
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-11
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-11.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 12
|
||||
id: bucket-artifact-12
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-12
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-12.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 13
|
||||
id: bucket-artifact-13
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-13
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-13.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 14
|
||||
id: bucket-artifact-14
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-14
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-14.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 15
|
||||
id: bucket-artifact-15
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-15
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-15.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 16
|
||||
id: bucket-artifact-16
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-16
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-16.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 17
|
||||
id: bucket-artifact-17
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-17
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-17.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 18
|
||||
id: bucket-artifact-18
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-18
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-18.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload jscpd bucket 19
|
||||
id: bucket-artifact-19
|
||||
if: always() && steps.split.outcome == 'success'
|
||||
continue-on-error: true
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: jscpd-bucket-${{ github.run_id }}-${{ github.run_attempt }}-19
|
||||
path: ${{ runner.temp }}/jscpd-buckets/bucket-19.json
|
||||
retention-days: 31
|
||||
if-no-files-found: error
|
||||
10
.github/workflows/prepare-docker-reusable.yml
vendored
10
.github/workflows/prepare-docker-reusable.yml
vendored
|
|
@ -9,6 +9,16 @@ name: 'Prepare n8n Docker (reusable)'
|
|||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
QA_METRICS_WEBHOOK_URL:
|
||||
description: 'QA metrics webhook URL.'
|
||||
required: false
|
||||
QA_METRICS_WEBHOOK_USER:
|
||||
description: 'QA metrics webhook username.'
|
||||
required: false
|
||||
QA_METRICS_WEBHOOK_PASSWORD:
|
||||
description: 'QA metrics webhook password.'
|
||||
required: false
|
||||
inputs:
|
||||
build-variant:
|
||||
description: 'standard, coverage, or pc'
|
||||
|
|
|
|||
2
.github/workflows/sec-publish-fix-1x.yml
vendored
2
.github/workflows/sec-publish-fix-1x.yml
vendored
|
|
@ -68,4 +68,4 @@ jobs:
|
|||
run: |
|
||||
node .github/scripts/slack/notify.mjs \
|
||||
--channel '#alerts-security' \
|
||||
--text 'Security fix PR creation failed (1.x). Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
--text 'Security fix PR creation failed (1.x). Run "Security: Sync from Public", merge the refreshed bundle branch into the fix branches, and reopen the bundle PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
|
|
|
|||
2
.github/workflows/sec-publish-fix.yml
vendored
2
.github/workflows/sec-publish-fix.yml
vendored
|
|
@ -58,4 +58,4 @@ jobs:
|
|||
run: |
|
||||
node .github/scripts/slack/notify.mjs \
|
||||
--channel '#alerts-security' \
|
||||
--text 'Security fix PR creation failed. Run "Security: Sync from Public" workflow, rebase your branch, reopen PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
--text 'Security fix PR creation failed. Run "Security: Sync from Public", merge the refreshed bundle branch into the fix branches, and reopen the bundle PR. (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
|
|
|
|||
33
.github/workflows/sec-sync-bundle-branches.yml
vendored
33
.github/workflows/sec-sync-bundle-branches.yml
vendored
|
|
@ -1,8 +1,8 @@
|
|||
# Keeps the bundle integration branches in sync with their base, in n8n-io/n8n-private.
|
||||
#
|
||||
# Fixes are integrated on bundle/2.x and bundle/1.x and eventually merged into private
|
||||
# master/1.x as a squashed `chore: Bundle/*` PR. Those branches must not drift from their
|
||||
# base, or the bundle cut turns into a conflict-resolution session.
|
||||
# master/1.x as a squashed `chore: Bundle/*` PR. Each bundle branch tracks its public base
|
||||
# directly, so a private-only bundle cut can never become part of the next bundle branch.
|
||||
#
|
||||
# The base is MERGED IN, never replayed: these branches receive PRs, and rewriting a branch
|
||||
# that receives PRs orphans the commits its PR branches already hold — each such PR then shows
|
||||
|
|
@ -12,18 +12,11 @@
|
|||
# Every push is verified to carry exactly the tree a merge would produce. See
|
||||
# sync-bundle-branch.mjs.
|
||||
#
|
||||
# Daily rather than hourly on purpose: a base force-push never re-triggered CI on the fix PRs,
|
||||
# so a faster cadence bought them nothing while re-running the full suite on the cut PR (whose
|
||||
# head is the bundle branch). A merged fix and a manual dispatch before a cut are the moments
|
||||
# that matter.
|
||||
#
|
||||
# Only published history is synced in. A cut leaves a private-only `chore: Bundle/*` commit on
|
||||
# private master that the hourly public→private mirror later discards for the public copy, so a
|
||||
# run in that window skips instead of building the branch on it — a dispatch or a merged fix PR
|
||||
# right after a cut can legitimately do nothing. See sync-bundle-branch.mjs.
|
||||
# Runs nightly at 03:00 UTC. Maintainers can also dispatch it before a bundle cut or when a
|
||||
# bundle branch needs to be synced sooner.
|
||||
#
|
||||
# One job per bundle branch: a conflict on one FAILS THAT JOB and leaves its branch untouched,
|
||||
# while the other still syncs. Recovery is manual and deliberate — merge the base in locally,
|
||||
# while the other still syncs. Recovery is manual and deliberate — merge the public base locally,
|
||||
# resolve, push, then re-run this workflow. That resolution then lives in the merge commit,
|
||||
# instead of being re-litigated on every later run.
|
||||
|
||||
|
|
@ -32,11 +25,6 @@ name: 'Security: Sync Bundle Branches'
|
|||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
pull_request: # a fix landing on a bundle branch is when freshness matters most
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'bundle/2.x'
|
||||
- 'bundle/1.x'
|
||||
workflow_dispatch:
|
||||
|
||||
# Least privilege by default; the sync job opts into exactly what it needs.
|
||||
|
|
@ -45,9 +33,7 @@ permissions: {}
|
|||
jobs:
|
||||
sync:
|
||||
name: Sync ${{ matrix.bundle }}
|
||||
if: |
|
||||
github.repository == 'n8n-io/n8n-private' &&
|
||||
(github.event_name != 'pull_request' || github.event.pull_request.merged == true)
|
||||
if: github.repository == 'n8n-io/n8n-private'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -75,17 +61,12 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Always run the script from the default branch, never from a pull_request merge
|
||||
# ref — the sync holds a contents:write token and fetches what it needs itself,
|
||||
# so the checked-out branch is irrelevant beyond supplying trusted code.
|
||||
# Always run the script from the default branch. The sync fetches both branches itself.
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
persist-credentials: false # we push with an explicit token URL instead
|
||||
|
||||
- name: Merge the base into the bundle branch
|
||||
# On a merged bundle PR, only the branch that received it needs syncing.
|
||||
# (`matrix` is not available in a job-level `if`, so the filter lives here.)
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.base.ref == matrix.bundle
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
BUNDLE_BRANCH: ${{ matrix.bundle }}
|
||||
|
|
|
|||
27
.github/workflows/sec-sync-public-to-private.yml
vendored
27
.github/workflows/sec-sync-public-to-private.yml
vendored
|
|
@ -7,10 +7,6 @@
|
|||
# Manual runs always sync (for conflict recovery after failed cherry-pick).
|
||||
#
|
||||
# A skipped branch or a hard failure is reported to #alerts-build.
|
||||
#
|
||||
# The bundle/* integration branches are kept current by sec-sync-bundle-branches.yml, which
|
||||
# depends on this mirror: it refuses to build a bundle branch on a base commit the public repo
|
||||
# does not have yet. So while this workflow is stuck, bundle syncing is stuck too.
|
||||
|
||||
name: 'Security: Sync from Public'
|
||||
run-name: "${{ github.event_name == 'workflow_dispatch' && format('Security: Sync from Public (force={0})', inputs.force) || '' }}"
|
||||
|
|
@ -31,7 +27,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: Generate App Token
|
||||
|
|
@ -104,28 +99,6 @@ jobs:
|
|||
git reset --hard public-1.x
|
||||
git push origin 1.x --force-with-lease
|
||||
|
||||
# Dispatch only: the bundle sync creates the branch and moves its PRs back onto it.
|
||||
- name: Dispatch re-creation of missing bundle branches
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
EXISTING=$(git ls-remote --heads origin 'refs/heads/bundle/*')
|
||||
|
||||
MISSING=()
|
||||
for BRANCH in bundle/2.x bundle/1.x; do
|
||||
if ! grep -qF "refs/heads/$BRANCH" <<<"$EXISTING"; then
|
||||
MISSING+=("$BRANCH")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING[@]} -eq 0 ]; then
|
||||
echo 'Both bundle branches exist; nothing to dispatch.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Missing: ${MISSING[*]} - dispatching Security: Sync Bundle Branches"
|
||||
gh workflow run sec-sync-bundle-branches.yml --ref master
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: failure()
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -36,4 +36,4 @@ jobs:
|
|||
|
||||
- name: Destroy cloud env
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
||||
working-directory: packages/quality/efficiency/scale/benchmark
|
||||
|
|
|
|||
8
.github/workflows/test-benchmark-nightly.yml
vendored
8
.github/workflows/test-benchmark-nightly.yml
vendored
|
|
@ -62,11 +62,11 @@ jobs:
|
|||
|
||||
- name: Destroy any existing environment
|
||||
run: pnpm destroy-cloud-env
|
||||
working-directory: packages/@n8n/benchmark
|
||||
working-directory: packages/quality/efficiency/scale/benchmark
|
||||
|
||||
- name: Provision the environment
|
||||
run: pnpm provision-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
working-directory: packages/quality/efficiency/scale/benchmark
|
||||
|
||||
- name: Run the benchmark
|
||||
id: benchmark
|
||||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
--n8nTag ${{ env.N8N_TAG }} \
|
||||
--benchmarkTag ${{ env.N8N_BENCHMARK_TAG }} \
|
||||
${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
working-directory: packages/quality/efficiency/scale/benchmark
|
||||
|
||||
# We need to login again because the access token expires
|
||||
- name: Azure login
|
||||
|
|
@ -95,7 +95,7 @@ jobs:
|
|||
- name: Destroy the environment
|
||||
if: always()
|
||||
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
|
||||
working-directory: packages/@n8n/benchmark
|
||||
working-directory: packages/quality/efficiency/scale/benchmark
|
||||
|
||||
- name: Fail `build` job if `benchmark` step failed
|
||||
if: steps.benchmark.outcome == 'failure'
|
||||
|
|
|
|||
7
.github/workflows/test-db-reusable.yml
vendored
7
.github/workflows/test-db-reusable.yml
vendored
|
|
@ -55,6 +55,13 @@ jobs:
|
|||
working-directory: packages/cli
|
||||
run: ${{ matrix.test-cmd }}
|
||||
|
||||
# A packaged backend module opts in by defining `test:postgres`.
|
||||
- name: Run packaged backend module PostgreSQL tests
|
||||
if: matrix.TEST_IMAGE_POSTGRES != ''
|
||||
env:
|
||||
COVERAGE_ENABLED: 'false'
|
||||
run: pnpm --filter './packages/modules/*/backend' --if-present run test:postgres --maxWorkers=100%
|
||||
|
||||
# Regenerates the schema docs for this leg's database (sqlite temp file /
|
||||
# postgres testcontainer) and fails if they differ from what's committed.
|
||||
# tbls runs as a Docker image in CI.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ on:
|
|||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/.playwright-browsers
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/quality/testing/playwright/.playwright-browsers
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
|
|
@ -53,6 +53,6 @@ jobs:
|
|||
with:
|
||||
name: dev-server-smoke-report
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
packages/quality/testing/playwright/test-results/
|
||||
packages/quality/testing/playwright/playwright-report/
|
||||
retention-days: 7
|
||||
|
|
|
|||
10
.github/workflows/test-e2e-coverage-nightly.yml
vendored
10
.github/workflows/test-e2e-coverage-nightly.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- name: Generate matrix (up to 20 shards, duration-weighted)
|
||||
id: gen
|
||||
run: |
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --project=coverage)
|
||||
MATRIX=$(node packages/quality/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --project=coverage)
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
|
|
@ -215,7 +215,7 @@ jobs:
|
|||
# Merges all lcovs into one lcov.info (SF: paths normalised to repo-root-
|
||||
# relative) and builds the per-spec E2E impact map for PR selection.
|
||||
- name: Aggregate coverage + build impact map
|
||||
working-directory: packages/testing/playwright
|
||||
working-directory: packages/quality/testing/playwright
|
||||
run: |
|
||||
node scripts/aggregate-coverage.mjs \
|
||||
--shards=/tmp/shards \
|
||||
|
|
@ -227,7 +227,7 @@ jobs:
|
|||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: coverage-report
|
||||
path: packages/testing/playwright/coverage/
|
||||
path: packages/quality/testing/playwright/coverage/
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload Combined Coverage to Codecov
|
||||
|
|
@ -235,7 +235,7 @@ jobs:
|
|||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: packages/testing/playwright/coverage/lcov.info
|
||||
files: packages/quality/testing/playwright/coverage/lcov.info
|
||||
flags: nightly-full
|
||||
name: nightly-full-coverage
|
||||
fail_ci_if_error: false
|
||||
|
|
@ -256,7 +256,7 @@ jobs:
|
|||
env:
|
||||
CODECOV_API_TOKEN: ${{ secrets.CODECOV_API_TOKEN }}
|
||||
run: |
|
||||
node packages/testing/playwright/scripts/coverage-analysis.mjs \
|
||||
node packages/quality/testing/playwright/scripts/coverage-analysis.mjs \
|
||||
--md --top=15 --out-json=coverage-gaps.json >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload Coverage Gap Report
|
||||
|
|
|
|||
12
.github/workflows/test-e2e-helm.yml
vendored
12
.github/workflows/test-e2e-helm.yml
vendored
|
|
@ -5,8 +5,8 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-e2e-helm.yml'
|
||||
- 'packages/testing/containers/helm-stack.ts'
|
||||
- 'packages/testing/containers/helm-start-stack.ts'
|
||||
- 'packages/quality/environments/containers/helm-stack.ts'
|
||||
- 'packages/quality/environments/containers/helm-start-stack.ts'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
|
|
@ -83,7 +83,7 @@ jobs:
|
|||
build-command: 'pnpm build'
|
||||
|
||||
- name: Install Browsers
|
||||
working-directory: packages/testing/playwright
|
||||
working-directory: packages/quality/testing/playwright
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Install Helm
|
||||
|
|
@ -115,7 +115,7 @@ jobs:
|
|||
echo "KUBECONFIG=${KUBECONFIG_PATH}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run Building Blocks E2E Tests
|
||||
working-directory: packages/testing/playwright
|
||||
working-directory: packages/quality/testing/playwright
|
||||
run: |
|
||||
N8N_BASE_URL="${{ env.N8N_BASE_URL }}" \
|
||||
RESET_E2E_DB=true \
|
||||
|
|
@ -150,8 +150,8 @@ jobs:
|
|||
with:
|
||||
name: helm-e2e-report-${{ inputs.mode || 'queue' }}
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
packages/quality/testing/playwright/test-results/
|
||||
packages/quality/testing/playwright/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
cleanup-ghcr:
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
# The aggregator auto-detects n8n version and commit SHA from the repo
|
||||
# (anchored to its own file location), so no path/version glue is needed.
|
||||
- name: Aggregate sizing matrix
|
||||
working-directory: packages/testing/playwright
|
||||
working-directory: packages/quality/testing/playwright
|
||||
run: |
|
||||
pnpm exec tsx scripts/sizing-matrix-aggregate.ts \
|
||||
--input /tmp/benchmark-reports \
|
||||
|
|
@ -86,8 +86,8 @@ jobs:
|
|||
with:
|
||||
name: sizing-matrix
|
||||
path: |
|
||||
packages/testing/playwright/sizing-matrix.json
|
||||
packages/testing/playwright/sizing-matrix.md
|
||||
packages/quality/testing/playwright/sizing-matrix.json
|
||||
packages/quality/testing/playwright/sizing-matrix.md
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test-e2e-pc-nightly.yml
vendored
2
.github/workflows/test-e2e-pc-nightly.yml
vendored
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
- name: Generate matrix (up to 20 shards, duration-weighted)
|
||||
id: gen
|
||||
run: |
|
||||
MATRIX=$(node packages/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --project=sqlite:e2e)
|
||||
MATRIX=$(node packages/quality/testing/playwright/scripts/distribute-tests.mjs --matrix 20 --orchestrate --project=sqlite:e2e)
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
|
||||
e2e:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
name: 'Test: E2E Performance'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
|
@ -24,6 +27,17 @@ jobs:
|
|||
artifact-prefix: performance
|
||||
secrets: inherit
|
||||
|
||||
idle-memory-baselines:
|
||||
needs: [prepare-docker]
|
||||
uses: ./.github/workflows/test-e2e-reusable.yml
|
||||
with:
|
||||
test-mode: docker-artifact
|
||||
test-command: pnpm --filter=@n8n/efficiency test:memory
|
||||
workers: '1'
|
||||
currents-project-id: 'O9BJaN'
|
||||
artifact-prefix: memory-performance
|
||||
secrets: inherit
|
||||
|
||||
canvas-perf-sentinels:
|
||||
name: Canvas Perf Sentinels
|
||||
# Sentinels are nightly only — wall-clock browser perf has 15-30% variance,
|
||||
|
|
@ -37,7 +51,7 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: packages/testing/playwright/scripts/canvas-perf-sentinels.mjs
|
||||
sparse-checkout: packages/quality/testing/playwright/scripts/canvas-perf-sentinels.mjs
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Download performance shard artifacts
|
||||
|
|
@ -54,4 +68,4 @@ jobs:
|
|||
echo "::warning::No test-results.json found in performance artifacts — sentinel skipped."
|
||||
exit 0
|
||||
fi
|
||||
node packages/testing/playwright/scripts/canvas-perf-sentinels.mjs "${RESULTS_PATHS[@]}"
|
||||
node packages/quality/testing/playwright/scripts/canvas-perf-sentinels.mjs "${RESULTS_PATHS[@]}"
|
||||
|
|
|
|||
21
.github/workflows/test-e2e-reusable.yml
vendored
21
.github/workflows/test-e2e-reusable.yml
vendored
|
|
@ -69,7 +69,7 @@ env:
|
|||
# Browser cache location - must match install-browsers script.
|
||||
# Absolute so nested pnpm invocations (e.g. coverage shard runner) don't
|
||||
# re-resolve a relative path against the package's INIT_CWD and double it.
|
||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/packages/testing/playwright/.playwright-browsers
|
||||
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/packages/quality/testing/playwright/.playwright-browsers
|
||||
# docker-artifact loads the image locally as n8nio/n8n:local; unused in local mode.
|
||||
TEST_IMAGE_N8N: 'n8nio/n8n:local'
|
||||
N8N_SKIP_LICENSES: 'true'
|
||||
|
|
@ -123,7 +123,7 @@ jobs:
|
|||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
run: |
|
||||
FILTERED=$(echo "$CANDIDATES" | node packages/testing/janitor/dist/cli.js filter-shard | tr '\n' ' ' | sed 's/ *$//')
|
||||
FILTERED=$(echo "$CANDIDATES" | node packages/quality/testing/janitor/dist/cli.js filter-shard | tr '\n' ' ' | sed 's/ *$//')
|
||||
echo "specs=$FILTERED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Resolve shard args
|
||||
|
|
@ -195,13 +195,14 @@ jobs:
|
|||
with:
|
||||
name: ${{ inputs.artifact-prefix }}-shard-${{ matrix.shard }}
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/test-results.json
|
||||
packages/testing/playwright/playwright-report/
|
||||
packages/testing/playwright/coverage/lcov.info
|
||||
packages/testing/playwright/coverage/by-spec/
|
||||
packages/testing/playwright/coverage/index.html
|
||||
packages/testing/playwright/coverage/assets/
|
||||
packages/quality/*/test-results*
|
||||
packages/quality/*/playwright-report/
|
||||
packages/quality/*/*/test-results*
|
||||
packages/quality/*/*/playwright-report/
|
||||
packages/quality/testing/playwright/coverage/lcov.info
|
||||
packages/quality/testing/playwright/coverage/by-spec/
|
||||
packages/quality/testing/playwright/coverage/index.html
|
||||
packages/quality/testing/playwright/coverage/assets/
|
||||
retention-days: 1
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
|
@ -214,7 +215,7 @@ jobs:
|
|||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ inputs.artifact-prefix }}-a11y-report-shard-${{ matrix.shard }}
|
||||
path: packages/testing/playwright/a11y-report/
|
||||
path: packages/quality/testing/playwright/a11y-report/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
|
|
|||
2
.github/workflows/test-evals-instance-ai.yml
vendored
2
.github/workflows/test-evals-instance-ai.yml
vendored
|
|
@ -346,7 +346,7 @@ jobs:
|
|||
# ANTHROPIC_API_KEY is the provider-native key resolveEvalModelConfig prefers
|
||||
# when N8N_INSTANCE_AI_EVAL_MODEL differs from N8N_INSTANCE_AI_MODEL.
|
||||
MODEL_ARGS+=(
|
||||
-e N8N_INSTANCE_AI_EVAL_MODEL=anthropic/claude-sonnet-4-6
|
||||
-e N8N_INSTANCE_AI_EVAL_MODEL=anthropic/claude-sonnet-5-5
|
||||
-e ANTHROPIC_API_KEY="$EVALS_ANTHROPIC_KEY"
|
||||
)
|
||||
|
||||
|
|
|
|||
71
.github/workflows/test-image-efficiency.yml
vendored
Normal file
71
.github/workflows/test-image-efficiency.yml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
name: 'Test: Image Efficiency'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare-docker:
|
||||
uses: ./.github/workflows/prepare-docker-reusable.yml
|
||||
secrets:
|
||||
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
|
||||
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
|
||||
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
|
||||
|
||||
image-efficiency:
|
||||
name: Image Efficiency
|
||||
needs: prepare-docker
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-2vcpu-ubuntu-2204' }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
CURRENTS_RECORD_KEY: ${{ secrets.CURRENTS_RECORD_KEY }}
|
||||
CURRENTS_PROJECT_ID: 'nHHLA5'
|
||||
CURRENTS_CI_BUILD_ID: ${{ github.repository }}-${{ github.run_id }}-${{ github.run_attempt }}-images
|
||||
QA_METRICS_WEBHOOK_URL: ${{ secrets.QA_METRICS_WEBHOOK_URL }}
|
||||
QA_METRICS_WEBHOOK_USER: ${{ secrets.QA_METRICS_WEBHOOK_USER }}
|
||||
QA_METRICS_WEBHOOK_PASSWORD: ${{ secrets.QA_METRICS_WEBHOOK_PASSWORD }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: useblacksmith/checkout@bcec731f1eb1367240608d1c889a75b96db6ec53 # v1.5.0
|
||||
|
||||
- name: Setup Environment
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
build-command: pnpm turbo run build --filter=@n8n/utils...
|
||||
|
||||
- name: Load images from cache
|
||||
uses: ./.github/actions/load-n8n-docker
|
||||
|
||||
- name: Install Dive
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Keep this version in sync with efficiency/images/dive-results.ts.
|
||||
VERSION=0.13.1
|
||||
DIR=$(mktemp -d "$RUNNER_TEMP/dive.XXXXXX")
|
||||
ARCHIVE="$DIR/dive.tar.gz"
|
||||
curl --fail --location --retry 3 \
|
||||
"https://github.com/wagoodman/dive/releases/download/v${VERSION}/dive_${VERSION}_linux_amd64.tar.gz" \
|
||||
--output "$ARCHIVE"
|
||||
echo "0970549eb4a306f8825a84145a2534153badb4d7dcf3febd1967c706367c3d0e $ARCHIVE" | sha256sum --check
|
||||
tar -xzf "$ARCHIVE" -C "$DIR" dive
|
||||
echo "$DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Measure image layer efficiency
|
||||
run: >-
|
||||
pnpm --filter=@n8n/efficiency exec playwright test
|
||||
--project=image-efficiency:n8n --project=image-efficiency:runners
|
||||
|
||||
- name: Upload image efficiency reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: image-efficiency-${{ github.run_attempt }}
|
||||
path: |
|
||||
packages/quality/efficiency/test-results/
|
||||
packages/quality/efficiency/test-results.json
|
||||
packages/quality/efficiency/playwright-report/
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
10
.github/workflows/test-single-instance-npm.yml
vendored
10
.github/workflows/test-single-instance-npm.yml
vendored
|
|
@ -90,13 +90,13 @@ jobs:
|
|||
env:
|
||||
BASE_BRANCH: ${{ inputs.base-branch }}
|
||||
run: |
|
||||
if [ -f packages/testing/code-health/package.json ]; then
|
||||
if [ -f packages/quality/policy/code-health/package.json ]; then
|
||||
echo 'present=true' >> "$GITHUB_OUTPUT"
|
||||
elif [ -n "$BASE_BRANCH" ]; then
|
||||
echo 'present=false' >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::packages/testing/code-health is absent on \"$BASE_BRANCH\"; nothing to verify."
|
||||
echo "::notice::packages/quality/policy/code-health is absent on \"$BASE_BRANCH\"; nothing to verify."
|
||||
else
|
||||
echo '::error::packages/testing/code-health is missing (moved or renamed?).'
|
||||
echo '::error::packages/quality/policy/code-health is missing (moved or renamed?).'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -119,10 +119,10 @@ jobs:
|
|||
# REPORT_ONLY is a single flag or empty. Quoting it would pass an empty argument, which
|
||||
# the CLI reads as an (unknown) package name and rejects.
|
||||
# shellcheck disable=SC2086
|
||||
pnpm --dir packages/testing/code-health exec tsx src/cli.ts verify-npm-install --changed="$BASE_REF" $REPORT_ONLY
|
||||
pnpm --dir packages/quality/policy/code-health exec tsx src/cli.ts verify-npm-install --changed="$BASE_REF" $REPORT_ONLY
|
||||
|
||||
- name: Verify npm-install closure (all packages)
|
||||
if: steps.verifier.outputs.present == 'true' && inputs.scope == 'all'
|
||||
run: |
|
||||
# shellcheck disable=SC2086 # see the scoped step above
|
||||
pnpm --dir packages/testing/code-health exec tsx src/cli.ts verify-npm-install --all $REPORT_ONLY
|
||||
pnpm --dir packages/quality/policy/code-health exec tsx src/cli.ts verify-npm-install --all $REPORT_ONLY
|
||||
|
|
|
|||
110
.github/workflows/test-workflows-pr-comment.yml
vendored
110
.github/workflows/test-workflows-pr-comment.yml
vendored
|
|
@ -1,110 +0,0 @@
|
|||
name: 'Test: Workflows PR Comment'
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
handle_comment_command:
|
||||
name: Handle /test-workflows Command
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/test-workflows')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
permission_granted: ${{ steps.pr_check_and_details.outputs.permission_granted }}
|
||||
git_ref: ${{ steps.pr_check_and_details.outputs.head_sha }}
|
||||
pr_number: ${{ steps.pr_check_and_details.outputs.pr_number_string }}
|
||||
|
||||
steps:
|
||||
- name: Validate User, Get PR Details, and React
|
||||
id: pr_check_and_details
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const commenter = context.actor;
|
||||
const issueOwner = context.repo.owner;
|
||||
const issueRepo = context.repo.repo;
|
||||
const commentId = context.payload.comment.id;
|
||||
const prNumber = context.issue.number; // In issue_comment on a PR, issue.number is the PR number
|
||||
|
||||
// Function to add a reaction to the comment
|
||||
async function addReaction(content) {
|
||||
try {
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
comment_id: commentId,
|
||||
content: content
|
||||
});
|
||||
} catch (reactionError) {
|
||||
// Log if reaction fails but don't fail the script for this
|
||||
console.log(`Failed to add reaction '${content}': ${reactionError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize outputs to a non-triggering state
|
||||
core.setOutput('permission_granted', 'false');
|
||||
core.setOutput('head_sha', '');
|
||||
core.setOutput('pr_number_string', '');
|
||||
|
||||
// 1. Check user permissions
|
||||
try {
|
||||
const { data: permissions } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
username: commenter
|
||||
});
|
||||
|
||||
const allowedPermissions = ['admin', 'write', 'maintain'];
|
||||
if (!allowedPermissions.includes(permissions.permission)) {
|
||||
console.log(`User @${commenter} has '${permissions.permission}' permission. Needs 'admin', 'write', or 'maintain'.`);
|
||||
await addReaction('-1'); // User does not have permission
|
||||
return; // Exit script, tests will not be triggered
|
||||
}
|
||||
console.log(`User @${commenter} has '${permissions.permission}' permission.`);
|
||||
} catch (error) {
|
||||
console.log(`Could not verify permissions for @${commenter}: ${error.message}`);
|
||||
await addReaction('confused'); // Error checking permissions
|
||||
return; // Exit script
|
||||
}
|
||||
|
||||
// 2. Fetch PR details (if permission check passed)
|
||||
let headSha;
|
||||
try {
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner: issueOwner,
|
||||
repo: issueRepo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
headSha = pr.head.sha;
|
||||
console.log(`Workspaced PR details: SHA - ${headSha}, PR Number - ${prNumber}`);
|
||||
|
||||
// Set outputs for the next job
|
||||
core.setOutput('permission_granted', 'true');
|
||||
core.setOutput('head_sha', headSha);
|
||||
core.setOutput('pr_number_string', prNumber.toString());
|
||||
await addReaction('+1'); // Command accepted, tests will be triggered
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Failed to fetch PR details for PR #${prNumber}: ${error.message}`);
|
||||
core.setOutput('permission_granted', 'false'); // Ensure this is false if PR fetch fails
|
||||
await addReaction('confused'); // Error fetching PR details
|
||||
}
|
||||
|
||||
trigger_reusable_tests:
|
||||
name: Trigger Reusable Test Workflow
|
||||
needs: handle_comment_command
|
||||
|
||||
if: >
|
||||
always() &&
|
||||
needs.handle_comment_command.result != 'skipped' &&
|
||||
needs.handle_comment_command.outputs.permission_granted == 'true' &&
|
||||
needs.handle_comment_command.outputs.git_ref != ''
|
||||
uses: ./.github/workflows/test-workflows-callable.yml
|
||||
with:
|
||||
git_ref: ${{ needs.handle_comment_command.outputs.git_ref }}
|
||||
secrets: inherit
|
||||
2
.github/workflows/util-data-tooling.yml
vendored
2
.github/workflows/util-data-tooling.yml
vendored
|
|
@ -71,7 +71,7 @@ jobs:
|
|||
- name: Resolve Postgres image
|
||||
if: fromJSON(steps.paths-filter.outputs.results).db
|
||||
run: |
|
||||
IMAGE=$(node -p "require('./packages/testing/containers/postgres-versions.json').primary")
|
||||
IMAGE=$(node -p "require('./packages/quality/environments/containers/postgres-versions.json').primary")
|
||||
echo "TEST_IMAGE_POSTGRES=$IMAGE" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Start Postgres
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ jobs:
|
|||
approve-and-automerge:
|
||||
needs: [update-baseline]
|
||||
if: needs.update-baseline.outputs.pull-request-number != ''
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
# The callee's app credentials live on the release environment and cannot
|
||||
# be forwarded as named secrets by the caller.
|
||||
uses: ./.github/workflows/util-approve-and-set-automerge.yml # zizmor: ignore[secrets-inherit]
|
||||
|
|
|
|||
118
.github/workflows/util-update-get-n8n-versions.yml
vendored
Normal file
118
.github/workflows/util-update-get-n8n-versions.yml
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
name: 'Util: Update get-n8n Versions'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Wednesday at 04:00 UTC.
|
||||
- cron: '0 4 * * 3'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: update-get-n8n-versions
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update-versions:
|
||||
name: Update get-n8n versions
|
||||
if: >-
|
||||
github.ref_name == github.event.repository.default_branch &&
|
||||
(github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'schedule' && github.repository == 'n8n-io/n8n'))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Resolve stable versions
|
||||
id: versions
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
run: |
|
||||
n8n_version="$(gh api repos/n8n-io/n8n/releases/latest --jq '.tag_name | sub("^n8n@"; "")')"
|
||||
|
||||
registry_token="$(curl -fsSL --retry 3 'https://ghcr.io/token?scope=repository:n8n-io/n8n-sandbox-service-api:pull' | jq -er '.token')"
|
||||
stable_digest="$(docker buildx imagetools inspect \
|
||||
'ghcr.io/n8n-io/n8n-sandbox-service-api:stable' \
|
||||
--format '{{json .Manifest}}' | jq -er '.digest')"
|
||||
sandbox_version=''
|
||||
while IFS= read -r version; do
|
||||
version_digest="$(docker buildx imagetools inspect \
|
||||
"ghcr.io/n8n-io/n8n-sandbox-service-api:${version}" \
|
||||
--format '{{json .Manifest}}' | jq -er '.digest')"
|
||||
if [ "$version_digest" = "$stable_digest" ]; then
|
||||
sandbox_version="$version"
|
||||
break
|
||||
fi
|
||||
done < <(curl -fsSL --retry 3 \
|
||||
-H "Authorization: Bearer $registry_token" \
|
||||
'https://ghcr.io/v2/n8n-io/n8n-sandbox-service-api/tags/list?n=1000' |
|
||||
jq -r '.tags[]' |
|
||||
grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' |
|
||||
sort -Vr)
|
||||
|
||||
printf '%s\n' "$n8n_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
printf '%s\n' "$sandbox_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
|
||||
for image in api runner-dind sandbox; do
|
||||
docker manifest inspect "ghcr.io/n8n-io/n8n-sandbox-service-${image}:${sandbox_version}" >/dev/null
|
||||
done
|
||||
|
||||
sed -i "s/^FALLBACK_N8N_VERSION=.*/FALLBACK_N8N_VERSION=\"${n8n_version}\"/" docker/get-n8n.sh
|
||||
sed -i "s/^SANDBOX_VERSION=.*/SANDBOX_VERSION=\"${sandbox_version}\"/" docker/get-n8n.sh
|
||||
|
||||
echo "n8n_version=$n8n_version" >> "$GITHUB_OUTPUT"
|
||||
echo "sandbox_version=$sandbox_version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check for changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet -- docker/get-n8n.sh; then
|
||||
echo 'has-changes=false' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'has-changes=true' >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test installer upgrade
|
||||
if: steps.changes.outputs.has-changes == 'true'
|
||||
run: |
|
||||
shellcheck docker/get-n8n.sh docker/test-get-n8n.sh
|
||||
dash -n docker/get-n8n.sh
|
||||
dash -n docker/test-get-n8n.sh
|
||||
sh docker/test-get-n8n.sh --e2e
|
||||
|
||||
- name: Create pull request
|
||||
if: steps.changes.outputs.has-changes == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
add-paths: docker/get-n8n.sh
|
||||
commit-message: 'chore: Update get-n8n fallback versions'
|
||||
labels: 'automation:scheduled-update'
|
||||
title: 'chore: Update get-n8n fallback versions'
|
||||
body: |
|
||||
This automated PR updates the get-n8n fallback release to ${{ steps.versions.outputs.n8n_version }}.
|
||||
It also updates the sandbox service release to ${{ steps.versions.outputs.sandbox_version }}.
|
||||
|
||||
The workflow verified that all sandbox images use the same release. It also upgraded a legacy installation to these versions and checked the running stack.
|
||||
|
||||
_Generated by the weekly get-n8n version update workflow._
|
||||
branch: update-get-n8n-versions
|
||||
base: master
|
||||
delete-branch: true
|
||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
node_modules
|
||||
.build/
|
||||
.pnpm-store
|
||||
.DS_Store
|
||||
.tmp
|
||||
|
|
@ -8,6 +9,7 @@ dist
|
|||
coverage
|
||||
coverage-by-spec
|
||||
coverage-by-spec-backend
|
||||
/report
|
||||
npm-debug.log*
|
||||
yarn.lock
|
||||
google-generated-credentials.json
|
||||
|
|
@ -25,6 +27,7 @@ nodelinter.config.json
|
|||
!.github/scripts/package-lock.json
|
||||
packages/**/.turbo
|
||||
.turbo
|
||||
.build/
|
||||
.stryker-tmp/
|
||||
**/reports/mutation/
|
||||
*.tsbuildinfo
|
||||
|
|
@ -38,23 +41,25 @@ build.log
|
|||
tcr-dry.log
|
||||
.agent-setup/
|
||||
.agent-recordings/
|
||||
.vitest/
|
||||
.vitest-profile/
|
||||
.pi-subagents/
|
||||
sbom-source.cdx.json
|
||||
*.junit.xml
|
||||
junit.xml
|
||||
test-results.json
|
||||
*.0x
|
||||
packages/testing/playwright/playwright-report
|
||||
packages/testing/playwright/eval-report
|
||||
packages/testing/playwright/a11y-report
|
||||
packages/testing/playwright/test-results
|
||||
packages/testing/playwright/results.xml
|
||||
packages/testing/playwright/eval-results.json
|
||||
packages/quality/testing/playwright/playwright-report
|
||||
packages/quality/testing/playwright/eval-report
|
||||
packages/quality/testing/playwright/a11y-report
|
||||
packages/quality/testing/playwright/test-results
|
||||
packages/quality/testing/playwright/results.xml
|
||||
packages/quality/testing/playwright/eval-results.json
|
||||
packages/@n8n/instance-ai/eval-results.json
|
||||
packages/@n8n/instance-ai/.eval-output/
|
||||
packages/@n8n/instance-ai/eval-pr-comment.md
|
||||
packages/testing/playwright/.playwright-browsers
|
||||
packages/testing/playwright/.playwright-cli
|
||||
packages/quality/testing/playwright/.playwright-browsers
|
||||
packages/quality/testing/playwright/.playwright-cli
|
||||
test-results/
|
||||
compiled_app_output
|
||||
trivy_report*
|
||||
|
|
@ -63,7 +68,7 @@ docker-build-manifest.json
|
|||
packages/cli/src/modules/my-feature
|
||||
.secrets
|
||||
act-event.json
|
||||
packages/testing/**/.cursor/rules/
|
||||
packages/quality/**/.cursor/rules/
|
||||
.venv
|
||||
.ruff_cache
|
||||
__pycache__
|
||||
|
|
|
|||
26
.jscpd.json
Normal file
26
.jscpd.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"threshold": 10.5,
|
||||
"reporters": ["json", "html"],
|
||||
"gitignore": true,
|
||||
"ignore": [
|
||||
"**/__snapshots__/**",
|
||||
"**/*.snap",
|
||||
"**/fixtures/**",
|
||||
"**/locales/**",
|
||||
"**/i18n/**",
|
||||
"**/*.generated.*",
|
||||
"**/__schema__/**",
|
||||
"**/cassettes/**",
|
||||
"packages/@n8n/typeorm/test/**",
|
||||
"docs/generated/**",
|
||||
"**/test/**/*.workflow.json",
|
||||
"**/tests/**/*.workflow.json",
|
||||
"**/__tests__/**/*.workflow.json",
|
||||
"**/test/**/workflow.*.json",
|
||||
"**/tests/**/workflow.*.json",
|
||||
"**/__tests__/**/workflow.*.json",
|
||||
"**/test/**/workflows/**/*.json",
|
||||
"**/tests/**/workflows/**/*.json",
|
||||
"**/__tests__/**/workflows/**/*.json"
|
||||
]
|
||||
}
|
||||
|
|
@ -52,8 +52,8 @@ skip:
|
|||
# Uses merge commit SHA from GitHub - the code has already been reviewed
|
||||
# and merged, not arbitrary PR code.
|
||||
- .github/workflows/test-unit-reusable.yml
|
||||
# Permission-gated: only maintainers (admin/write/maintain) can trigger
|
||||
# via /test-workflows comment. Verified in test-workflows-pr-comment.yml.
|
||||
# Reachable only from test-workflows-nightly.yml, which is
|
||||
# workflow_dispatch-only, so the ref comes from a maintainer, not a PR.
|
||||
- .github/workflows/test-workflows-callable.yml
|
||||
# Reusable workflow reachable only via workflow_dispatch (manual) or from
|
||||
# ci-mcp-evals.yml, which is itself workflow_dispatch-only — never from a
|
||||
|
|
|
|||
60
AGENTS.md
60
AGENTS.md
|
|
@ -14,6 +14,11 @@ frontend, and extensible node-based workflow engine.
|
|||
- Write all technical text (code comments, PR descriptions, issue and ticket
|
||||
descriptions, docs) in ASD-STE100 Simplified Technical English: use short
|
||||
sentences, the active voice, and one instruction for each sentence
|
||||
- **JSDoc on `@Env(...)` config properties is public documentation.** The
|
||||
docs team exports these comments into the environment variable reference on
|
||||
docs.n8n.io. Write them for a self-hosting operator and follow the
|
||||
`n8n:env-var-docs` skill, which applies the n8n docs style guide and
|
||||
overrides the ASD-STE100 rule above for these comments
|
||||
- **Secrets on the command line:** if a developer opted into anonymous dev
|
||||
metrics (`scripts/dev-metrics`), pnpm command arguments are recorded. Arguments
|
||||
of secret-carrying words (`config`, `login`, `publish`, `token`) — whether a
|
||||
|
|
@ -35,6 +40,10 @@ frontend, and extensible node-based workflow engine.
|
|||
suggested by Linear, **unless it is a security fix** (see Security Fix
|
||||
Hygiene below)
|
||||
- Use mermaid diagrams in MD files when you need to visualise something
|
||||
- Write an ADR only when asked. When a change makes a decision that an
|
||||
engineer could challenge in 12 months, say so in one line and let the
|
||||
human decide. Read [docs/adr/README.md](docs/adr/README.md) before you
|
||||
write one. CI enforces its conventions
|
||||
- **Developing v3 features:** land normal feature work on `master` behind an
|
||||
opt-in flag; introduce breaking changes only on the `3.x` branch. See
|
||||
[.github/DEVELOPING_V3.md](.github/DEVELOPING_V3.md).
|
||||
|
|
@ -43,7 +52,8 @@ frontend, and extensible node-based workflow engine.
|
|||
comments keep the historical `n8nConnect` / `n8n credits` / AI Gateway names
|
||||
- **Shared utilities:** before you hand-roll a utility (`isRecord`, secret or
|
||||
PII redaction, JSON extraction from LLM output, Zod to JSON Schema, model-id
|
||||
parsing, …), you MUST check the shared packages for an existing
|
||||
parsing, AI client HTTP transport and response-size limiting, …), you MUST
|
||||
check the shared packages for an existing
|
||||
implementation and use it: `@n8n/utils` (generic helpers, redaction),
|
||||
`@n8n/ai-utilities` (AI- and LLM-specific helpers) and `n8n-workflow`
|
||||
(workflow graph and traversal). A new shared helper usually belongs in one of
|
||||
|
|
@ -64,10 +74,15 @@ are namespaced under `n8n:`. Use `n8n:` prefix when invoking them (e.g.
|
|||
|
||||
## Essential Commands
|
||||
|
||||
For full-repo lint and typecheck, use `pnpm agent:lint` and
|
||||
`pnpm agent:typecheck`. They save full logs and return compact results.
|
||||
For routine pre-commit checks, follow [Verify changes](#verify-changes).
|
||||
For focused tests, use `pnpm agent:test` or `pnpm agent:playwright`.
|
||||
Run each command with `--help` for its package, script, and file options.
|
||||
Pass test files and worker limits after `--`. Run each command with `--help`
|
||||
for its options.
|
||||
|
||||
`pnpm agent:lint` and `pnpm agent:typecheck` run full-repo checks by default.
|
||||
With `--filter <package>`, they still check the whole package. These wrappers
|
||||
save logs and reduce console output. They do not reduce CPU or memory use.
|
||||
Do not use them as routine pre-commit checks.
|
||||
|
||||
### Fresh checkout / agent setup
|
||||
|
||||
|
|
@ -214,12 +229,16 @@ const children = getChildNodes(workflow.connections, 'NodeName', 'main', 1);
|
|||
|
||||
### Persistence layer & the TypeORM boundary
|
||||
|
||||
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer** — the `@n8n/db`
|
||||
package or a backend module's own `database/` folder (entity/repository files).
|
||||
TypeORM (`@n8n/typeorm`) must stay in the **persistence layer**. The shared
|
||||
`@n8n/db` package and backend persistence adapters can import it. A module can
|
||||
colocate or group its entities and repositories according to its domain.
|
||||
Business logic — services, controllers, handlers, commands, factories — must not
|
||||
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). In
|
||||
`packages/cli` this is enforced by the `misplaced-n8n-typeorm-import` lint rule;
|
||||
a new import (or an inline `eslint-disable` of the rule) fails CI.
|
||||
import from `@n8n/typeorm` (including `@n8n/typeorm/...` subpaths). The
|
||||
`typeorm-persistence-boundary` Code Health rule recognizes entity and repository
|
||||
declarations. It scans backend packages that depend on `@n8n/db` or
|
||||
`@n8n/typeorm`. Tests, migrations, and explicit helper-only adapters are
|
||||
exceptions. Existing business-logic imports stay in the shrink-only Code Health
|
||||
baseline. A new business-logic import fails CI.
|
||||
|
||||
- **Pattern:** when a query needs operators (`In`, `IsNull`, `LessThan`,
|
||||
`FindOptionsWhere`, …), put it behind a **use-case-named repository method**
|
||||
|
|
@ -305,18 +324,29 @@ extends one of those layers):
|
|||
|
||||
### Verify changes
|
||||
|
||||
- Run focused tests from the owning package: `pnpm test <test-file>`.
|
||||
- Run that package's `pnpm lint` and `pnpm typecheck` before committing code.
|
||||
Build first when shared types or cross-package dependencies change.
|
||||
- Before committing, run formatting and lint checks on changed files. Use
|
||||
the package's configured tools through `pnpm exec` with explicit file paths.
|
||||
Do not append file paths to `pnpm lint`; package scripts can still check
|
||||
the whole package.
|
||||
- Run tests that cover the changed behavior from the owning package:
|
||||
`pnpm test <test-file> --maxWorkers=1`. Or use the root wrapper:
|
||||
`pnpm agent:test --filter <package> -- <test-file> --maxWorkers=1`.
|
||||
For Playwright, use `pnpm agent:playwright -- <spec-file> --workers=1`.
|
||||
- For routine commits, run checks one at a time. Use one test worker by
|
||||
default. Do not run full frontend typechecks locally. Use CI for
|
||||
comprehensive lint, typechecks, and test suites.
|
||||
- For documentation-only changes, review the diff and run `git diff --check`.
|
||||
Skip code tests when no behavior changed.
|
||||
- Report which checks ran and which checks were left to CI.
|
||||
- Use Vitest for unit tests. Use
|
||||
[Playwright](packages/testing/playwright/AGENTS.md) when a test needs its
|
||||
[Playwright](packages/quality/testing/playwright/AGENTS.md) when a test needs its
|
||||
browser, fixtures, or managed containers.
|
||||
- For Vitest packages with `@n8n/di` decorators, use
|
||||
`createVitestConfigWithDecorators` from `@n8n/vitest-config/node-decorators`.
|
||||
- Check import and mock side effects before running tests. Keep tests out of
|
||||
user-owned directories. Set `N8N_USER_FOLDER` to a test-owned directory before
|
||||
importing n8n settings. Clean up only paths that the test created.
|
||||
- CI runs [`@n8n/code-health`](packages/testing/code-health/README.md) static
|
||||
- CI runs [`@n8n/code-health`](packages/quality/policy/code-health/README.md) static
|
||||
analysis on PRs. It checks monorepo rules, including dependency hygiene and
|
||||
encryption-boundary coverage.
|
||||
|
||||
|
|
@ -330,7 +360,7 @@ extends one of those layers):
|
|||
| Start a Codespace backend and share its port | `pnpm dev:up` |
|
||||
|
||||
The root `pnpm dev` command does not start a server. See the
|
||||
[Playwright guide](packages/testing/playwright/README.md) and the
|
||||
[Playwright guide](packages/quality/testing/playwright/README.md) and the
|
||||
[Codespaces guide](.devcontainer/codespaces/README.md) for details.
|
||||
|
||||
### Common Development Tasks
|
||||
|
|
|
|||
302
CHANGELOG.md
302
CHANGELOG.md
|
|
@ -1,3 +1,305 @@
|
|||
# [2.44.0](https://github.com/n8n-io/n8n/compare/n8n@2.43.0...n8n@2.44.0) (2026-10-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Align execution retry menu and clarify actions ([#40710](https://github.com/n8n-io/n8n/issues/40710)) ([84d2fd6](https://github.com/n8n-io/n8n/commit/84d2fd6f3ae68e973aa49f7e2b1dd5dbf06ad18c))
|
||||
* Allow per-process crash journal paths ([#37670](https://github.com/n8n-io/n8n/issues/37670)) ([447e939](https://github.com/n8n-io/n8n/commit/447e939b732b778662572098d81f014527e3ffe2))
|
||||
* **core:** Allow reverting a workflow version that uses credentials the user cannot use ([#40701](https://github.com/n8n-io/n8n/issues/40701)) ([033a9b5](https://github.com/n8n-io/n8n/commit/033a9b512c2d52d05b43d4dd2d0e10634dc36e2a))
|
||||
* **core:** Apply node type policies to agent node tools ([#39711](https://github.com/n8n-io/n8n/issues/39711)) ([e7d8533](https://github.com/n8n-io/n8n/commit/e7d8533880df0d36438e39a07f0594fcaaaaab55))
|
||||
* **core:** Apply the credential type policy to credential tests and probes ([#39927](https://github.com/n8n-io/n8n/issues/39927)) ([9191cf3](https://github.com/n8n-io/n8n/commit/9191cf3fb07ce6bbb7b2217592ce56372b52f6c2))
|
||||
* **core:** Block the assistant when AI credits hit zero ([#40659](https://github.com/n8n-io/n8n/issues/40659)) ([8b7065a](https://github.com/n8n-io/n8n/commit/8b7065a87684bb3a055d8bcfc5d34c9b70094080))
|
||||
* **core:** Check the credential save policy before package import creates stubs ([#40687](https://github.com/n8n-io/n8n/issues/40687)) ([a3391ba](https://github.com/n8n-io/n8n/commit/a3391ba93c4574405d06e79e2efc791b6ba8a21f))
|
||||
* **core:** Check the target project's policy on folder, credential and delete-transfer moves ([#40301](https://github.com/n8n-io/n8n/issues/40301)) ([77370af](https://github.com/n8n-io/n8n/commit/77370af99a273073faf7df57a1fe8f71d3833bbe))
|
||||
* **core:** Count jobs as running from the start of job processing ([#40050](https://github.com/n8n-io/n8n/issues/40050)) ([dc5c80f](https://github.com/n8n-io/n8n/commit/dc5c80f26af8d32e5887ccbf7634f3da1be5f771))
|
||||
* **core:** Expose 1Password fields for secret preview and autocomplete ([#40538](https://github.com/n8n-io/n8n/issues/40538)) ([bef5f3f](https://github.com/n8n-io/n8n/commit/bef5f3f97aca163ac289af61e10869c0fa7fff60))
|
||||
* **core:** Fix Kimi K3 and other OpenAI-compatible Assistant model providers on self-hosted Assistant ([#40439](https://github.com/n8n-io/n8n/issues/40439)) ([1ef7756](https://github.com/n8n-io/n8n/commit/1ef77564e3bef2e34c57d8a67df892ace3858a5d))
|
||||
* **core:** Handle each Chat Hub segment once in queue mode ([#40791](https://github.com/n8n-io/n8n/issues/40791)) ([9ff8d4f](https://github.com/n8n-io/n8n/commit/9ff8d4fc738ca0e54db863c574e5a6d93df9ce2e))
|
||||
* **core:** Improve node placement when n8n Assistant edits a workflow ([#40139](https://github.com/n8n-io/n8n/issues/40139)) ([d900743](https://github.com/n8n-io/n8n/commit/d900743bce0cf8274c42e1f4a579fc8b5cf74997))
|
||||
* **core:** Keep waiting executions unchanged when a resume request fails ([#40683](https://github.com/n8n-io/n8n/issues/40683)) ([bbd35e2](https://github.com/n8n-io/n8n/commit/bbd35e281b100a36aa209518c5ab027eabf85a1f))
|
||||
* **core:** Let the editor download binary data of an Engine v2 execution ([#40663](https://github.com/n8n-io/n8n/issues/40663)) ([c9cb3ee](https://github.com/n8n-io/n8n/commit/c9cb3ee3abd23fe146cd4a2daed49c4bddb80d97))
|
||||
* **core:** Let the streaming heartbeat stop on Chat Hub responses ([#40792](https://github.com/n8n-io/n8n/issues/40792)) ([ee8a608](https://github.com/n8n-io/n8n/commit/ee8a608a22ec7a23dc795ee0554edd34186e87d2))
|
||||
* **core:** Match regex case-insensitively when ignore case is enabled ([#33589](https://github.com/n8n-io/n8n/issues/33589)) ([2596f3a](https://github.com/n8n-io/n8n/commit/2596f3ac1c657aa4d2ddf40430b9e5940fb9c1b0))
|
||||
* **core:** Match the Brazilian real symbol in the in-isolate currency regex ([#40893](https://github.com/n8n-io/n8n/issues/40893)) ([d3deffb](https://github.com/n8n-io/n8n/commit/d3deffbfa59d358614458f8b1ae2d56d118a9ca5))
|
||||
* **core:** Pass `storedAt` into `WorkflowExecute` at every callsite ([#40495](https://github.com/n8n-io/n8n/issues/40495)) ([5ab2187](https://github.com/n8n-io/n8n/commit/5ab21874169d44eb01a19d6f0ba19bb7a254d952))
|
||||
* **core:** Remove credentials of deleted projects on source control pull ([#40692](https://github.com/n8n-io/n8n/issues/40692)) ([978f810](https://github.com/n8n-io/n8n/commit/978f810bbef9e284b564db4897be7085257064b1))
|
||||
* **core:** Report approval tools with no tool connected in workflow validation ([#40561](https://github.com/n8n-io/n8n/issues/40561)) ([bc0a9cb](https://github.com/n8n-io/n8n/commit/bc0a9cbc112aaed2853f14c32157082f41ba42b4))
|
||||
* **core:** Report MCP server connection failures in verify_mcp_server ([#40559](https://github.com/n8n-io/n8n/issues/40559)) ([fbbbadc](https://github.com/n8n-io/n8n/commit/fbbbadc292791112e30101edee229e9d5f4dbc02))
|
||||
* **core:** Require workflow create scope for single node runs in n8n Assistant ([#40908](https://github.com/n8n-io/n8n/issues/40908)) ([27fcb96](https://github.com/n8n-io/n8n/commit/27fcb968fb711a9957eed48fa78c5adfa78fc607))
|
||||
* **core:** Resume tool calls for deferred tools that a skill loaded ([#40126](https://github.com/n8n-io/n8n/issues/40126)) ([1c84af2](https://github.com/n8n-io/n8n/commit/1c84af2e96bda41cd84852af82e5cae5b823f0f8))
|
||||
* **core:** Return jobs dequeued during worker shutdown to the queue ([#40086](https://github.com/n8n-io/n8n/issues/40086)) ([084f3b3](https://github.com/n8n-io/n8n/commit/084f3b376a0d64594320853bae90f13537d3b8c9))
|
||||
* **core:** Return jobs fetched before a worker paused to the queue ([#40162](https://github.com/n8n-io/n8n/issues/40162)) ([f26747b](https://github.com/n8n-io/n8n/commit/f26747bfc665ce0c137b0b7645245b1a97d1d01b))
|
||||
* **core:** Save agent draft definitions atomically ([#40374](https://github.com/n8n-io/n8n/issues/40374)) ([9ee5d1a](https://github.com/n8n-io/n8n/commit/9ee5d1ad7a54cde445ac945fc56a7ba5a4ee4707))
|
||||
* **core:** Serve the migration report on every main in a multi-main setup ([#40180](https://github.com/n8n-io/n8n/issues/40180)) ([a6f1e79](https://github.com/n8n-io/n8n/commit/a6f1e79394adced5958219f812e8ca14a10b9962))
|
||||
* **core:** Show the policy block before node issues on execute and publish ([#40290](https://github.com/n8n-io/n8n/issues/40290)) ([0bbea43](https://github.com/n8n-io/n8n/commit/0bbea438e1906a0773cdbb46e273b4c01fea359b))
|
||||
* **core:** Stabilise background child approval wait ([#40834](https://github.com/n8n-io/n8n/issues/40834)) ([2ca4e55](https://github.com/n8n-io/n8n/commit/2ca4e55502e03985d2c9b1b0033b6f5151ccb8f4))
|
||||
* **core:** Start a new test session when call_agent gets an unknown session ID ([#40458](https://github.com/n8n-io/n8n/issues/40458)) ([e16566b](https://github.com/n8n-io/n8n/commit/e16566b31324991a369708b7eb1f46027f80445b))
|
||||
* **core:** Stop marking the Message an Agent node as hidden ([#40358](https://github.com/n8n-io/n8n/issues/40358)) ([3366d2b](https://github.com/n8n-io/n8n/commit/3366d2b9b3151a769116a66499668c3a36cf65a4))
|
||||
* **core:** Validate execution timeout against the configured maximum in Instance AI saves ([#40769](https://github.com/n8n-io/n8n/issues/40769)) ([2176819](https://github.com/n8n-io/n8n/commit/2176819865a03e9247dac0a61177572ac4f7711c))
|
||||
* **core:** Validate the size argument of the chunk() expression extension ([#40880](https://github.com/n8n-io/n8n/issues/40880)) ([3ff9c83](https://github.com/n8n-io/n8n/commit/3ff9c83bdaf873509e4470f97e26919e0653add8))
|
||||
* **core:** Wire Entra OAuth2 into agent Azure OpenAI runtime ([#40218](https://github.com/n8n-io/n8n/issues/40218)) ([96a8ff4](https://github.com/n8n-io/n8n/commit/96a8ff4d15baa2ab15debc8ea02da81847f5a504))
|
||||
* **Databricks Node:** Request float encoding for embeddings from the Unity AI Gateway ([#40684](https://github.com/n8n-io/n8n/issues/40684)) ([4c0902f](https://github.com/n8n-io/n8n/commit/4c0902f6bf4a0aa6634d5598e75fc677aa55f429))
|
||||
* **editor:** Add resend to retryable Agent preview errors ([#40592](https://github.com/n8n-io/n8n/issues/40592)) ([f7e52a8](https://github.com/n8n-io/n8n/commit/f7e52a851ed9416615167826daf706d90a19d53b))
|
||||
* **editor:** Align nodes panel to the embedded canvas in the n8n Assistant split view ([#40795](https://github.com/n8n-io/n8n/issues/40795)) ([37799ad](https://github.com/n8n-io/n8n/commit/37799ad2f21067ec7951c2656c9e938dc87443e1))
|
||||
* **editor:** Align tooltip trigger box with its content ([#40422](https://github.com/n8n-io/n8n/issues/40422)) ([a694531](https://github.com/n8n-io/n8n/commit/a694531a0367bc6b7991b194ec762e7ad51aaa0a))
|
||||
* **editor:** Keep Agent Builder settings inside a narrow column ([#40759](https://github.com/n8n-io/n8n/issues/40759)) ([e2b4fee](https://github.com/n8n-io/n8n/commit/e2b4fee0ec0f0f34245664c23869153089362236))
|
||||
* **editor:** Keep Assistant fixes in Agent builder and correct tooltips ([#40348](https://github.com/n8n-io/n8n/issues/40348)) ([a13705a](https://github.com/n8n-io/n8n/commit/a13705aa8fff5a170ba5b069946e205e9acc1985))
|
||||
* **editor:** Keep folder creation available globally after Community registration ([#40082](https://github.com/n8n-io/n8n/issues/40082)) ([e1025f3](https://github.com/n8n-io/n8n/commit/e1025f35e4203b49436aedb89dcb679e96169528))
|
||||
* **editor:** Keep same-named parameter values apart when their types differ ([#40170](https://github.com/n8n-io/n8n/issues/40170)) ([6013045](https://github.com/n8n-io/n8n/commit/601304543d2b87e6aa42211c3a8c6c2766c2c61d))
|
||||
* **editor:** Keep the prompt visible while the assistant starts a new thread ([#40796](https://github.com/n8n-io/n8n/issues/40796)) ([be402be](https://github.com/n8n-io/n8n/commit/be402be262974d46f79ba3464e20efc0ee734ac4))
|
||||
* **editor:** Keep the selected credential visible when the picker does not list it ([#40450](https://github.com/n8n-io/n8n/issues/40450)) ([66b382d](https://github.com/n8n-io/n8n/commit/66b382de52fb56db87987910929e932292837305))
|
||||
* **editor:** Keep the workflow setup step counter on one line ([#40771](https://github.com/n8n-io/n8n/issues/40771)) ([55eb2ce](https://github.com/n8n-io/n8n/commit/55eb2ce44291648adf6ca05b5b1605efea8d77f7))
|
||||
* **editor:** Keep typed characters in Markdown fields ([#40805](https://github.com/n8n-io/n8n/issues/40805)) ([16e29da](https://github.com/n8n-io/n8n/commit/16e29da52f3173ee5edc7b96697a4c227b4983d0))
|
||||
* **editor:** Load node types before authenticated pages render ([#40812](https://github.com/n8n-io/n8n/issues/40812)) ([8297b2e](https://github.com/n8n-io/n8n/commit/8297b2e6d263d63571926545bf8b5c490f5125bf))
|
||||
* **editor:** Place the restricted node popover per host, keep one open, and stack the admin dialog ([#40678](https://github.com/n8n-io/n8n/issues/40678)) ([b28dceb](https://github.com/n8n-io/n8n/commit/b28dceb462d8e51855d4e13a87e9c975638ef977))
|
||||
* **editor:** Polish n8n Assistant and Execute button UI details ([#40414](https://github.com/n8n-io/n8n/issues/40414)) ([76cab1e](https://github.com/n8n-io/n8n/commit/76cab1e26d97cbbf50790926f1bbb14cfb12d078))
|
||||
* **editor:** Preserve Live Data highlights on scroll in expression editor ([#33842](https://github.com/n8n-io/n8n/issues/33842)) ([0f02b50](https://github.com/n8n-io/n8n/commit/0f02b509be304523955e7700c7c9eeefe58d0aa9))
|
||||
* **editor:** Remove redundant description for create workflow option in agent config ([#40882](https://github.com/n8n-io/n8n/issues/40882)) ([b542f66](https://github.com/n8n-io/n8n/commit/b542f6611998c23b40b54d25d1e923841ee24160))
|
||||
* **editor:** Rename the "Executions fail" label to "Executions will fail" ([#40888](https://github.com/n8n-io/n8n/issues/40888)) ([2a51f38](https://github.com/n8n-io/n8n/commit/2a51f38d55f37f65382ebf3900a340fc62b15314))
|
||||
* **editor:** Respect the default voice for chat read aloud ([#40440](https://github.com/n8n-io/n8n/issues/40440)) ([022b239](https://github.com/n8n-io/n8n/commit/022b23945ba576024685bfe1702489f427a3c9fe))
|
||||
* **editor:** Shorten the share button label on the credential sharing tab ([#40637](https://github.com/n8n-io/n8n/issues/40637)) ([c133d6c](https://github.com/n8n-io/n8n/commit/c133d6c893a879c907d20bb71a10817d358db553))
|
||||
* **editor:** Show the Agent artifact while the builder creates it ([#40535](https://github.com/n8n-io/n8n/issues/40535)) ([1c63119](https://github.com/n8n-io/n8n/commit/1c631197563b8f65d343d060355050c4551f381d))
|
||||
* **editor:** Show the owner and credential type for credentials the user cannot use ([#40669](https://github.com/n8n-io/n8n/issues/40669)) ([63dc285](https://github.com/n8n-io/n8n/commit/63dc2854936f3e282ca422cd9b9babeab65a2375))
|
||||
* **editor:** Show the renamed agent name in the assistant artifact tab ([#40770](https://github.com/n8n-io/n8n/issues/40770)) ([4032797](https://github.com/n8n-io/n8n/commit/40327977903613fee5d033efd31c6e01416a2bf0))
|
||||
* **editor:** Show the unavailable credential at the top of the node picker ([#40693](https://github.com/n8n-io/n8n/issues/40693)) ([1933723](https://github.com/n8n-io/n8n/commit/19337237ec59bd03c89e300a23289d2bdfbf269e))
|
||||
* **editor:** Show why a node on another branch can't be referenced ([#40565](https://github.com/n8n-io/n8n/issues/40565)) ([a195096](https://github.com/n8n-io/n8n/commit/a19509673336fbbf6687b107ce0af5bcfec68393))
|
||||
* **editor:** Small UI fixes to budget guardrail setup components ([#40383](https://github.com/n8n-io/n8n/issues/40383)) ([958d23d](https://github.com/n8n-io/n8n/commit/958d23d81cd99d7b9fd63d2484b79382c2d20c23))
|
||||
* **editor:** Use pulsing status dot for the enabled n8n Assistant setting ([#40712](https://github.com/n8n-io/n8n/issues/40712)) ([3ba5229](https://github.com/n8n-io/n8n/commit/3ba522910b1cc3009c108a202634388fd838717f))
|
||||
* **Google Gemini Chat Model Node:** Point the AI builder to the model catalog for new Gemini models ([#40159](https://github.com/n8n-io/n8n/issues/40159)) ([c21ed4f](https://github.com/n8n-io/n8n/commit/c21ed4f13aee6a1551207a92122bea2859e1b8d3))
|
||||
* **Invoice Ninja Node:** Fix retrieving currencies for bank transactions ([#12997](https://github.com/n8n-io/n8n/issues/12997)) ([00849c4](https://github.com/n8n-io/n8n/commit/00849c4deabada87641c146773c0981e873a6328))
|
||||
* Keep execution details clear of preview canvas ([#40176](https://github.com/n8n-io/n8n/issues/40176)) ([af98327](https://github.com/n8n-io/n8n/commit/af98327a5e4f2332b78b4d460e6eb208f0782d28))
|
||||
* Load only the publish history that the editor shows ([#40183](https://github.com/n8n-io/n8n/issues/40183)) ([b28adf9](https://github.com/n8n-io/n8n/commit/b28adf97895bbdba2e0c777bdfb5cd11d04c0e56))
|
||||
* **Microsoft Outlook Trigger Node:** Report failed polls and limit the backlog per poll ([#40147](https://github.com/n8n-io/n8n/issues/40147)) ([6bc5c93](https://github.com/n8n-io/n8n/commit/6bc5c93513efbe4d236c5794cfd07e1ac4d916f6))
|
||||
* Multiple migration timestamps withing a single PR ([#40379](https://github.com/n8n-io/n8n/issues/40379)) ([7db7fa4](https://github.com/n8n-io/n8n/commit/7db7fa42c80a8a24fb5ca5ec7448ffb12431fad9))
|
||||
* **Odoo Node:** Normalize datetime fields to Odoo format before write ([#34186](https://github.com/n8n-io/n8n/issues/34186)) ([6b77613](https://github.com/n8n-io/n8n/commit/6b77613ee827560d9f34f551478a74daf4848dec))
|
||||
* Prevent node types corruption when multiple loaders run at the same time ([#40633](https://github.com/n8n-io/n8n/issues/40633)) ([fdc3948](https://github.com/n8n-io/n8n/commit/fdc394819cecf96379ed7ba475364912099a6296))
|
||||
* Remove loading flashes from data tables ([#39874](https://github.com/n8n-io/n8n/issues/39874)) ([2f4a386](https://github.com/n8n-io/n8n/commit/2f4a3868fbd74c07e91888eb35b1f3d4bcb2acb1))
|
||||
* Report resource-operation violations as errors ([#40830](https://github.com/n8n-io/n8n/issues/40830)) ([a180010](https://github.com/n8n-io/n8n/commit/a1800105ef8f7c229ffd044e1233d273e95b6cd2))
|
||||
* Return UTC ISO dates without changing other cells ([#40156](https://github.com/n8n-io/n8n/issues/40156)) ([8059b63](https://github.com/n8n-io/n8n/commit/8059b63da89ba89309cbc6eacce715110c9c5748))
|
||||
* **S3 Node:** Percent-encode the copy source path ([#40282](https://github.com/n8n-io/n8n/issues/40282)) ([81f34c2](https://github.com/n8n-io/n8n/commit/81f34c23744424d17ca99eaf03beb9861c21a1ca))
|
||||
* Separate agent response steps and show saved reasoning ([#40590](https://github.com/n8n-io/n8n/issues/40590)) ([1e6da4e](https://github.com/n8n-io/n8n/commit/1e6da4e615900d7665e6eeafe9ce6002d1510c31))
|
||||
* Update matching nodes when selecting imported credentials ([#39650](https://github.com/n8n-io/n8n/issues/39650)) ([4b5b68e](https://github.com/n8n-io/n8n/commit/4b5b68ee2929344288b93469efe732379e3904a1))
|
||||
* Use configured OAuth server rate limits ([#40542](https://github.com/n8n-io/n8n/issues/40542)) ([4c8b76b](https://github.com/n8n-io/n8n/commit/4c8b76b6f08d090bf6e7cf22f66d60ec3d0efdc6))
|
||||
* Use ID token claims when UserInfo is unavailable ([#40779](https://github.com/n8n-io/n8n/issues/40779)) ([b8b0387](https://github.com/n8n-io/n8n/commit/b8b0387a31b556e296529235f9c4a75b307bb8f1))
|
||||
* Validate and default public API pagination limits ([#40483](https://github.com/n8n-io/n8n/issues/40483)) ([f212d5b](https://github.com/n8n-io/n8n/commit/f212d5b900fe92c3012001b32f4cf17bd831921e))
|
||||
* Wait for MockServer HTTP status without following logs ([#40846](https://github.com/n8n-io/n8n/issues/40846)) ([4593838](https://github.com/n8n-io/n8n/commit/45938389b86b6bfe25a8a8f3d132b600ee807f58))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add Antigravity to the MCP client connection dialog ([#40879](https://github.com/n8n-io/n8n/issues/40879)) ([f0a38cf](https://github.com/n8n-io/n8n/commit/f0a38cfe5801ba8d62b97f6d4ffeb031f23aa231))
|
||||
* Add Google Vertex AI credentials ([#40665](https://github.com/n8n-io/n8n/issues/40665)) ([a7cba90](https://github.com/n8n-io/n8n/commit/a7cba90a854ea34198dd9c1fcb4f7eeae08c364d))
|
||||
* **ai-builder:** Reopen closed AI Assistant tabs when the agent changes the artifact ([#40081](https://github.com/n8n-io/n8n/issues/40081)) ([5d38667](https://github.com/n8n-io/n8n/commit/5d3866775c828e766ca16642d5ae2a340d4133ca))
|
||||
* **Azure AI Foundry Chat Model Node:** Speak the Anthropic Messages schema for Claude deployments ([#40306](https://github.com/n8n-io/n8n/issues/40306)) ([c134edf](https://github.com/n8n-io/n8n/commit/c134edf473276ff63387d28aea0d08c9ff676f7c))
|
||||
* **Azure OpenAI Chat Model Node:** Rename the Foundry nodes and credentials to Microsoft Foundry ([#40398](https://github.com/n8n-io/n8n/issues/40398)) ([e5c8a8b](https://github.com/n8n-io/n8n/commit/e5c8a8bb62d2ce468d1cda3965562eeaa69996f8))
|
||||
* Configure workflow tool inputs for Agents ([#40285](https://github.com/n8n-io/n8n/issues/40285)) ([4cb98ae](https://github.com/n8n-io/n8n/commit/4cb98aea9fbcede8ae414c3f817c7b0490d401f8))
|
||||
* **core:** Add credentialTransfer policy enforcement point ([#40300](https://github.com/n8n-io/n8n/issues/40300)) ([c2f6920](https://github.com/n8n-io/n8n/commit/c2f6920794e66e7108b0005233506f7694a11a7e))
|
||||
* **core:** Add execution identity to every OTel span ([#40649](https://github.com/n8n-io/n8n/issues/40649)) ([22ba4e7](https://github.com/n8n-io/n8n/commit/22ba4e7bc5c6fbe5243ff5c227c718ab7197c5e1))
|
||||
* **core:** Add overwrite schema conflict policy to data table package import ([#40345](https://github.com/n8n-io/n8n/issues/40345)) ([3af9817](https://github.com/n8n-io/n8n/commit/3af98170d0d98a201df1a45f2ecee17f5f00f298))
|
||||
* **core:** Add overwrite-non-destructive schema conflict policy to data table package import ([#40446](https://github.com/n8n-io/n8n/issues/40446)) ([dc33c29](https://github.com/n8n-io/n8n/commit/dc33c29d1af3f47a6179a76ff21ccc7bd1f75357))
|
||||
* **core:** Add Prometheus metrics for database connection recovery ([#39890](https://github.com/n8n-io/n8n/issues/39890)) ([d904e7a](https://github.com/n8n-io/n8n/commit/d904e7a2476794acdf3928faacd86ae9b3e45220))
|
||||
* **core:** Add the WhatsApp agent channel ([#38750](https://github.com/n8n-io/n8n/issues/38750)) ([b0d1a30](https://github.com/n8n-io/n8n/commit/b0d1a30f50fdf6b3720c2a5773e28efff5c5b0b5))
|
||||
* **core:** Add workflow suggestion review actions ([#39858](https://github.com/n8n-io/n8n/issues/39858)) ([eaf8c25](https://github.com/n8n-io/n8n/commit/eaf8c25d1c2b680fe55f58fc9f6d10647e6ae640))
|
||||
* **core:** Allow confirming destructive data table changes when continuing an apply ([#40491](https://github.com/n8n-io/n8n/issues/40491)) ([9946cfe](https://github.com/n8n-io/n8n/commit/9946cfe901b538ec5ca29e85cde3ec2044d33d00))
|
||||
* **core:** Apply the member credential rule to Owners and admins in team projects ([#40448](https://github.com/n8n-io/n8n/issues/40448)) ([3cff8dc](https://github.com/n8n-io/n8n/commit/3cff8dc4ecac5fd173d198f3e319ab473cdd7b22))
|
||||
* **core:** Block create/update of workflows with deprecated nodes ([#31360](https://github.com/n8n-io/n8n/issues/31360)) ([ea8417e](https://github.com/n8n-io/n8n/commit/ea8417e29ad6f8b981d9a04b17283d5d77f2b5bb))
|
||||
* **core:** Email the agent owner when a monthly budget alert is crossed ([#40544](https://github.com/n8n-io/n8n/issues/40544)) ([19e1f70](https://github.com/n8n-io/n8n/commit/19e1f70ea62ad151abe18b5a25721caa35d7bedd))
|
||||
* **core:** Emit opt-in start marker spans for workflow and node executions ([#40762](https://github.com/n8n-io/n8n/issues/40762)) ([c713545](https://github.com/n8n-io/n8n/commit/c7135451ebd552267aedf4898d845c53a97aab54))
|
||||
* **core:** Experiment to show assistant composer and starter templates in agent list ([#40396](https://github.com/n8n-io/n8n/issues/40396)) ([fe26ba8](https://github.com/n8n-io/n8n/commit/fe26ba8d3a63963d6c7868338e73fb77378e94a9))
|
||||
* **core:** Handle inbound media handling for whatsapp ([#39409](https://github.com/n8n-io/n8n/issues/39409)) ([50c2ca0](https://github.com/n8n-io/n8n/commit/50c2ca0a27189136d82339bdafd5d8c7513a9916))
|
||||
* **core:** Handle message quality rate limits for whatsapp integration ([#39205](https://github.com/n8n-io/n8n/issues/39205)) ([3c33856](https://github.com/n8n-io/n8n/commit/3c33856997b084a3a87038c0d72f80d9b91af1b7))
|
||||
* **core:** Hide credential-only nodes under a credential type policy ([#39652](https://github.com/n8n-io/n8n/issues/39652)) ([370b30f](https://github.com/n8n-io/n8n/commit/370b30f9195ba8d5ceaf75fb4cdfcf127105a3c6))
|
||||
* **core:** Pair data table columns by id on package import ([#40709](https://github.com/n8n-io/n8n/issues/40709)) ([27b43de](https://github.com/n8n-io/n8n/commit/27b43de85f987ddd783e7c7ccbde7977d09a6d03))
|
||||
* **core:** Persist self-healing results and handoffs ([#40115](https://github.com/n8n-io/n8n/issues/40115)) ([168a0ac](https://github.com/n8n-io/n8n/commit/168a0ac86056afd743bfde63863abbab4e8515f4))
|
||||
* **core:** Post a budget-stop message on external agent channels ([#40767](https://github.com/n8n-io/n8n/issues/40767)) ([5e01906](https://github.com/n8n-io/n8n/commit/5e01906a1e6c177c0ce02d18317ce2d7b6ee7e97))
|
||||
* **core:** Promote folder and project renames alongside selected workflows ([#40502](https://github.com/n8n-io/n8n/issues/40502)) ([a249e20](https://github.com/n8n-io/n8n/commit/a249e202905f30829fe3a7eacd2eba8dff6573ef))
|
||||
* **core:** Report crashed executions from startup log recovery ([#40390](https://github.com/n8n-io/n8n/issues/40390)) ([668e71d](https://github.com/n8n-io/n8n/commit/668e71d54a26da7c12d092a7375a7cb3349a8471))
|
||||
* **core:** Report destructive data table changes in the promotion apply preflight ([#40454](https://github.com/n8n-io/n8n/issues/40454)) ([5d208c8](https://github.com/n8n-io/n8n/commit/5d208c85ed275c22cc5597124b7a9cf590d18f11))
|
||||
* **core:** Report the deployment artifact in telemetry ([#40319](https://github.com/n8n-io/n8n/issues/40319)) ([344dea5](https://github.com/n8n-io/n8n/commit/344dea5c77fc5f61ce10171a2553faa9bef4e609))
|
||||
* **core:** Run the workflow history trim as a durable daily system task ([#40085](https://github.com/n8n-io/n8n/issues/40085)) ([38eea65](https://github.com/n8n-io/n8n/commit/38eea65993339ea3e8156cdbacef247749331351))
|
||||
* **core:** Search, create and reopen agents from the command bar ([#40504](https://github.com/n8n-io/n8n/issues/40504)) ([75a9cc7](https://github.com/n8n-io/n8n/commit/75a9cc7e1219d35b441b02c20446dd89db186ff7))
|
||||
* **core:** Serve Prometheus metrics while the database is unavailable ([#39908](https://github.com/n8n-io/n8n/issues/39908)) ([4e217ae](https://github.com/n8n-io/n8n/commit/4e217aeac6b029692fb8a692a3849ae8e5310b5a))
|
||||
* **core:** Serve the instance CSP on all non-script responses ([#40205](https://github.com/n8n-io/n8n/issues/40205)) ([cabf0bf](https://github.com/n8n-io/n8n/commit/cabf0bf3b466a725deada3b532347b7db530dcac))
|
||||
* **core:** Show every user the migration report, scoped to the workflows they can edit ([#40449](https://github.com/n8n-io/n8n/issues/40449)) ([f4f7dcf](https://github.com/n8n-io/n8n/commit/f4f7dcf13b6d93d46a2860e2c2577f9f6dda4c52))
|
||||
* **core:** Show the agent chat attach button on the left of the composer ([#40320](https://github.com/n8n-io/n8n/issues/40320)) ([209adf9](https://github.com/n8n-io/n8n/commit/209adf9a7ee9e71cfe3d5119316ca9fce4b3ac68))
|
||||
* **Databricks Node:** Guard the Vector Store insert against the wrong index ([#39699](https://github.com/n8n-io/n8n/issues/39699)) ([0932e95](https://github.com/n8n-io/n8n/commit/0932e9572e2650165b260fc591d1f02204c17aae))
|
||||
* **editor:** Add a state column to the migration rule detail ([#40388](https://github.com/n8n-io/n8n/issues/40388)) ([682d092](https://github.com/n8n-io/n8n/commit/682d0920ba5cb2ab5e3ac0671f993c8e529e8389))
|
||||
* **editor:** Add agent setup checklist ([#39919](https://github.com/n8n-io/n8n/issues/39919)) ([883ade4](https://github.com/n8n-io/n8n/commit/883ade4b9d2505f359057eb10c8d71ab153033a7))
|
||||
* **editor:** Add order to settings sidebar links ([#40505](https://github.com/n8n-io/n8n/issues/40505)) ([cef87cf](https://github.com/n8n-io/n8n/commit/cef87cfcab3f59a657c8c27309f4807bd2d4a93a))
|
||||
* **editor:** Add project selection to Vertex AI credentials ([#40667](https://github.com/n8n-io/n8n/issues/40667)) ([cdc23c6](https://github.com/n8n-io/n8n/commit/cdc23c666ffd6ed55f019aa24c99efdaf10fad14))
|
||||
* **editor:** Animate the n8n Assistant activity indicator ([#40497](https://github.com/n8n-io/n8n/issues/40497)) ([04e1e7f](https://github.com/n8n-io/n8n/commit/04e1e7fc4771ff1371ac41f1d74aef7ced26b175))
|
||||
* **editor:** Disable restricted model providers in the agent model selector ([#40658](https://github.com/n8n-io/n8n/issues/40658)) ([cde7a9d](https://github.com/n8n-io/n8n/commit/cde7a9dbd985ae9e83de2d39dccc508a41d9a676))
|
||||
* **editor:** Explain a restricted model provider with the restriction popover ([#40705](https://github.com/n8n-io/n8n/issues/40705)) ([75de193](https://github.com/n8n-io/n8n/commit/75de193d9090d160dd191667adb85dc423951045))
|
||||
* **editor:** Explain instance-role access in the credential sharing "Used in" row ([#40555](https://github.com/n8n-io/n8n/issues/40555)) ([48f05f9](https://github.com/n8n-io/n8n/commit/48f05f92fe9e7bd1c1d220a53a8c17055a17e70c))
|
||||
* **editor:** Rebuild the command bar with type tabs and paged search ([#40461](https://github.com/n8n-io/n8n/issues/40461)) ([e36093b](https://github.com/n8n-io/n8n/commit/e36093b11db0580add10306d27d090a27790f4fb))
|
||||
* **editor:** Rebuild the personal settings page with the design-system settings components ([#40298](https://github.com/n8n-io/n8n/issues/40298)) ([09436b7](https://github.com/n8n-io/n8n/commit/09436b78f9f6f320073f911ca11160ad9a66d94f))
|
||||
* **editor:** Recommend Claude Opus 5.5 and GPT-6 models in AI Assistant setup ([#40660](https://github.com/n8n-io/n8n/issues/40660)) ([1ad3c54](https://github.com/n8n-io/n8n/commit/1ad3c54803569dd83976c055785b51b62555bf44))
|
||||
* **editor:** Replace Windsurf with Devin in the MCP client dialog ([#40912](https://github.com/n8n-io/n8n/issues/40912)) ([e2c4054](https://github.com/n8n-io/n8n/commit/e2c4054cba14a2f1dc29215fc623f6ae42b7a238))
|
||||
* **editor:** Show the restriction callout and lock the agent tool configuration form ([#40657](https://github.com/n8n-io/n8n/issues/40657)) ([01da889](https://github.com/n8n-io/n8n/commit/01da88958d490e5006874cce2b05554347301b9b))
|
||||
* **editor:** Show who created each preference on the Preferences page ([#40868](https://github.com/n8n-io/n8n/issues/40868)) ([22ff2a0](https://github.com/n8n-io/n8n/commit/22ff2a06368b49fa25b0ad1c2cf74640c56a0284))
|
||||
* **editor:** Sort the credential sharing list by people, then projects ([#40478](https://github.com/n8n-io/n8n/issues/40478)) ([02af7e7](https://github.com/n8n-io/n8n/commit/02af7e7659cce85749c719bb6a1b910043357baf))
|
||||
* **Embeddings Azure OpenAI Node:** Add Entra ID authentication ([#39390](https://github.com/n8n-io/n8n/issues/39390)) ([830486a](https://github.com/n8n-io/n8n/commit/830486a851ceb3eeada1eb945e7cd11c05262901))
|
||||
* **Google Gemini Node:** Show Nano Banana 2.1 and 2 Lite in the image model list ([#40596](https://github.com/n8n-io/n8n/issues/40596)) ([97c54f6](https://github.com/n8n-io/n8n/commit/97c54f618c6210bbe1dcfadaac702d40e89a820b))
|
||||
* Make health check timeout configurable ([#39548](https://github.com/n8n-io/n8n/issues/39548)) ([df8605e](https://github.com/n8n-io/n8n/commit/df8605e8750443d66388060212306cd205a4f5d3))
|
||||
* **Microsoft SharePoint Trigger Node:** Add the Microsoft SharePoint Trigger node ([#40055](https://github.com/n8n-io/n8n/issues/40055)) ([7c126fa](https://github.com/n8n-io/n8n/commit/7c126fab32772241473e1f180c6806af4ce189f8))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** Keep agent builder system prompt static for prompt caching ([#40364](https://github.com/n8n-io/n8n/issues/40364)) ([ee1db0b](https://github.com/n8n-io/n8n/commit/ee1db0bc12bebb572afd04e6e53efc30ffa01209))
|
||||
* **core:** Let the agent builder chain config writes without re-reading the config ([#40462](https://github.com/n8n-io/n8n/issues/40462)) ([6b4da93](https://github.com/n8n-io/n8n/commit/6b4da93dcbb909bcf86c03251e3c74033d0dd70f))
|
||||
* **core:** Let the agent builder edit skill instructions in place ([#40447](https://github.com/n8n-io/n8n/issues/40447)) ([1de45a1](https://github.com/n8n-io/n8n/commit/1de45a1a9b0b0db7036cb25e27bffc95a7df41e2))
|
||||
* **core:** Prepare Instance AI skill sandbox in the background ([#40363](https://github.com/n8n-io/n8n/issues/40363)) ([b36c2b5](https://github.com/n8n-io/n8n/commit/b36c2b506b92f391f859ec10067e57d96c8b09c8))
|
||||
* **core:** Send agent builder config writes as objects with changed fields only ([#40666](https://github.com/n8n-io/n8n/issues/40666)) ([2aa3fbf](https://github.com/n8n-io/n8n/commit/2aa3fbf18547cfb6719baabdcd8719fa6d0f53af))
|
||||
|
||||
|
||||
# [2.43.0](https://github.com/n8n-io/n8n/compare/n8n@2.42.0...n8n@2.43.0) (2026-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Agent Node:** Remove outdated tutorial tip callout ([#35505](https://github.com/n8n-io/n8n/issues/35505)) ([53b84d2](https://github.com/n8n-io/n8n/commit/53b84d2baac4f6975627f85ff3d550d84008e749))
|
||||
* **ai-builder:** Treat a blank folder path as no folder in the build tool ([#40116](https://github.com/n8n-io/n8n/issues/40116)) ([b7071c9](https://github.com/n8n-io/n8n/commit/b7071c91c7ef959e1cc3d703495e4fee9fcc9744))
|
||||
* Align From AI button with parameter inputs ([#40152](https://github.com/n8n-io/n8n/issues/40152)) ([9675fc9](https://github.com/n8n-io/n8n/commit/9675fc97fd193c0474b36c084bd3d6e928885fed))
|
||||
* **API:** Emit valid schemas for untyped values in the Public API spec ([#39884](https://github.com/n8n-io/n8n/issues/39884)) ([6918fda](https://github.com/n8n-io/n8n/commit/6918fda0182e55e41efa847a8e6831d08cb3085d))
|
||||
* **API:** Let global members grant variable scopes to API keys ([#40064](https://github.com/n8n-io/n8n/issues/40064)) ([c95e34f](https://github.com/n8n-io/n8n/commit/c95e34f97af49af5d21bf544faacf92e87fe6319))
|
||||
* **API:** Return an execution when its stored trace context is incomplete ([#39715](https://github.com/n8n-io/n8n/issues/39715)) ([dfd7a52](https://github.com/n8n-io/n8n/commit/dfd7a522244720ca536e73b8cb3da7802a850a48))
|
||||
* **Azure OpenAI Chat Model Node:** Support Responses-only deployments ([#39403](https://github.com/n8n-io/n8n/issues/39403)) ([6610987](https://github.com/n8n-io/n8n/commit/66109870570bf195e70bd2ed9c56483945ca6ffd))
|
||||
* **Brevo Node:** Accept both spellings of the recipients parameter ([#40015](https://github.com/n8n-io/n8n/issues/40015)) ([e144b33](https://github.com/n8n-io/n8n/commit/e144b337de3e1f7a54d653c4ae8eb6dedab94c5b))
|
||||
* Center pasted nodes in the canvas viewport ([#39995](https://github.com/n8n-io/n8n/issues/39995)) ([bd9e1d8](https://github.com/n8n-io/n8n/commit/bd9e1d8947cb5a4fd187fa6bbf93c6bf0ff1116f))
|
||||
* Clear connection hover promptly ([#39958](https://github.com/n8n-io/n8n/issues/39958)) ([fa93a7f](https://github.com/n8n-io/n8n/commit/fa93a7f01f0b405f060b3f3c0c43b524e4d2855f))
|
||||
* **core:** Apply MCP availability rules to error workflow settings ([#39885](https://github.com/n8n-io/n8n/issues/39885)) ([e13c480](https://github.com/n8n-io/n8n/commit/e13c4806262469bca3888da0ad91a3e8354e6d78))
|
||||
* **core:** Apply policy checks to CLI import and execute commands ([#39714](https://github.com/n8n-io/n8n/issues/39714)) ([c575f18](https://github.com/n8n-io/n8n/commit/c575f18c999d2f7740ff57862ff1077f08107f9e))
|
||||
* **core:** Apply removed fields when the Agent Builder patches agent config ([#40122](https://github.com/n8n-io/n8n/issues/40122)) ([93c0a5c](https://github.com/n8n-io/n8n/commit/93c0a5c0ff716089047d3b7bb0c233025585f286))
|
||||
* **core:** Block publishing workflows with unconnected required node inputs ([#37265](https://github.com/n8n-io/n8n/issues/37265)) ([5e28ff8](https://github.com/n8n-io/n8n/commit/5e28ff8c238e9481706ebd8aee3f65704f734235))
|
||||
* **core:** Cap pending task request timeouts to the shutdown deadline ([#40136](https://github.com/n8n-io/n8n/issues/40136)) ([28bd124](https://github.com/n8n-io/n8n/commit/28bd124258f0d2c16b76c2485675dd6bbc92b4b9))
|
||||
* **core:** Connect Slack Agent channels through managed setup in MCP ([#39776](https://github.com/n8n-io/n8n/issues/39776)) ([93b658c](https://github.com/n8n-io/n8n/commit/93b658c8db04869c992fc8311a1fec1faf5b195e))
|
||||
* **core:** Correct Agent session pagination on SQLite ([#39802](https://github.com/n8n-io/n8n/issues/39802)) ([1882cff](https://github.com/n8n-io/n8n/commit/1882cff76b8bcac8259ef2ceb55bcedce31132ff))
|
||||
* **core:** Deduplicate agent message deliveries ([#39794](https://github.com/n8n-io/n8n/issues/39794)) ([0811443](https://github.com/n8n-io/n8n/commit/081144390982a1589433a9f318d349109c23ec5c))
|
||||
* **core:** Emit unique keys and conditional requirements in generated node type definitions ([#39960](https://github.com/n8n-io/n8n/issues/39960)) ([1700702](https://github.com/n8n-io/n8n/commit/1700702e52255efadec3def48e9c7575a6c1b956))
|
||||
* **core:** Generate missing workflow IDs in the import:workflow command ([#40011](https://github.com/n8n-io/n8n/issues/40011)) ([d12cab0](https://github.com/n8n-io/n8n/commit/d12cab0ed18abbfb27881afa6cc52f2cddc5a078))
|
||||
* **core:** Improve workflow SDK group layout for AI subtrees and sticky notes ([#39981](https://github.com/n8n-io/n8n/issues/39981)) ([7675d2b](https://github.com/n8n-io/n8n/commit/7675d2bcbd7d9942685179254d9d88b38afaad96))
|
||||
* **core:** Keep concurrent credential tests from sharing one node type registry ([#40054](https://github.com/n8n-io/n8n/issues/40054)) ([1cac1e0](https://github.com/n8n-io/n8n/commit/1cac1e056daade255b123d48a4b39ce8a38f9247))
|
||||
* **core:** Keep floating entitlements attached when a multi-main instance shuts down ([#39713](https://github.com/n8n-io/n8n/issues/39713)) ([ad90c63](https://github.com/n8n-io/n8n/commit/ad90c6392320c5f2d417ea5ae81c15f314af03b0))
|
||||
* **core:** Keep jobs a worker enqueued out of its shutdown drain ([#39800](https://github.com/n8n-io/n8n/issues/39800)) ([cd9d1ac](https://github.com/n8n-io/n8n/commit/cd9d1ac669c48e8c0fa68be8feffc4e51e651183))
|
||||
* **core:** Keep node parameters when n8n Assistant cannot see them ([#39454](https://github.com/n8n-io/n8n/issues/39454)) ([f82fa0c](https://github.com/n8n-io/n8n/commit/f82fa0c1af43b2278bb9f689a2dd73390f1693b4))
|
||||
* **core:** Keep the task runner running on an unhandled promise rejection ([#39946](https://github.com/n8n-io/n8n/issues/39946)) ([59c6ab2](https://github.com/n8n-io/n8n/commit/59c6ab2057ead5026a04e99427c0151c62ecdca6))
|
||||
* **core:** Limit background task continuation to the latest stop group ([#40333](https://github.com/n8n-io/n8n/issues/40333)) ([314c430](https://github.com/n8n-io/n8n/commit/314c430bfc2a1fc38ccf1d11a952a5dcc77b6c3a))
|
||||
* **core:** Load workflow publish history in a separate query ([#40171](https://github.com/n8n-io/n8n/issues/40171)) ([548bd57](https://github.com/n8n-io/n8n/commit/548bd57802f64694d98e70ab1a5fbfa1cdd0fcbe))
|
||||
* **core:** Pass graceful-shutdown timeout to runners in the runners image ([#39796](https://github.com/n8n-io/n8n/issues/39796)) ([d8dcc92](https://github.com/n8n-io/n8n/commit/d8dcc924b730d4f5c713803372006cbfe11aeab4))
|
||||
* **core:** Pass registry and auth token to npm outdated for community packages ([#31515](https://github.com/n8n-io/n8n/issues/31515)) ([1998011](https://github.com/n8n-io/n8n/commit/1998011d90c1c4bec561d8c9007e140cd8a01481))
|
||||
* **core:** Redact MCP tool telemetry properties ([#39887](https://github.com/n8n-io/n8n/issues/39887)) ([fc99aaf](https://github.com/n8n-io/n8n/commit/fc99aaf87be94082f56e3f061e96907c1806a89d))
|
||||
* **core:** Reload static data for queued executions ([#39668](https://github.com/n8n-io/n8n/issues/39668)) ([439e8ba](https://github.com/n8n-io/n8n/commit/439e8ba2e70be371268bbdb86f6afb9bc5e50699))
|
||||
* **core:** Report empty node output in Instance AI workflow verification ([#40047](https://github.com/n8n-io/n8n/issues/40047)) ([6438ead](https://github.com/n8n-io/n8n/commit/6438ead58d109b51b9e29752ea5d58f492bd5fcb))
|
||||
* **core:** Resolve the request target against the dispatcher origin ([#39506](https://github.com/n8n-io/n8n/issues/39506)) ([9183d04](https://github.com/n8n-io/n8n/commit/9183d0422c66b70d83669f255f6d710d6b9c7e7c))
|
||||
* **core:** Resume Preview agents after waiting workflows finish ([#40043](https://github.com/n8n-io/n8n/issues/40043)) ([bdab0b1](https://github.com/n8n-io/n8n/commit/bdab0b10172a7355804cf6e38afbeb8aa5bd194c))
|
||||
* **core:** Return upstream error details from the explore_node_resources MCP tool ([#39775](https://github.com/n8n-io/n8n/issues/39775)) ([a931dd8](https://github.com/n8n-io/n8n/commit/a931dd8660b29bb8cbe6424bb1f9a7a7f1412479))
|
||||
* **core:** Stop waiting on Bull job.finished() for queued executions ([#39723](https://github.com/n8n-io/n8n/issues/39723)) ([c83fee0](https://github.com/n8n-io/n8n/commit/c83fee02fac194543fa4936cfa4f3a57a1da7931))
|
||||
* **core:** Stop waiting on enqueued jobs in worker shutdown ([#40065](https://github.com/n8n-io/n8n/issues/40065)) ([fc3954b](https://github.com/n8n-io/n8n/commit/fc3954bfe2d88338b8d22453cbb4c111a69d057e))
|
||||
* **core:** Use the new client ID and secret for OAuth2 client credentials ([#39891](https://github.com/n8n-io/n8n/issues/39891)) ([44d9adf](https://github.com/n8n-io/n8n/commit/44d9adfc7e311e721a6cc70797b854abe3ed528e))
|
||||
* **core:** Wait for credential deletion to finish ([#40103](https://github.com/n8n-io/n8n/issues/40103)) ([407cf23](https://github.com/n8n-io/n8n/commit/407cf23da75d6027b7490c00ec274114e6438786))
|
||||
* **Databricks Trigger Node:** Cap pipeline event pages at 250, the limit Databricks enforces ([#40154](https://github.com/n8n-io/n8n/issues/40154)) ([b8cd29f](https://github.com/n8n-io/n8n/commit/b8cd29fde40288d0f14db15bd50429035c210174))
|
||||
* **DataTable Node:** Continue paginated reads when row counts change ([#39764](https://github.com/n8n-io/n8n/issues/39764)) ([d2eae41](https://github.com/n8n-io/n8n/commit/d2eae41dd6e46d2cb2d430d9df661c4ea8afd4da))
|
||||
* **editor:** Align Agent confirmation warning icons ([#40040](https://github.com/n8n-io/n8n/issues/40040)) ([f1d4017](https://github.com/n8n-io/n8n/commit/f1d4017883bdae464d0a7588b5eff84443e50351))
|
||||
* **editor:** Align input heights on the project settings page ([#39611](https://github.com/n8n-io/n8n/issues/39611)) ([ea9f451](https://github.com/n8n-io/n8n/commit/ea9f451076722a6784e0f9d78af4410952be9a99))
|
||||
* **editor:** Align Preview and Assistant chat history controls ([#40165](https://github.com/n8n-io/n8n/issues/40165)) ([a30bb7d](https://github.com/n8n-io/n8n/commit/a30bb7d89d051f26030800db9f7e31003a8aaa4e))
|
||||
* **editor:** Align workflow and agent action menus ([#39234](https://github.com/n8n-io/n8n/issues/39234)) ([d09e6d6](https://github.com/n8n-io/n8n/commit/d09e6d65fcb3398098341d63b7df1b7116baf4c7))
|
||||
* **editor:** Close the AI Assistant tab hover card when the pointer leaves the tab ([#40003](https://github.com/n8n-io/n8n/issues/40003)) ([0817b0e](https://github.com/n8n-io/n8n/commit/0817b0e8cc605565d269cfcf329eb8336eafc8ef))
|
||||
* **editor:** Correct the expanded Markdown editor close label ([#40041](https://github.com/n8n-io/n8n/issues/40041)) ([ee46834](https://github.com/n8n-io/n8n/commit/ee46834915197609cd75ce56fdce2c2e8409f787))
|
||||
* **editor:** Do not run the workflow when the save before execute fails ([#39763](https://github.com/n8n-io/n8n/issues/39763)) ([7204df6](https://github.com/n8n-io/n8n/commit/7204df600d9c9b2dc3c8ac2bc635082d207f72e6))
|
||||
* **editor:** Enable Save when SSO role provisioning switches back to manual ([#39925](https://github.com/n8n-io/n8n/issues/39925)) ([360ec09](https://github.com/n8n-io/n8n/commit/360ec09cf2a32da71dc595be2618a3e2d834dac8))
|
||||
* **editor:** Extend workflow review divider to full height ([#40035](https://github.com/n8n-io/n8n/issues/40035)) ([c5658ba](https://github.com/n8n-io/n8n/commit/c5658ba4bf21e839464fc313bb04940ae93506d6))
|
||||
* **editor:** Fix copy button hover state in MCP settings ([#39888](https://github.com/n8n-io/n8n/issues/39888)) ([f0b7013](https://github.com/n8n-io/n8n/commit/f0b70134a8acec730d69a46d71ee76b7358fb4e8))
|
||||
* **editor:** Keep credentials unchanged on read-only canvases ([#39889](https://github.com/n8n-io/n8n/issues/39889)) ([d69b52e](https://github.com/n8n-io/n8n/commit/d69b52e2c78fa1e2c993e11ce270f48f11590d05))
|
||||
* **editor:** Keep execution filters when navigating away from executions ([#39781](https://github.com/n8n-io/n8n/issues/39781)) ([fefb2da](https://github.com/n8n-io/n8n/commit/fefb2da2b1ed670114aa5f38f22bf7fe82e8dd97))
|
||||
* **editor:** Leave the sweep time of interrupted tool calls out of the Assistant thinking duration ([#40161](https://github.com/n8n-io/n8n/issues/40161)) ([3879051](https://github.com/n8n-io/n8n/commit/387905148a793e83f0fa5d4877947b3c52fd0f22))
|
||||
* **editor:** Preserve Agent instruction edits during autosave ([#39972](https://github.com/n8n-io/n8n/issues/39972)) ([4d931ca](https://github.com/n8n-io/n8n/commit/4d931ca77e9fd2250cb75ce3b7a55b995c39cef7))
|
||||
* **editor:** Refresh the Assistant chat list when the tab becomes visible ([#40158](https://github.com/n8n-io/n8n/issues/40158)) ([064c3bf](https://github.com/n8n-io/n8n/commit/064c3bf979538b1ac9e309590b34842265545427))
|
||||
* **editor:** Remove the new-agent pill from the AI assistant composer ([#40014](https://github.com/n8n-io/n8n/issues/40014)) ([e79a2ba](https://github.com/n8n-io/n8n/commit/e79a2ba84bce39a65151d4ba7ad72da08266daeb))
|
||||
* **editor:** Remove the one-click connector for the ChatGPT MCP client ([#40123](https://github.com/n8n-io/n8n/issues/40123)) ([cbaea98](https://github.com/n8n-io/n8n/commit/cbaea9852139ceabdd629d582291965f924d9de1))
|
||||
* **editor:** Restore full-width Agent preview ([#39986](https://github.com/n8n-io/n8n/issues/39986)) ([5c0aff8](https://github.com/n8n-io/n8n/commit/5c0aff8cbc82b384a4205fb3e99bd86954aaade5))
|
||||
* **editor:** Scale up node icons that declare a size smaller than their slot ([#40181](https://github.com/n8n-io/n8n/issues/40181)) ([3fe4612](https://github.com/n8n-io/n8n/commit/3fe4612d93b8ecf7e04e86d985329650411bc256))
|
||||
* **editor:** Show full reasoning on first expand of assistant thinking block ([#39892](https://github.com/n8n-io/n8n/issues/39892)) ([57ef0d1](https://github.com/n8n-io/n8n/commit/57ef0d138fb609c472521d9e8b09981625a8b201))
|
||||
* **editor:** Show remaining credits on Assistant banner for Cloud UBB ([#39901](https://github.com/n8n-io/n8n/issues/39901)) ([29c09c6](https://github.com/n8n-io/n8n/commit/29c09c6f997bcee2fa8133a716d08c3cf2121d0e))
|
||||
* **editor:** Show warning when moving a workflow with data table dependency ([#39752](https://github.com/n8n-io/n8n/issues/39752)) ([fce9570](https://github.com/n8n-io/n8n/commit/fce9570711e5b14c42204ac2b184680f36b956d6))
|
||||
* **editor:** Stop credential icon overlapping the credential name ([#39944](https://github.com/n8n-io/n8n/issues/39944)) ([1694727](https://github.com/n8n-io/n8n/commit/169472754843910279010e1007a842030206c4c1))
|
||||
* **editor:** Unify MCP access labels ([#40090](https://github.com/n8n-io/n8n/issues/40090)) ([88d6579](https://github.com/n8n-io/n8n/commit/88d657945995973b939156b0fb43cdf5baa8cf9d))
|
||||
* **editor:** Use only server-provided hints for the OAuth consent screen icon ([#37317](https://github.com/n8n-io/n8n/issues/37317)) ([01e5ebf](https://github.com/n8n-io/n8n/commit/01e5ebf7267d7d4eb39ba2caded51e92c5ba2945))
|
||||
* **editor:** Wait for OAuth sign-in before the assistant credential card continues ([#39998](https://github.com/n8n-io/n8n/issues/39998)) ([0a1ed4e](https://github.com/n8n-io/n8n/commit/0a1ed4e09b0e16cd55a29ada08b2641447a5f815))
|
||||
* **Execute Workflow Node:** Return declared sub-workflow outputs ([#39755](https://github.com/n8n-io/n8n/issues/39755)) ([41a0cca](https://github.com/n8n-io/n8n/commit/41a0ccaf160c22839ab82211e7741d4a607ba03f))
|
||||
* **Google Calendar Trigger Node:** Handle all-day event boundaries ([#39898](https://github.com/n8n-io/n8n/issues/39898)) ([900fe0c](https://github.com/n8n-io/n8n/commit/900fe0c03a96009aee9103e8ec83e756daa6d943))
|
||||
* Hide node types that the instance does not load ([#39571](https://github.com/n8n-io/n8n/issues/39571)) ([0afda58](https://github.com/n8n-io/n8n/commit/0afda588042c712da6225663669f847cdfe788f7))
|
||||
* Keep workflow sharing open until changes are saved ([#40137](https://github.com/n8n-io/n8n/issues/40137)) ([9e5eb79](https://github.com/n8n-io/n8n/commit/9e5eb79cc46b2d6fe806be00a078225905ee77f5))
|
||||
* Limit MCP Agent model discovery results ([#39973](https://github.com/n8n-io/n8n/issues/39973)) ([996dec3](https://github.com/n8n-io/n8n/commit/996dec31936c7aeba7a9212df8476ead584f7d3d))
|
||||
* Pass shutdown margin to JavaScript runner in runners image ([#40178](https://github.com/n8n-io/n8n/issues/40178)) ([df27223](https://github.com/n8n-io/n8n/commit/df27223a4edf9cca4287bdd321d4acfba1effac9))
|
||||
* Persist nested node custom span attributes ([#40107](https://github.com/n8n-io/n8n/issues/40107)) ([4a0865b](https://github.com/n8n-io/n8n/commit/4a0865bb62a602397a61f6f635bdf16bb4b99e6a))
|
||||
* Replace deprecated APIs in services and tooling ([#40199](https://github.com/n8n-io/n8n/issues/40199)) ([be2ee6f](https://github.com/n8n-io/n8n/commit/be2ee6f35661f4e052cc600047594a153a71bdc3))
|
||||
* Report the real cause when a module entrypoint fails to load ([#37625](https://github.com/n8n-io/n8n/issues/37625)) ([5db0c39](https://github.com/n8n-io/n8n/commit/5db0c39dde9086dbd5ad6041e53ad6d896af0b8f))
|
||||
* Scope the legacy setup credential picker to the target workflow and report unusable credentials on apply ([#38197](https://github.com/n8n-io/n8n/issues/38197)) ([5c1f8f7](https://github.com/n8n-io/n8n/commit/5c1f8f7f6c7537bfd5a65e94a886d8cc29196908))
|
||||
* **Split Out Node:** Support dot notation in destination fields ([#39661](https://github.com/n8n-io/n8n/issues/39661)) ([f9dbd2c](https://github.com/n8n-io/n8n/commit/f9dbd2c8d63413c61ba781ab1050ec77a2499973))
|
||||
* **WhatsApp Node:** Fetch all templates for the dropdown ([#39497](https://github.com/n8n-io/n8n/issues/39497)) ([ce295b6](https://github.com/n8n-io/n8n/commit/ce295b6080f34cc0c9b0c51119d2b1469d19a5ac))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai-builder:** Change knowledge subtitle ([#39838](https://github.com/n8n-io/n8n/issues/39838)) ([6cc8c0d](https://github.com/n8n-io/n8n/commit/6cc8c0d4480b4a7a0ea44d38655060325fe939f7))
|
||||
* **Azure OpenAI Chat Model Node:** Add the deployment dropdown ([#39643](https://github.com/n8n-io/n8n/issues/39643)) ([022f4a6](https://github.com/n8n-io/n8n/commit/022f4a6477d0f53f61acda88d5b71617cdd4ca5b))
|
||||
* **core:** Add 'align' prop to Select component ([#39965](https://github.com/n8n-io/n8n/issues/39965)) ([b15ffa9](https://github.com/n8n-io/n8n/commit/b15ffa95423c8a238a155f1d379fcf031bf1a4eb))
|
||||
* **core:** Add a guardrail runner to the agent loop ([#39621](https://github.com/n8n-io/n8n/issues/39621)) ([3745c83](https://github.com/n8n-io/n8n/commit/3745c835b2b47ed484246fcd7aefd5e9be3be5bf))
|
||||
* **core:** Add an instance setting for Agents ([#40153](https://github.com/n8n-io/n8n/issues/40153)) ([628ada7](https://github.com/n8n-io/n8n/commit/628ada76cde4f51348849893dceaa61c3528e36e))
|
||||
* **core:** Add an opt-in budget guardrail for agent runs ([#39765](https://github.com/n8n-io/n8n/issues/39765)) ([d5937ed](https://github.com/n8n-io/n8n/commit/d5937ed2b6e947cb309566b9fb2cbfb32bb33c66))
|
||||
* **core:** Add engine suspension at node boundaries for resumable executions ([#37649](https://github.com/n8n-io/n8n/issues/37649)) ([680f958](https://github.com/n8n-io/n8n/commit/680f9582e16bff0d6e8c4077a13f2ac26061bb88))
|
||||
* **core:** Add inbound-auth-core module with trusted source entities and migration ([#39642](https://github.com/n8n-io/n8n/issues/39642)) ([0f63032](https://github.com/n8n-io/n8n/commit/0f630329167896a1452a2d6c9b5e12cf6dce165b))
|
||||
* **core:** Add remote search and programmatic focus to N8nCombobox so the MCP selects can leave N8nSelect ([#40140](https://github.com/n8n-io/n8n/issues/40140)) ([8f13f28](https://github.com/n8n-io/n8n/commit/8f13f2830a771e10ecfcc3c178a60c614103e3b5))
|
||||
* **core:** Add tool to mark an agent session as failed ([#39754](https://github.com/n8n-io/n8n/issues/39754)) ([114a573](https://github.com/n8n-io/n8n/commit/114a5737f57b271cb4d63dc849a13e8d27e148a4))
|
||||
* **core:** Capture sidecall generation usd cost ([#39627](https://github.com/n8n-io/n8n/issues/39627)) ([8c9733b](https://github.com/n8n-io/n8n/commit/8c9733b5c68202dc17a41aa7d72816879e223c66))
|
||||
* **core:** Change IAI permissions logic ([#39303](https://github.com/n8n-io/n8n/issues/39303)) ([99182c2](https://github.com/n8n-io/n8n/commit/99182c29be23cd2d8cdb1e1e5b46d3d37ceaf716))
|
||||
* **core:** Escalate background sub-agent approvals to parent chats ([#39552](https://github.com/n8n-io/n8n/issues/39552)) ([5d40d83](https://github.com/n8n-io/n8n/commit/5d40d837f632799d3b81b9a4d8dcd0d0dad8ad03))
|
||||
* **core:** Export the stored schedule of durable system tasks as metrics ([#40009](https://github.com/n8n-io/n8n/issues/40009)) ([3faf54f](https://github.com/n8n-io/n8n/commit/3faf54fea360d8bebb2c598d6a730c9e034dee90))
|
||||
* **core:** Hide current Agent references and show built Workflow cards in the embedded Assistant ([#40149](https://github.com/n8n-io/n8n/issues/40149)) ([1043504](https://github.com/n8n-io/n8n/commit/1043504c7a76de1809a9dcf2e1e6f0c88f5b1b9f))
|
||||
* **core:** Indicate unpublished changes in agent resource list view ([#40177](https://github.com/n8n-io/n8n/issues/40177)) ([6a5aa78](https://github.com/n8n-io/n8n/commit/6a5aa7862ea38cbf75445b13993018aaff587c79))
|
||||
* **core:** New agent styled differently on agent list view ([#39856](https://github.com/n8n-io/n8n/issues/39856)) ([f931ea8](https://github.com/n8n-io/n8n/commit/f931ea81d52b920d14000c57eaf71a646981467f))
|
||||
* **core:** Persist agent budget spend in the database ([#40138](https://github.com/n8n-io/n8n/issues/40138)) ([a2fd418](https://github.com/n8n-io/n8n/commit/a2fd4189f1788a8656e63c8d17cf2c3be197f5ca))
|
||||
* **core:** Run legacy Cron node schedules on the durable scheduler ([#39411](https://github.com/n8n-io/n8n/issues/39411)) ([bab2978](https://github.com/n8n-io/n8n/commit/bab2978ccf0a9bcb8a55c71386e973531bd958d9))
|
||||
* **core:** Send a log streaming audit event when a policy blocks an action ([#39581](https://github.com/n8n-io/n8n/issues/39581)) ([55fd9da](https://github.com/n8n-io/n8n/commit/55fd9daa089b3af5f387f968644a6cc10e57c451))
|
||||
* **core:** Serve the migration report overview from the finding table ([#39969](https://github.com/n8n-io/n8n/issues/39969)) ([6769662](https://github.com/n8n-io/n8n/commit/6769662ecf875317187dd70e4b82b6dc099893ed))
|
||||
* **core:** Serve the migration rule detail from the finding table ([#39974](https://github.com/n8n-io/n8n/issues/39974)) ([b8ff99f](https://github.com/n8n-io/n8n/commit/b8ff99f2cc66c64c8347d97d803fb1ed63c13547))
|
||||
* **core:** Stop engine v2 executions from the control plane ([#39786](https://github.com/n8n-io/n8n/issues/39786)) ([56d6e9d](https://github.com/n8n-io/n8n/commit/56d6e9da2c64a3671ead1dfa399f94b934b1f149))
|
||||
* **core:** Store isolated workflow suggestions for review ([#39505](https://github.com/n8n-io/n8n/issues/39505)) ([0352a19](https://github.com/n8n-io/n8n/commit/0352a196542703a8af80a116c7f94973b6eb2cf2))
|
||||
* **core:** Vanish Fix with Assistant banner when AIA fixes error ([#39977](https://github.com/n8n-io/n8n/issues/39977)) ([0f11660](https://github.com/n8n-io/n8n/commit/0f11660414aa34e0792c102d174f59d0dda126cd))
|
||||
* **Crypto Node:** Add RSA-OAEP hash option ([#39853](https://github.com/n8n-io/n8n/issues/39853)) ([94a49d0](https://github.com/n8n-io/n8n/commit/94a49d08ce39efc8c20e46ac4b23138dd9419f06))
|
||||
* **Databricks Node:** Enable the Job resource ([#40142](https://github.com/n8n-io/n8n/issues/40142)) ([fb69206](https://github.com/n8n-io/n8n/commit/fb69206450729bf96f659ed9b7ff5789b395619a))
|
||||
* **editor:** Add activation menus to Agent capabilities ([#40104](https://github.com/n8n-io/n8n/issues/40104)) ([53b6a68](https://github.com/n8n-io/n8n/commit/53b6a68e12483fed869ac907e089bf86b502291b))
|
||||
* **editor:** Add keyboard controls to n8n Assistant panel ([#39386](https://github.com/n8n-io/n8n/issues/39386)) ([41994bc](https://github.com/n8n-io/n8n/commit/41994bc6c7476e051fe6d5b960dd6d6f03924de5))
|
||||
* **editor:** Add Remove context menus to Agent configuration ([#40089](https://github.com/n8n-io/n8n/issues/40089)) ([3d70eef](https://github.com/n8n-io/n8n/commit/3d70eef3a3593f368110d8d76411769b0c6e90f6))
|
||||
* **editor:** Add Stop all tasks to the Preview task panel ([#40120](https://github.com/n8n-io/n8n/issues/40120)) ([25831a8](https://github.com/n8n-io/n8n/commit/25831a8ac0d0f2b5c060dcc39c7ed28ef5da5306))
|
||||
* **editor:** Add usage and budget settings for agents ([#39909](https://github.com/n8n-io/n8n/issues/39909)) ([c4d31db](https://github.com/n8n-io/n8n/commit/c4d31db8d61f972e29d0d48e03987d67edbfec0a))
|
||||
* **editor:** Allow reordering queued messages in Agent Preview ([#40101](https://github.com/n8n-io/n8n/issues/40101)) ([33505d7](https://github.com/n8n-io/n8n/commit/33505d7faade510f270f032a6f7aa44744e3ab8d))
|
||||
* **editor:** Close AI Assistant tabs and keep the open tabs per thread ([#39660](https://github.com/n8n-io/n8n/issues/39660)) ([9ab649f](https://github.com/n8n-io/n8n/commit/9ab649ff55b8b5af3d3331c4355d841d37a41b34))
|
||||
* **editor:** Open and collapse the logs panel around runs in the AI Assistant artifact view ([#38773](https://github.com/n8n-io/n8n/issues/38773)) ([9a4a1dc](https://github.com/n8n-io/n8n/commit/9a4a1dcf27d1a16b32ea6c68a6e7764403853e6a))
|
||||
* **editor:** Open any project resource as an AI Assistant tab ([#39663](https://github.com/n8n-io/n8n/issues/39663)) ([0c4bf38](https://github.com/n8n-io/n8n/commit/0c4bf38033be064f021bcaea2fcfc50d82c874c6))
|
||||
* **editor:** Refine the workflow review detail layout ([#39866](https://github.com/n8n-io/n8n/issues/39866)) ([373bf77](https://github.com/n8n-io/n8n/commit/373bf77f3a4d97cb616f6f1248cdf487ac598f6e))
|
||||
* **editor:** Release Browser Use to all users ([#39762](https://github.com/n8n-io/n8n/issues/39762)) ([f58f0ec](https://github.com/n8n-io/n8n/commit/f58f0ec1d046204b1e1047f6c104606a0277cb41))
|
||||
* **editor:** Reorder AI Assistant tabs by dragging ([#39664](https://github.com/n8n-io/n8n/issues/39664)) ([2f4bbfa](https://github.com/n8n-io/n8n/commit/2f4bbfa7d2e0debdbfa8b8db981b44393c262c33))
|
||||
* **editor:** Replace severity with impact categories in the migration report ([#39551](https://github.com/n8n-io/n8n/issues/39551)) ([0f77ba5](https://github.com/n8n-io/n8n/commit/0f77ba5e7ee90bb378764a2dc5689a1d6fe3f7a6))
|
||||
* **editor:** Show Gateway credits promotions on community nodes ([#40010](https://github.com/n8n-io/n8n/issues/40010)) ([190d7c0](https://github.com/n8n-io/n8n/commit/190d7c0dc0fb6dcef315de97d8cb2d24579afdff))
|
||||
* **editor:** Show migration progress on report overview ([#39861](https://github.com/n8n-io/n8n/issues/39861)) ([9d11369](https://github.com/n8n-io/n8n/commit/9d11369db77719f43ff52e6d48455251c3358f87))
|
||||
* **editor:** Show n8n logo on canvas in canvas-only mode ([#39846](https://github.com/n8n-io/n8n/issues/39846)) ([1153702](https://github.com/n8n-io/n8n/commit/1153702d1a29223f7b80a51e1d73d4772f4bc462))
|
||||
* **editor:** Show the trigger icon on the execute button and run a trigger from its menu ([#39580](https://github.com/n8n-io/n8n/issues/39580)) ([55a8787](https://github.com/n8n-io/n8n/commit/55a87877b5ba82f7e460ac9894fcfc515263e3b3))
|
||||
* **editor:** Show whose credential a node uses and let others switch from it ([#40281](https://github.com/n8n-io/n8n/issues/40281)) ([92ae1b3](https://github.com/n8n-io/n8n/commit/92ae1b336f1542b73eb0c65f01426d9c3739836d))
|
||||
* **editor:** Stop Execute and Publish when a credential is not yours to use ([#39883](https://github.com/n8n-io/n8n/issues/39883)) ([deaa9b1](https://github.com/n8n-io/n8n/commit/deaa9b105477ef0ace3ddedaa32f37c77a106ca3))
|
||||
* **editor:** Submit selected project changes from the promote modal ([#39609](https://github.com/n8n-io/n8n/issues/39609)) ([f7cfb52](https://github.com/n8n-io/n8n/commit/f7cfb520bf666a6122bb8b99e7ae2f976f9a0b0a))
|
||||
* Hide generated AI tools listed in NODES_EXCLUDE and empty node creator entries ([#39577](https://github.com/n8n-io/n8n/issues/39577)) ([5e376b6](https://github.com/n8n-io/n8n/commit/5e376b69edf3eb10dfa7f4a735ddf36a5cb027bf))
|
||||
* Implements n8n Assistant nudge on workflow errors ([#39774](https://github.com/n8n-io/n8n/issues/39774)) ([af26954](https://github.com/n8n-io/n8n/commit/af2695407abd2d299819f94aa8c4a9eb254587f3))
|
||||
* **Supabase Node:** Add OAuth credentials and reuse them for MCP ([#39979](https://github.com/n8n-io/n8n/issues/39979)) ([8e339d7](https://github.com/n8n-io/n8n/commit/8e339d70513d5bed3d708d6ea25b4e43c9ed6099))
|
||||
* Tell the AI Assistant which preview tabs are open ([#39768](https://github.com/n8n-io/n8n/issues/39768)) ([10e484f](https://github.com/n8n-io/n8n/commit/10e484f552abbddf3694f13ccda2875323ba8e68))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **core:** Load project relations without joining every role scope ([#39756](https://github.com/n8n-io/n8n/issues/39756)) ([d92bc4d](https://github.com/n8n-io/n8n/commit/d92bc4d3ab1ca45e0e2d096e4d66a9dcc7e14511))
|
||||
|
||||
|
||||
# [2.42.0](https://github.com/n8n-io/n8n/compare/n8n@2.41.0...n8n@2.42.0) (2026-09-29)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ Each instance gets its own port, and the webhook URL matches the main URL. Multi
|
|||
>- This does not work with Podman out of the box. You need to [configure Testcontainers for Podman](https://podman-desktop.io/tutorial/testcontainers-with-podman) first.
|
||||
>- Alternatively, you can use colima and set DOCKER_HOST and TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variables as described [here](https://node.testcontainers.org/supported-container-runtimes/#colima)
|
||||
|
||||
Refer to [packages/testing/containers/README.md](packages/testing/containers/README.md) for more information.
|
||||
Refer to [packages/quality/environments/containers/README.md](packages/quality/environments/containers/README.md) for more information.
|
||||
|
||||
### Work locally with syslog
|
||||
|
||||
|
|
@ -661,7 +661,7 @@ E2E tests can be started via one of the following commands:
|
|||
- `pnpm --filter=n8n-playwright test:local --ui` - Run tests in interactive UI mode (useful for debugging)
|
||||
- `pnpm --filter=n8n-playwright test:local --grep="test-name"` - Run specific tests matching pattern
|
||||
|
||||
See `packages/testing/playwright/README.md` for more test commands and `packages/testing/playwright/CONTRIBUTING.md` for writing guidelines.
|
||||
See `packages/quality/testing/playwright/README.md` for more test commands and `packages/quality/testing/playwright/CONTRIBUTING.md` for writing guidelines.
|
||||
|
||||
## Create custom nodes
|
||||
|
||||
|
|
|
|||
235
OWNERS
235
OWNERS
|
|
@ -8,8 +8,8 @@
|
|||
#
|
||||
# Last-match-wins: specific rules MUST come AFTER general rules.
|
||||
#
|
||||
# The `required` option makes team approval mandatory: a member of the team
|
||||
# must approve the PR before merge. This is enforced by the
|
||||
# The `required` option makes team approval mandatory for non-test files: a
|
||||
# member of the team must approve the PR before merge. This is enforced by the
|
||||
# "Required Reviews" commit status (ci-owners-required-reviews.yml).
|
||||
# All other entries are advisory (reviewer recommendations and opt-in
|
||||
# reviewer auto-assignment).
|
||||
|
|
@ -28,6 +28,9 @@ packages/@n8n/config/ @n8n-io/catalysts
|
|||
packages/@n8n/backend-common/ @n8n-io/catalysts
|
||||
packages/@n8n/backend-network/ @n8n-io/catalysts
|
||||
packages/@n8n/backend-services/ @n8n-io/catalysts
|
||||
packages/@n8n/backend-services/src/services/role.service.ts @n8n-io/iam
|
||||
packages/@n8n/backend-services/src/services/role-cache.service.ts @n8n-io/iam
|
||||
packages/@n8n/backend-services/src/services/jwt.service.ts @n8n-io/iam
|
||||
packages/@n8n/backend-test-utils/ @n8n-io/catalysts
|
||||
packages/@n8n/di/ @n8n-io/catalysts
|
||||
packages/@n8n/errors/ @n8n-io/catalysts
|
||||
|
|
@ -50,21 +53,20 @@ packages/@n8n/db/src/migrations/ @n8n-io/migrations-revie
|
|||
# Top-level paths
|
||||
scripts/ @n8n-io/qa-dx
|
||||
patches/ @n8n-io/qa-dx
|
||||
assets/ @n8n-io/adore
|
||||
security/ @n8n-io/qa-dx
|
||||
.code-health-baseline.json @n8n-io/qa-dx required
|
||||
OWNERS @n8n-io/qa-dx required
|
||||
|
||||
# @n8n/cli
|
||||
packages/@n8n/cli/ @n8n-io/adore
|
||||
packages/@n8n/cli/ @n8n-io/context
|
||||
packages/@n8n/cli/src/commands/credential/ @n8n-io/iam
|
||||
packages/@n8n/cli/src/commands/user/ @n8n-io/iam
|
||||
packages/@n8n/cli/src/commands/data-table/ @n8n-io/adore
|
||||
packages/@n8n/cli/src/commands/tag/ @n8n-io/adore
|
||||
packages/@n8n/cli/src/commands/data-table/ @n8n-io/knowledge
|
||||
packages/@n8n/cli/src/commands/tag/ @n8n-io/context
|
||||
packages/@n8n/cli/src/commands/project/ @n8n-io/ligo
|
||||
packages/@n8n/cli/src/commands/source-control/ @n8n-io/ligo
|
||||
packages/@n8n/cli/src/commands/variable/ @n8n-io/ligo
|
||||
packages/@n8n/cli/src/commands/skill/ @n8n-io/ai
|
||||
packages/@n8n/cli/src/commands/skill/ @n8n-io/context
|
||||
|
||||
# packages/cli
|
||||
packages/cli/ @n8n-io/catalysts
|
||||
|
|
@ -83,7 +85,6 @@ packages/cli/src/security-audit/ @n8n-io/catalysts
|
|||
packages/cli/src/modules/workflow-index/ @n8n-io/catalysts
|
||||
packages/cli/src/modules/breaking-changes/ @n8n-io/catalysts
|
||||
packages/cli/src/workflows/publication/ @n8n-io/catalysts required
|
||||
packages/cli/src/modules/otel/ @n8n-io/ligo
|
||||
|
||||
packages/cli/src/auth/ @n8n-io/iam
|
||||
packages/cli/src/credentials/ @n8n-io/iam
|
||||
|
|
@ -107,23 +108,22 @@ packages/cli/src/environments.ee/ @n8n-io/ligo
|
|||
packages/cli/src/public-api/ @n8n-io/ligo
|
||||
packages/cli/src/modules/source-control.ee/ @n8n-io/ligo
|
||||
packages/cli/src/modules/external-secrets.ee/ @n8n-io/ligo
|
||||
packages/cli/src/modules/insights/ @n8n-io/ligo
|
||||
packages/modules/insights/ @n8n-io/ligo
|
||||
|
||||
packages/cli/src/collaboration/ @n8n-io/catalysts
|
||||
packages/cli/src/binary-data/ @n8n-io/catalysts
|
||||
packages/cli/src/posthog/ @n8n-io/adore
|
||||
packages/cli/src/modules/data-table/ @n8n-io/adore
|
||||
packages/cli/src/posthog/ @n8n-io/growth
|
||||
packages/cli/src/modules/data-table/ @n8n-io/knowledge
|
||||
|
||||
packages/cli/src/evaluation.ee/ @n8n-io/ai
|
||||
packages/cli/src/chat/ @n8n-io/ai
|
||||
packages/cli/src/evaluation.ee/ @n8n-io/ai-trust
|
||||
packages/cli/src/chat/ @n8n-io/ai-agents
|
||||
packages/cli/src/modules/agents/ @n8n-io/ai-agents
|
||||
packages/cli/src/tool-generation/ @n8n-io/ai
|
||||
packages/cli/src/modules/workflow-builder/ @n8n-io/ai
|
||||
packages/cli/src/modules/mcp/ @n8n-io/adore
|
||||
packages/cli/src/modules/quick-connect/ @n8n-io/ai
|
||||
packages/cli/src/modules/chat-hub/ @n8n-io/ai
|
||||
packages/modules/workflow-builder/ @n8n-io/ai-assistant
|
||||
packages/cli/src/modules/mcp/ @n8n-io/context
|
||||
packages/cli/src/modules/chat-hub/ @n8n-io/ai-assistant
|
||||
packages/cli/src/modules/instance-ai/ @n8n-io/ai-assistant
|
||||
|
||||
packages/cli/src/modules/community-packages/ @n8n-io/nodes
|
||||
packages/modules/inbox/ @n8n-io/ligo
|
||||
|
||||
# CLI controllers
|
||||
packages/cli/src/controllers/auth.controller.ts @n8n-io/iam
|
||||
|
|
@ -138,14 +138,14 @@ packages/cli/src/controllers/user-settings.controller.ts @n8n-io/iam
|
|||
packages/cli/src/controllers/api-keys.controller.ts @n8n-io/iam
|
||||
packages/cli/src/controllers/security-settings.controller.ts @n8n-io/iam
|
||||
packages/cli/src/controllers/oauth/ @n8n-io/iam
|
||||
packages/cli/src/controllers/ai.controller.ts @n8n-io/ai
|
||||
packages/cli/src/controllers/ai.controller.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/controllers/annotation-tags.controller.ee.ts @n8n-io/ai
|
||||
packages/cli/src/controllers/cta.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/folder.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/tags.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/binary-data.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/posthog.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/translation.controller.ts @n8n-io/adore
|
||||
packages/cli/src/controllers/cta.controller.ts @n8n-io/knowledge
|
||||
packages/cli/src/controllers/folder.controller.ts @n8n-io/context
|
||||
packages/cli/src/controllers/tags.controller.ts @n8n-io/context
|
||||
packages/cli/src/controllers/binary-data.controller.ts @n8n-io/knowledge
|
||||
packages/cli/src/controllers/posthog.controller.ts @n8n-io/context
|
||||
packages/cli/src/controllers/translation.controller.ts @n8n-io/catalysts
|
||||
packages/cli/src/controllers/project.controller.ts @n8n-io/ligo
|
||||
packages/cli/src/controllers/workflow-statistics.controller.ts @n8n-io/ligo
|
||||
packages/cli/src/controllers/node-types.controller.ts @n8n-io/nodes
|
||||
|
|
@ -153,24 +153,23 @@ packages/cli/src/controllers/dynamic-node-parameters.controller.ts @n8n-io/node
|
|||
packages/cli/src/controllers/e2e.controller.ts @n8n-io/qa-dx
|
||||
|
||||
# CLI services
|
||||
packages/cli/src/services/jwt.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/user.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/role.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/role-cache.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/password.utility.ts @n8n-io/iam
|
||||
packages/cli/src/services/public-api-key.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/security-settings.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/static-auth-service.ts @n8n-io/iam
|
||||
packages/cli/src/services/access.service.ts @n8n-io/iam
|
||||
packages/cli/src/services/ai.service.ts @n8n-io/ai
|
||||
packages/cli/src/services/ai-usage.service.ts @n8n-io/ai
|
||||
packages/cli/src/services/ai-workflow-builder.service.ts @n8n-io/ai
|
||||
packages/cli/src/services/annotation-tag.service.ee.ts @n8n-io/ai
|
||||
packages/cli/src/services/folder.service.ts @n8n-io/adore
|
||||
packages/cli/src/services/tag.service.ts @n8n-io/adore
|
||||
packages/cli/src/services/cta.service.ts @n8n-io/adore
|
||||
packages/cli/src/services/frontend.service.ts @n8n-io/adore
|
||||
packages/cli/src/services/banner.service.ts @n8n-io/adore
|
||||
packages/cli/src/services/ai.service.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/services/ai-usage.service.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/services/__tests__/ai-usage.service.test.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/controllers/__tests__/ai.controller.test.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/services/ai-workflow-builder.service.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/services/annotation-tag.service.ee.ts @n8n-io/context
|
||||
packages/cli/src/services/folder.service.ts @n8n-io/context
|
||||
packages/cli/src/services/tag.service.ts @n8n-io/context
|
||||
packages/cli/src/services/cta.service.ts @n8n-io/knowledge
|
||||
packages/cli/src/services/frontend.service.ts @n8n-io/catalysts
|
||||
packages/cli/src/services/banner.service.ts @n8n-io/context
|
||||
packages/cli/src/services/project.service.ee.ts @n8n-io/ligo
|
||||
packages/cli/src/services/workflow-statistics.service.ts @n8n-io/ligo
|
||||
packages/cli/src/services/export.service.ts @n8n-io/ligo
|
||||
|
|
@ -178,10 +177,11 @@ packages/cli/src/services/import.service.ts @n8n-io/ligo
|
|||
packages/cli/src/services/ownership.service.ts @n8n-io/ligo
|
||||
packages/cli/src/services/dynamic-node-parameters.service.ts @n8n-io/nodes
|
||||
|
||||
# Adore
|
||||
|
||||
packages/frontend/editor-ui/ @n8n-io/frontend
|
||||
packages/frontend/editor-ui/src/features/ai/ @n8n-io/ai
|
||||
packages/frontend/editor-ui/src/features/ai/assistant/ @n8n-io/ai-assistant
|
||||
packages/frontend/editor-ui/src/features/ai/chatHub/ @n8n-io/ai-assistant
|
||||
packages/frontend/editor-ui/src/features/ai/instanceAi/ @n8n-io/ai-assistant
|
||||
packages/frontend/editor-ui/src/features/credentials/ @n8n-io/iam
|
||||
packages/frontend/editor-ui/src/features/execution/ @n8n-io/ligo
|
||||
packages/frontend/editor-ui/src/features/integrations/ @n8n-io/ligo
|
||||
|
|
@ -194,19 +194,59 @@ packages/frontend/@n8n/storybook/ @n8n-io/design-engineeri
|
|||
packages/frontend/@n8n/i18n/ @n8n-io/frontend
|
||||
packages/@n8n/stylelint-config/ @n8n-io/qa-dx
|
||||
|
||||
packages/frontend/editor-ui/src/features/ai/gateway/ @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/app/composables/useAiGatewayTopUp.ts @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/app/composables/useFreeAiCredits.ts @n8n-io/relay
|
||||
|
||||
packages/frontend/editor-ui/src/features/settings/communityNodes/ @n8n-io/nodes
|
||||
packages/frontend/editor-ui/src/features/credentials/quickConnect/ @n8n-io/nodes required
|
||||
packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.api.ts @n8n-io/nodes
|
||||
packages/frontend/editor-ui/src/features/ndv/runData/schemaPreview.store.ts @n8n-io/nodes
|
||||
packages/frontend/editor-ui/src/features/agents/channels/ @n8n-io/nodes
|
||||
|
||||
# AI
|
||||
|
||||
packages/@n8n/instance-ai/ @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/ @n8n-io/ai
|
||||
packages/@n8n/ai-utilities/ @n8n-io/ai
|
||||
packages/@n8n/ai-node-sdk/ @n8n-io/ai
|
||||
packages/@n8n/ai-workflow-builder.ee/ @n8n-io/ai
|
||||
packages/@n8n/agents/ @n8n-io/ai
|
||||
packages/frontend/@n8n/chat/ @n8n-io/ai
|
||||
packages/@n8n/nodes-langchain/nodes/agents/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/chains/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/code/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/output_parser/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/llms/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/vendors/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/ModelSelector/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/embeddings/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/vector_store/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/document_loaders/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/text_splitters/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/retrievers/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/rerankers/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/memory/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/tools/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/ToolExecutor/ @n8n-io/ai-agents
|
||||
packages/@n8n/nodes-langchain/nodes/trigger/ @n8n-io/ai-agents
|
||||
packages/@n8n/ai-utilities/ @n8n-io/ai-agents
|
||||
packages/@n8n/ai-workflow-builder.ee/ @n8n-io/ai-assistant
|
||||
packages/@n8n/agents/ @n8n-io/ai-agents
|
||||
packages/frontend/@n8n/chat/ @n8n-io/ai-agents
|
||||
packages/frontend/editor-ui/src/features/agents/ @n8n-io/ai-agents
|
||||
|
||||
# Chat
|
||||
# Chat Hub
|
||||
|
||||
packages/@n8n/chat-hub/ @n8n-io/ai
|
||||
packages/@n8n/chat-hub/ @n8n-io/ai-assistant
|
||||
packages/@n8n/api-types/src/chat-hub.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/api-types/src/push/chat-hub.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/config/src/configs/chat-hub.config.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/nodes/vector_store/ChatHubVectorStorePGVector/ @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/nodes/vector_store/ChatHubVectorStorePinecone/ @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/nodes/vector_store/ChatHubVectorStoreQdrant/ @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/nodes/vector_store/shared/chatHub.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/credentials/ChatHubVectorStorePGVectorApi.credentials.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/credentials/ChatHubVectorStorePineconeApi.credentials.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/credentials/ChatHubVectorStoreQdrantApi.credentials.ts @n8n-io/ai-assistant
|
||||
packages/@n8n/nodes-langchain/nodes/mcp/ @n8n-io/nodes required
|
||||
packages/frontend/editor-ui/src/features/shared/commandBar/composables/useChatHubCommands.ts @n8n-io/ai-assistant
|
||||
packages/cli/src/modules/breaking-changes/rules/v3/chat-hub-deprecated.rule.ts @n8n-io/ai-assistant
|
||||
|
||||
# Nodes
|
||||
|
||||
|
|
@ -214,37 +254,95 @@ packages/@n8n/codemirror-lang/ @n8n-io/nodes
|
|||
packages/@n8n/codemirror-lang-html/ @n8n-io/nodes
|
||||
packages/@n8n/codemirror-lang-sql/ @n8n-io/nodes
|
||||
packages/nodes-base/ @n8n-io/nodes
|
||||
packages/nodes-base/nodes/Code/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/ExecuteCommand/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/Ssh/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/HttpRequest/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/Webhook/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/RespondToWebhook/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/Form/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/Wait/ @n8n-io/nodes required
|
||||
packages/nodes-base/utils/sendAndWait/ @n8n-io/nodes required
|
||||
packages/nodes-base/nodes/Git/ @n8n-io/nodes required
|
||||
packages/@n8n/decorators/ @n8n-io/catalysts
|
||||
packages/node-dev/ @n8n-io/nodes
|
||||
packages/@n8n/create-node/ @n8n-io/nodes
|
||||
packages/@n8n/node-cli/ @n8n-io/nodes
|
||||
packages/@n8n/imap/ @n8n-io/iam
|
||||
packages/@n8n/syslog-client/ @n8n-io/iam
|
||||
packages/@n8n/scan-community-package/ @n8n-io/nodes
|
||||
packages/@n8n/eslint-plugin-community-nodes/ @n8n-io/nodes
|
||||
packages/@n8n/computer-use/ @n8n-io/nodes
|
||||
packages/@n8n/local-gateway/ @n8n-io/nodes
|
||||
packages/@n8n/mcp-browser/ @n8n-io/nodes
|
||||
packages/@n8n/mcp-browser-extension/ @n8n-io/nodes
|
||||
packages/@n8n/imap/ @n8n-io/nodes
|
||||
packages/@n8n/computer-use/ @n8n-io/nodes required
|
||||
packages/@n8n/local-gateway/ @n8n-io/nodes required
|
||||
packages/@n8n/mcp-browser/ @n8n-io/nodes required
|
||||
packages/@n8n/mcp-browser-extension/ @n8n-io/nodes required
|
||||
packages/modules/quick-connect/ @n8n-io/nodes required
|
||||
packages/@n8n/api-types/src/dto/quick-connect/ @n8n-io/nodes required
|
||||
packages/@n8n/api-types/src/quick-connect.ts @n8n-io/nodes required
|
||||
packages/cli/src/modules/community-packages/ @n8n-io/nodes
|
||||
packages/cli/src/modules/mcp-registry/ @n8n-io/nodes required
|
||||
packages/cli/src/modules/agents/integrations/ @n8n-io/nodes
|
||||
packages/@n8n/api-types/src/schemas/mcp-registry.schema.ts @n8n-io/nodes required
|
||||
packages/cli/templates/send-and-wait-no-action-required.handlebars @n8n-io/nodes required
|
||||
packages/@n8n/ai-node-sdk/ @n8n-io/nodes required
|
||||
|
||||
# Relay (AI gateway & credits)
|
||||
packages/cli/src/services/ai-gateway.service.ts @n8n-io/relay required
|
||||
packages/cli/src/services/ai-gateway-eligibility.ts @n8n-io/relay required
|
||||
packages/cli/src/services/free-ai-credits.service.ts @n8n-io/relay required
|
||||
packages/@n8n/config/src/configs/ai-gateway.config.ts @n8n-io/relay
|
||||
packages/@n8n/api-types/src/dto/ai/ai-gateway-config-response.dto.ts @n8n-io/relay
|
||||
packages/@n8n/api-types/src/dto/ai/ai-gateway-usage-response.dto.ts @n8n-io/relay
|
||||
packages/@n8n/api-types/src/dto/ai/ai-gateway-wallet-response.dto.ts @n8n-io/relay
|
||||
packages/cli/src/modules/instance-ai/instance-ai-gateway.service.ts @n8n-io/relay
|
||||
packages/cli/src/modules/mcp/mcp-ai-gateway.helper.ts @n8n-io/relay
|
||||
packages/cli/src/modules/agents/json-config/node-tool-ai-gateway.service.ts @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/app/stores/aiGateway.store.ts @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/app/composables/useAiGateway.ts @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/features/credentials/gatewayCreditsPromotion/ @n8n-io/relay
|
||||
packages/frontend/editor-ui/src/features/shared/nodeCreator/components/SectionHeaderCreditsTag.vue @n8n-io/relay
|
||||
|
||||
# Community engineering
|
||||
.agents/skills/create-community-node-lint-rule/ @n8n-io/community-engineering
|
||||
.agents/skills/community-pr-readiness-check/ @n8n-io/community-engineering
|
||||
packages/@n8n/scan-community-package/ @n8n-io/community-engineering
|
||||
packages/@n8n/eslint-plugin-community-nodes/ @n8n-io/community-engineering
|
||||
packages/@n8n/node-cli/ @n8n-io/community-engineering
|
||||
packages/@n8n/create-node/ @n8n-io/community-engineering
|
||||
|
||||
# IAM
|
||||
|
||||
packages/@n8n/permissions/ @n8n-io/iam
|
||||
packages/@n8n/client-oauth2/ @n8n-io/iam
|
||||
|
||||
# LiGo
|
||||
|
||||
packages/extensions/insights/ @n8n-io/ligo
|
||||
packages/@n8n/inbound-auth/ @n8n-io/iam
|
||||
packages/@n8n/syslog-client/ @n8n-io/iam
|
||||
|
||||
# Governance & Observability
|
||||
|
||||
packages/modules/type-availability-policies/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/modules/type-availability-policies/ @n8n-io/governance-and-observability
|
||||
packages/modules/otel/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/modules/otel/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/controllers/otel.public.controller.ts @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/shared/otel.mapper.ts @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/controllers/node-type-policies.public.controller.ts @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/controllers/credential-type-policies.public.controller.ts @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/handlers/node-type-policies/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/public-api/v1/handlers/credential-type-policies/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/modules/policy-infrastructure/ @n8n-io/governance-and-observability
|
||||
packages/cli/src/policy/ @n8n-io/governance-and-observability
|
||||
packages/@n8n/decorators/src/policy-check/ @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/dto/otel/ @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/dto/type-availability-policies/ @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/node-type-availability.ts @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/credential-type-availability.ts @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/schemas/policy-check-failure.schema.ts @n8n-io/governance-and-observability
|
||||
packages/@n8n/api-types/src/schemas/policy-violation.schema.ts @n8n-io/governance-and-observability
|
||||
packages/@n8n/telemetry/src/events/policies.ts @n8n-io/governance-and-observability
|
||||
packages/@n8n/telemetry/src/events/type-availability-policies.ts @n8n-io/governance-and-observability
|
||||
packages/workflow/src/errors/policy-refusal.ts @n8n-io/governance-and-observability
|
||||
packages/frontend/editor-ui/src/app/composables/usePolicyViolationToast.ts @n8n-io/governance-and-observability
|
||||
|
||||
# CI/CD
|
||||
|
||||
.github/ @n8n-io/qa-dx
|
||||
.github/workflows/ @n8n-io/qa-dx required
|
||||
.github/workflows/community-pr-close-stale.yml @n8n-io/community-engineering
|
||||
.github/scripts/ @n8n-io/qa-dx required
|
||||
.github/actions/ @n8n-io/qa-dx required
|
||||
.github/poutine-rules/ @n8n-io/qa-dx required
|
||||
|
|
@ -253,8 +351,7 @@ docker/get-n8n.sh @n8n-io/qa-dx required
|
|||
|
||||
# QA
|
||||
|
||||
packages/testing/ @n8n-io/qa-dx
|
||||
packages/@n8n/benchmark/ @n8n-io/qa-dx
|
||||
packages/quality/ @n8n-io/qa-dx
|
||||
packages/@n8n/vitest-config/ @n8n-io/qa-dx
|
||||
|
||||
# Encryption boundary (IAM) — the encryption area and its guardrail rules.
|
||||
|
|
@ -267,3 +364,15 @@ packages/@n8n/db/src/entities/deployment-key.ts @n8n-io/iam required
|
|||
packages/@n8n/db/src/repositories/deployment-key.repository.ts @n8n-io/iam required
|
||||
packages/@n8n/eslint-config/src/configs/encryption-boundary.ts @n8n-io/iam required
|
||||
packages/@n8n/eslint-config/src/rules/no-encryption-guardrail-disable.ts @n8n-io/iam required
|
||||
|
||||
# AI Trust (evaluations)
|
||||
# Placed last so these rules override the broader package rules above.
|
||||
|
||||
packages/@n8n/instance-ai/evaluations/ @n8n-io/ai-trust
|
||||
packages/cli/src/modules/instance-ai/eval/ @n8n-io/ai-trust
|
||||
packages/quality/testing/playwright/tests/evals/instance-ai/ @n8n-io/ai-trust
|
||||
.agents/skills/create-instance-ai-eval/ @n8n-io/ai-trust
|
||||
.agents/skills/create-agent-builder-eval/ @n8n-io/ai-trust
|
||||
packages/cli/src/modules/agent-evals/ @n8n-io/ai-trust
|
||||
packages/@n8n/agents/src/evals/ @n8n-io/ai-trust
|
||||
packages/frontend/editor-ui/src/features/ai/evaluation.ee/ @n8n-io/ai-trust
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"repository": "n8n-io/cat-bot",
|
||||
"releaseTag": "harness-v0.3.0",
|
||||
"version": "0.3.0",
|
||||
"assetName": "n8n-opencode-harness-0.3.0.tgz"
|
||||
"releaseTag": "harness-v0.6.0",
|
||||
"version": "0.6.0",
|
||||
"assetName": "n8n-opencode-harness-0.6.0.tgz"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"**/cl100k_base.json",
|
||||
"**/o200k_base.json",
|
||||
"**/*.generated.ts",
|
||||
"**/benchmark/scripts/mock-api/**",
|
||||
"**/expectations/**"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ component_management:
|
|||
- packages/@n8n/expression-runtime/**
|
||||
- packages/@n8n/extension-sdk/**
|
||||
- packages/@n8n/imap/**
|
||||
- packages/@n8n/inbound-auth/**
|
||||
- packages/@n8n/local-gateway/**
|
||||
- packages/@n8n/permissions/**
|
||||
- packages/@n8n/scan-community-package/**
|
||||
|
|
@ -149,5 +150,5 @@ ignore:
|
|||
- (?s:.*/migrations/(postgresdb|sqlite|mysqldb)/.*\.ts.*)\Z
|
||||
- (?s:.*/scripts/.*)\Z
|
||||
# Test/build infrastructure packages
|
||||
- (?s:.*/packages/testing/.*)\Z
|
||||
- (?s:.*/packages/quality/.*)\Z
|
||||
- (?s:.*/packages/@n8n/(benchmark|eslint-config|eslint-plugin-community-nodes|stylelint-config|typescript-config|vitest-config)/.*)\Z
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ reviews:
|
|||
- docker/**
|
||||
- scripts/**
|
||||
- patches/**
|
||||
- packages/testing/**
|
||||
- packages/quality/**
|
||||
- packages/@n8n/eslint-config/**
|
||||
- packages/@n8n/stylelint-config/**
|
||||
- packages/@n8n/vitest-config/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Every n8n image build uses this file. `pnpm build:docker` and CI both drive
|
||||
# these targets, so a pin changed here changes both.
|
||||
|
||||
variable "NODE_VERSION" { default = "26.7.0" }
|
||||
variable "NODE_VERSION" { default = "26.10.0" }
|
||||
variable "N8N_VERSION" { default = "snapshot" }
|
||||
variable "N8N_RELEASE_TYPE" { default = "dev" }
|
||||
|
||||
|
|
@ -15,14 +15,14 @@ variable "BUILDER_IMAGE" { default = "" }
|
|||
variable "RUNTIME_IMAGE" { default = "" }
|
||||
|
||||
variable "PC_BUILDER_IMAGE" {
|
||||
default = "n8nio/node-pc:26.7.0-dev@sha256:c64642dcb9464e50bd08aef8504affafeb795d01aed2f7ab158976c334a36ef6"
|
||||
default = "n8nio/node-pc:26.10.0-dev@sha256:61e4b1f3b3824ffb908812e01bcbddabb64a01ef23d408d561696c344c6679e3"
|
||||
}
|
||||
variable "PC_RUNTIME_IMAGE" {
|
||||
default = "n8nio/node-pc:26.7.0@sha256:6577d0742ad043baaa39db7dda2ce8aeb73f32c84eb241c49bf716f170c53297"
|
||||
default = "n8nio/node-pc:26.10.0@sha256:386bae24d41c0091d21b4eee124a86464710fff24d5cb7f37386c4c5649b0c89"
|
||||
}
|
||||
|
||||
variable "DHI_REF" {
|
||||
default = "dhi.io/node:26.7.0-alpine3.24-dev@sha256:4b494d89fb26c950ce97865acf45b480dc7a6868fdc2b81c2d66599702eeac3f"
|
||||
default = "dhi.io/node:26.10.0-alpine3.24-dev@sha256:8da859df5dc853c923ace53c4cfe994a909a28917f7fcfc8508869beab8cf132"
|
||||
}
|
||||
|
||||
variable "N8N_TAGS" { default = "" }
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ set -eu
|
|||
SCRIPT_VERSION="1.4.0"
|
||||
# The version to install is derived from the latest stable GitHub release in
|
||||
# resolve_n8n_version(); this fallback only applies when that lookup fails.
|
||||
FALLBACK_N8N_VERSION="2.32.0"
|
||||
FALLBACK_N8N_VERSION="2.43.4"
|
||||
# Sandbox service release (API, runner and sandbox images ship together).
|
||||
# Pinned here, not resolved at run time: the stack definition must match it.
|
||||
# --upgrade moves existing installs to this pin.
|
||||
SANDBOX_VERSION="1.3.4"
|
||||
SANDBOX_VERSION="1.8.1"
|
||||
N8N_DIR="${N8N_DIR:-./n8n}"
|
||||
N8N_PORT=5678
|
||||
SOURCE_URL="https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# CI passes one DHI reference per published Node version (see the matrix in
|
||||
# build-base-image.yml). The default keeps plain `docker build` working.
|
||||
ARG DHI_REF=dhi.io/node:24.19.0-alpine3.24-dev@sha256:dc2989ad23938772abaf549ed7bde5d61d70377005229bb55bef94bff40f8bbd
|
||||
ARG DHI_REF=dhi.io/node:24.21.0-alpine3.24-dev@sha256:d22ba0e9ab2a54609ae533df4ebfd6f36c1f233b662a4c547f5a3195e8c21937
|
||||
FROM ${DHI_REF}
|
||||
|
||||
# Install all dependencies in a single layer to minimize image size
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
ARG NODE_VERSION=26.7.0
|
||||
ARG NODE_VERSION=26.10.0
|
||||
ARG N8N_VERSION=snapshot
|
||||
|
||||
# The digest pins node:26.7.0-alpine3.24 for amd64 and arm64. Change the tag and the digest together.
|
||||
ARG BUILDER_IMAGE=node:26.7.0-alpine3.24@sha256:aadf416b2cdce311a8811ba3f0608a61b77dbf997500e2eafe781b51f6a0b019
|
||||
ARG RUNTIME_IMAGE=n8nio/base:26.7.0@sha256:33687300c4e94dc00f42ec79ae15082ae07330ecd82ae1167125905b65908ff8
|
||||
# The digest pins node:26.10.0-alpine3.24 for amd64 and arm64. Change the tag and the digest together.
|
||||
ARG BUILDER_IMAGE=node:26.10.0-alpine3.24@sha256:0b36e8c136b94cd4fcf02188228e76c31ad5872eef3fec8cbd2eee500cfd9e80
|
||||
ARG RUNTIME_IMAGE=n8nio/base:26.10.0@sha256:9205c76a7745bfe71de35c2a0b7420238ccb0bdf69fc5273ab2e617fe918cf5f
|
||||
|
||||
# The runtime base has no compiler, so this stage supplies one.
|
||||
FROM ${BUILDER_IMAGE} AS toolchain
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user