Compare commits
1305
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24876bb05d | ||
|
|
0cdd478f25 | ||
|
|
0db9714eb6 | ||
|
|
9da4d167fa | ||
|
|
a1ad4be61e | ||
|
|
8cb2cffbf7 | ||
|
|
a8f009a965 | ||
|
|
0bfbd2061e | ||
|
|
c8abb11bf0 | ||
|
|
f9320b2c91 | ||
|
|
f0fd7ddb60 | ||
|
|
b77f5f1e4a | ||
|
|
b67c3a5f76 | ||
|
|
5b5a06136a | ||
|
|
6e3c9597ff | ||
|
|
3c98f96ae8 | ||
|
|
44725dde74 | ||
|
|
d3058d63bd | ||
|
|
4fc62d3b38 | ||
|
|
e14cb209a4 | ||
|
|
bb3c9929f9 | ||
|
|
35a55813d2 | ||
|
|
a247d625e5 | ||
|
|
5c7f05bd10 | ||
|
|
02e1a60cde | ||
|
|
57b573d02b | ||
|
|
9de40e8f21 | ||
|
|
cad0942f4d | ||
|
|
cb9b1b593e | ||
|
|
2a8bdc2373 | ||
|
|
ee6a07d13c | ||
|
|
2d6c9300e3 | ||
|
|
d6b4c800c2 | ||
|
|
1b37635f92 | ||
|
|
86af36429d | ||
|
|
b96ea94505 | ||
|
|
d649cccda0 | ||
|
|
5cbbd78f38 | ||
|
|
5a0ad4ecd9 | ||
|
|
1e76b9e1b8 | ||
|
|
1b972c4e09 | ||
|
|
7968d2c3c8 | ||
|
|
7507e9ba67 | ||
|
|
ca31762e26 | ||
|
|
f591da7865 | ||
|
|
f19679b217 | ||
|
|
b682591c7a | ||
|
|
8f6b59feff | ||
|
|
99833f65d8 | ||
|
|
696fc5c213 | ||
|
|
eae44cfecb | ||
|
|
dea4e66456 | ||
|
|
3cd0da303a | ||
|
|
888483a2f8 | ||
|
|
7056985f72 | ||
|
|
6ce61eae59 | ||
|
|
079af312c6 | ||
|
|
0da13dfe4d | ||
|
|
1ff4d75084 | ||
|
|
e35d23c3cb | ||
|
|
e530e84772 | ||
|
|
2257a4f1ef | ||
|
|
f465dc5090 | ||
|
|
5c1cfe6ada | ||
|
|
8d401d84c7 | ||
|
|
b74287c2ab | ||
|
|
c64d3e98c4 | ||
|
|
a3265f722e | ||
|
|
5658065b97 | ||
|
|
8fc2807194 | ||
|
|
fc7716704d | ||
|
|
71ffaacb58 | ||
|
|
cfc1cf2b8c | ||
|
|
055d9b9f0a | ||
|
|
21711bebeb | ||
|
|
becccbf288 | ||
|
|
86497045c9 | ||
|
|
687a177b24 | ||
|
|
4a6d278354 | ||
|
|
7d69302e9f | ||
|
|
bcd573e560 | ||
|
|
07c0c4e7b1 | ||
|
|
a8a2ca7b98 | ||
|
|
de47d43b65 | ||
|
|
240912cef5 | ||
|
|
72e040ead3 | ||
|
|
c0ee821d45 | ||
|
|
c7c3296572 | ||
|
|
e7be04fd58 | ||
|
|
df6b5be50a | ||
|
|
8e5f09091b | ||
|
|
0a3005701f | ||
|
|
d8571ce965 | ||
|
|
f241ae25be | ||
|
|
c6474a2a8b | ||
|
|
480d05bc48 | ||
|
|
f75725ccd9 | ||
|
|
2fe8c48255 | ||
|
|
ec5404cc9d | ||
|
|
20f62b9919 | ||
|
|
04f5555580 | ||
|
|
129af96c23 | ||
|
|
df40960f5d | ||
|
|
599960024d | ||
|
|
6805d9bfc0 | ||
|
|
928f888ef5 | ||
|
|
f46c03460e | ||
|
|
0b60338ad5 | ||
|
|
91ac465982 | ||
|
|
9490d63c50 | ||
|
|
ae538ced47 | ||
|
|
487249728b | ||
|
|
372a2e3e9c | ||
|
|
4939a9c33d | ||
|
|
b6f92f1dc4 | ||
|
|
ce276573a8 | ||
|
|
5070cc9668 | ||
|
|
a392a72960 | ||
|
|
30270b5c30 | ||
|
|
24715a9570 | ||
|
|
c530a5d272 | ||
|
|
418ee7398e | ||
|
|
78f40c0d25 | ||
|
|
2cc567c6a3 | ||
|
|
a180ab19e4 | ||
|
|
13eaa436e7 | ||
|
|
3596d12e4c | ||
|
|
e8de10a3b5 | ||
|
|
f5ab5e7eb3 | ||
|
|
0c40e1c2a0 | ||
|
|
c29d76757e | ||
|
|
91c1d3ad81 | ||
|
|
57b02e341c | ||
|
|
b94ff65e9f | ||
|
|
678260e34e | ||
|
|
739e34d08a | ||
|
|
825fb9cb89 | ||
|
|
0e1f19a380 | ||
|
|
332d1ea533 | ||
|
|
9cdeffd0b1 | ||
|
|
09ef785a20 | ||
|
|
d2788d7aba | ||
|
|
cee90a4e82 | ||
|
|
b2710b875b | ||
|
|
6464255d33 | ||
|
|
50face5760 | ||
|
|
b034449a0c | ||
|
|
a8d380bcaf | ||
|
|
bee21c9f86 | ||
|
|
cab215e209 | ||
|
|
7ae4ca9a60 | ||
|
|
d342ff1a1e | ||
|
|
4384d8910e | ||
|
|
fc773b9f57 | ||
|
|
6e1e0d9439 | ||
|
|
5c5a6e83e5 | ||
|
|
dade318f00 | ||
|
|
ebff9a3639 | ||
|
|
58b8fc21d4 | ||
|
|
e0ad088657 | ||
|
|
323b2b82e0 | ||
|
|
7d45335a32 | ||
|
|
f5d664887b | ||
|
|
5aa24c25d9 | ||
|
|
eed8d659d1 | ||
|
|
59e99ee1ae | ||
|
|
533929d314 | ||
|
|
fb07b43107 | ||
|
|
0dad426101 | ||
|
|
2a1df4de62 | ||
|
|
2b97f6c8c2 | ||
|
|
75d6511284 | ||
|
|
fd059720e5 | ||
|
|
2a5f7bb1aa | ||
|
|
0f06aa2fdd | ||
|
|
884e2b864b | ||
|
|
a728e0ac69 | ||
|
|
7d464d014c | ||
|
|
0ce0127e7e | ||
|
|
25da7ae0d9 | ||
|
|
4d6f8eba2a | ||
|
|
87268f0662 | ||
|
|
135e01930b | ||
|
|
fe86fa31ec | ||
|
|
b1f0a11d84 | ||
|
|
fbfb4b3a00 | ||
|
|
3a467d1d63 | ||
|
|
23539c5bcc | ||
|
|
9ddd98a265 | ||
|
|
ecfee2f072 | ||
|
|
345ba80942 | ||
|
|
e617435d03 | ||
|
|
5f7eb7bde9 | ||
|
|
eb41c9b769 | ||
|
|
8876efb419 | ||
|
|
adb14d23de | ||
|
|
6f1623e02a | ||
|
|
67d99723ea | ||
|
|
639e12a306 | ||
|
|
ed17b6161f | ||
|
|
baf0cf8e4e | ||
|
|
1e9c94b788 | ||
|
|
ffd336cfe8 | ||
|
|
fc4178476a | ||
|
|
6133c2ab6a | ||
|
|
603532863d | ||
|
|
a8ca0d47b9 | ||
|
|
7b1aa33ad4 | ||
|
|
5645ea0def | ||
|
|
6b1b1f3790 | ||
|
|
7c65975507 | ||
|
|
72ee50c74f | ||
|
|
8d99326fb3 | ||
|
|
2a0c098857 | ||
|
|
790ca72627 | ||
|
|
4d8b6c7dc0 | ||
|
|
473c945839 | ||
|
|
a698c60b29 | ||
|
|
24bab5fb2a | ||
|
|
93b7a81071 | ||
|
|
157e6244dd | ||
|
|
964aaad7ed | ||
|
|
92181dbe09 | ||
|
|
30deef45d9 | ||
|
|
ee28074390 | ||
|
|
1fb491337b | ||
|
|
82b0a03f5a | ||
|
|
6185016910 | ||
|
|
b4f5f4869f | ||
|
|
7ecbed3b04 | ||
|
|
5b58defd62 | ||
|
|
73196de5e1 | ||
|
|
ea5e487d3c | ||
|
|
f19702f76c | ||
|
|
092c8bca81 | ||
|
|
c50d504c44 | ||
|
|
1b4356b66a | ||
|
|
7f633622aa | ||
|
|
66f5ab4cfc | ||
|
|
0cf9597f52 | ||
|
|
60cd346fa6 | ||
|
|
56d4d54c16 | ||
|
|
9f9cb4d17e | ||
|
|
7d0d9019d8 | ||
|
|
d646bcf257 | ||
|
|
e3b45a48eb | ||
|
|
848c15a265 | ||
|
|
be8627233d | ||
|
|
1fe8b7fb1d | ||
|
|
5a585c8618 | ||
|
|
cc9b90a5ae | ||
|
|
b64d4b53ca | ||
|
|
5cdf4e405b | ||
|
|
7cb14cb4cc | ||
|
|
de38bba99b | ||
|
|
f04d809426 | ||
|
|
7ed3c7c500 | ||
|
|
77f1aeb1ac | ||
|
|
7bc5c89e3c | ||
|
|
718ab8433e | ||
|
|
8f197c5a0a | ||
|
|
0295862d0d | ||
|
|
2b2a5824c1 | ||
|
|
468cc19e68 | ||
|
|
77333e57a7 | ||
|
|
f52491e2c1 | ||
|
|
05408af8a1 | ||
|
|
d3ae074456 | ||
|
|
0b48a7e991 | ||
|
|
809f513ccb | ||
|
|
d9e90d0fa0 | ||
|
|
d1417bbe4b | ||
|
|
2565637e36 | ||
|
|
cae9923e5a | ||
|
|
a328bbbced | ||
|
|
5276eb689b | ||
|
|
4b2badb6f2 | ||
|
|
34a89416f7 | ||
|
|
a13ab76002 | ||
|
|
b04b4449db | ||
|
|
674cdc3521 | ||
|
|
2031d31ee8 | ||
|
|
04d62867af | ||
|
|
7f392b6950 | ||
|
|
b0a3399774 | ||
|
|
2d5186fb28 | ||
|
|
06f076e0ff | ||
|
|
5b79f7e99d | ||
|
|
1cee1a25b6 | ||
|
|
c0f237bf35 | ||
|
|
75d7fc0526 | ||
|
|
c057b5c5ff | ||
|
|
5468c4ec96 | ||
|
|
f4c02e4c6b | ||
|
|
9dc95eeb20 | ||
|
|
76bba64b79 | ||
|
|
59e96fbb2a | ||
|
|
06ea0f7ac2 | ||
|
|
730a0bef9e | ||
|
|
2eb37caf2e | ||
|
|
7e8147295b | ||
|
|
c07686928a | ||
|
|
d1238180ed | ||
|
|
969760364d | ||
|
|
ceabfeb3a7 | ||
|
|
c407f40e0d | ||
|
|
28818f2e2a | ||
|
|
e2c52c9b0f | ||
|
|
1925d58369 | ||
|
|
b79fc5d6b4 | ||
|
|
6649e4025e | ||
|
|
b96f372f45 | ||
|
|
127fbf2c9a | ||
|
|
3c70d28064 | ||
|
|
cd4d6f8a22 | ||
|
|
9d0906c684 | ||
|
|
41b6f894c0 | ||
|
|
e7e6fe8813 | ||
|
|
c0bdd6792f | ||
|
|
27b084c4d4 | ||
|
|
3f7a68fc77 | ||
|
|
a252fbddfa | ||
|
|
ff02636a4b | ||
|
|
63946d829e | ||
|
|
cdcfd2ef2c | ||
|
|
b04a3851cc | ||
|
|
b41338cd08 | ||
|
|
28153df4d3 | ||
|
|
3bc3386535 | ||
|
|
7654f14241 | ||
|
|
194b54bae4 | ||
|
|
0e16d36edb | ||
|
|
432a6412a3 | ||
|
|
55d05fe52d | ||
|
|
0d500e6965 | ||
|
|
5798610f27 | ||
|
|
a35b28dbef | ||
|
|
1a4288c811 | ||
|
|
9dc32f2318 | ||
|
|
7210f856c9 | ||
|
|
ebcc1200a3 | ||
|
|
e660d7af38 | ||
|
|
d9ccfcbc6e | ||
|
|
a9bcec013f | ||
|
|
aeb7687e2c | ||
|
|
9355d36718 | ||
|
|
a03ee828a3 | ||
|
|
7066372892 | ||
|
|
55f95dbc36 | ||
|
|
8b40de3c4e | ||
|
|
af4b9bfa8f | ||
|
|
b9e3130388 | ||
|
|
12d33652b6 | ||
|
|
fe8cf2aff4 | ||
|
|
d1d190374d | ||
|
|
e1be4e6aa8 | ||
|
|
301a470e7a | ||
|
|
91251ad5a5 | ||
|
|
3f6644a615 | ||
|
|
5edc682c4a | ||
|
|
13c00ecfc4 | ||
|
|
9d545144ce | ||
|
|
2afa39cdcb | ||
|
|
bb1c883be4 | ||
|
|
03861bcee3 | ||
|
|
c34fc429ae | ||
|
|
d110112863 | ||
|
|
934a20e745 | ||
|
|
7e56a244a8 | ||
|
|
6facd9360c | ||
|
|
a18d7f51eb | ||
|
|
680ef077ae | ||
|
|
c26be9d3f4 | ||
|
|
51a8f79d67 | ||
|
|
bb73776339 | ||
|
|
9424bf60b0 | ||
|
|
cbedcd2882 | ||
|
|
1a93af5cd0 | ||
|
|
cd90d7ffc1 | ||
|
|
4bb987eca3 | ||
|
|
4fd4615c56 | ||
|
|
c7d30bf09a | ||
|
|
59dab7deac | ||
|
|
a60cb3b800 | ||
|
|
6164408da1 | ||
|
|
7fc40e6c9e | ||
|
|
d625ac0bf1 | ||
|
|
1082f488a1 | ||
|
|
f1c4c1a5ff | ||
|
|
dd1cdbbd41 | ||
|
|
74a04afe27 | ||
|
|
b108ea42f6 | ||
|
|
1aa6188b7d | ||
|
|
bd0d10ac5c | ||
|
|
2162ea6a68 | ||
|
|
153064bbd4 | ||
|
|
a643b05368 | ||
|
|
279b66bc7f | ||
|
|
e134c1e0d5 | ||
|
|
9127209dd5 | ||
|
|
a2ee151e48 | ||
|
|
9e3e616391 | ||
|
|
837b5cad86 | ||
|
|
1a011dc14a | ||
|
|
bf117dd0c8 | ||
|
|
1e6dc62470 | ||
|
|
0b70eec695 | ||
|
|
e8dc706414 | ||
|
|
9a2bea9287 | ||
|
|
b95e6f6a7a | ||
|
|
b99ba74aa4 | ||
|
|
7b5c371b9d | ||
|
|
c67ce6f66d | ||
|
|
e48d7bb097 | ||
|
|
24ea21db25 | ||
|
|
8581a68174 | ||
|
|
f720a3bed2 | ||
|
|
4a56763d2f | ||
|
|
861f73267c | ||
|
|
1efd365b62 | ||
|
|
65c36a51ef | ||
|
|
19476109da | ||
|
|
f3eb3ab4dd | ||
|
|
2c9e30426d | ||
|
|
2bb1f0906b | ||
|
|
d5ad6aedc0 | ||
|
|
5ebeb34feb | ||
|
|
c5ac191a79 | ||
|
|
140ad6ba4e | ||
|
|
e03eb3a76c | ||
|
|
38a419d073 | ||
|
|
c74cbb68da | ||
|
|
271019006e | ||
|
|
19bf36a716 | ||
|
|
48d27e250b | ||
|
|
d06b5529b3 | ||
|
|
8132c444dc | ||
|
|
cb0356e9d7 | ||
|
|
4d80892d7b | ||
|
|
af754f497a | ||
|
|
8f93bb36ba | ||
|
|
82f24b336d | ||
|
|
927a17804b | ||
|
|
29f34848cd | ||
|
|
1b0d4637b3 | ||
|
|
936a09c704 | ||
|
|
5cc62fd1c9 | ||
|
|
7bc19d8251 | ||
|
|
e845475408 | ||
|
|
9a8aa6a0c3 | ||
|
|
76a7f5f4b9 | ||
|
|
2ff50514c8 | ||
|
|
7901ac9a97 | ||
|
|
ecd830083a | ||
|
|
203b3a9499 | ||
|
|
9331024d91 | ||
|
|
c6a5de3c18 | ||
|
|
cd3327013a | ||
|
|
cd66559ebf | ||
|
|
8b77ec7f31 | ||
|
|
bb3de5dd32 | ||
|
|
1e2d309122 | ||
|
|
a24ec60e51 | ||
|
|
8fd616d27f | ||
|
|
e5bdc40dce | ||
|
|
376c43e5ac | ||
|
|
3ebb449d25 | ||
|
|
5297ac76ec | ||
|
|
bbed1d4a7c | ||
|
|
c804dbed8c | ||
|
|
00bf3f83f2 | ||
|
|
7e6745e105 | ||
|
|
d648ce6888 | ||
|
|
f3c3534e33 | ||
|
|
8967ff34b3 | ||
|
|
57539792c1 | ||
|
|
03e227f8f1 | ||
|
|
506e1a8bc7 | ||
|
|
f8873ec07b | ||
|
|
b2dacf0718 | ||
|
|
70eb98d6c5 | ||
|
|
b83f296634 | ||
|
|
5c68f12bb8 | ||
|
|
4df7c00859 | ||
|
|
995c43f3dd | ||
|
|
c0431ec843 | ||
|
|
a0af10abc8 | ||
|
|
8e2b8168be | ||
|
|
1f29565673 | ||
|
|
90fe54ca9e | ||
|
|
b43ebf539d | ||
|
|
853b859032 | ||
|
|
8f3e42e9c2 | ||
|
|
1359c03216 | ||
|
|
4b7dc17546 | ||
|
|
81090effe2 | ||
|
|
d92c336394 | ||
|
|
cd9daef564 | ||
|
|
2876839d7e | ||
|
|
7ba408eebe | ||
|
|
3708e3eef1 | ||
|
|
ff5c2c57a1 | ||
|
|
955c25589d | ||
|
|
54bde0bdf6 | ||
|
|
87add9a4f3 | ||
|
|
574d5865f4 | ||
|
|
458fab1c48 | ||
|
|
88196c186e | ||
|
|
dcf21a6a84 | ||
|
|
91f92c7083 | ||
|
|
0ca339103f | ||
|
|
5cf741895f | ||
|
|
11c52e90f6 | ||
|
|
f01e099729 | ||
|
|
195ff4711d | ||
|
|
fe2f7a8920 | ||
|
|
3b1458c08f | ||
|
|
9f47317032 | ||
|
|
e751ec323e | ||
|
|
f1d72eb5d2 | ||
|
|
44242d03b4 | ||
|
|
ed7ea68f7d | ||
|
|
afbc30c9ed | ||
|
|
0e55dcb297 | ||
|
|
25973c7d77 | ||
|
|
73ecdd5494 | ||
|
|
6fafeec415 | ||
|
|
d23cefe005 | ||
|
|
16d408d908 | ||
|
|
0536549f73 | ||
|
|
d0956039e7 | ||
|
|
38eb04dc98 | ||
|
|
d2e1da269c | ||
|
|
1d3498f659 | ||
|
|
b8dea56198 | ||
|
|
e2becd6746 | ||
|
|
28a26f2d59 | ||
|
|
8c7393ef46 | ||
|
|
5a7a955210 | ||
|
|
3e4849d765 | ||
|
|
0c280ef708 | ||
|
|
282561a861 | ||
|
|
cbb4cc5d76 | ||
|
|
2d6babeeb4 | ||
|
|
1065a4840a | ||
|
|
b6aa5a7d69 | ||
|
|
949f930698 | ||
|
|
65a08ed7ab | ||
|
|
cc4d6db7c8 | ||
|
|
6b5d6dacb2 | ||
|
|
89bf75eba9 | ||
|
|
3a28868a6c | ||
|
|
4036515abe | ||
|
|
6c089cab66 | ||
|
|
818a71d637 | ||
|
|
3db107edc9 | ||
|
|
2677d90860 | ||
|
|
859756c4f6 | ||
|
|
295fb6e74a | ||
|
|
2326fb7a83 | ||
|
|
2d6eaf69f9 | ||
|
|
3e826c0000 | ||
|
|
b1b977e284 | ||
|
|
23648141c9 | ||
|
|
d6dee43c09 | ||
|
|
7efc887e32 | ||
|
|
8b346e69d9 | ||
|
|
ef7ff3356d | ||
|
|
7b5c0b5045 | ||
|
|
f00512dd5d | ||
|
|
e6ef774fd5 | ||
|
|
ce50c6cf1c | ||
|
|
7002512106 | ||
|
|
c3aebb8403 | ||
|
|
0baefa6163 | ||
|
|
7bcedcbaab | ||
|
|
791fc5819d | ||
|
|
2d09c4788d | ||
|
|
9bd5c2f8ec | ||
|
|
5e336c47fd | ||
|
|
be4c828214 | ||
|
|
ec450eb7f9 | ||
|
|
48e13f65dc | ||
|
|
38fc2a6574 | ||
|
|
ed8d3f3e8d | ||
|
|
0c8dec3315 | ||
|
|
38e831c1b3 | ||
|
|
1c5d62d98a | ||
|
|
6b4736bf78 | ||
|
|
c9503fd818 | ||
|
|
91a1df96cb | ||
|
|
5b2c5da945 | ||
|
|
b59ecea346 | ||
|
|
61c0948136 | ||
|
|
f746c7bdf2 | ||
|
|
2a3deee385 | ||
|
|
4b6803ba06 | ||
|
|
4c908c8f39 | ||
|
|
afec528f51 | ||
|
|
491061b8f4 | ||
|
|
8b1533438f | ||
|
|
ba924fc97b | ||
|
|
712e522220 | ||
|
|
33eebe8cfc | ||
|
|
2e1b11bdb2 | ||
|
|
d65a6b4810 | ||
|
|
44a91e344c | ||
|
|
0fec9af6a6 | ||
|
|
5e5113e08e | ||
|
|
73f9a9e7d6 | ||
|
|
48d23cd744 | ||
|
|
0b60bf6ef0 | ||
|
|
3b24d8d2d1 | ||
|
|
051ba99cd2 | ||
|
|
dc83e8aa09 | ||
|
|
77f8f2babb | ||
|
|
77d6c108e7 | ||
|
|
c2a5962023 | ||
|
|
d583b1b835 | ||
|
|
da00de6688 | ||
|
|
a633387e9b | ||
|
|
df389eba1c | ||
|
|
5cae61eb5a | ||
|
|
ba8e0681d5 | ||
|
|
de123a8695 | ||
|
|
21e5962f98 | ||
|
|
db60e649b8 | ||
|
|
e561788809 | ||
|
|
3cd6ef4464 | ||
|
|
39dc636b02 | ||
|
|
4f03b7193e | ||
|
|
0d921cd21d | ||
|
|
1a7e46368e | ||
|
|
8c8ad02a6f | ||
|
|
8f070f2190 | ||
|
|
c7641bb1ce | ||
|
|
97bb338e7d | ||
|
|
ceec00e172 | ||
|
|
d81444683b | ||
|
|
3627c1e720 | ||
|
|
ba5d8c1b29 | ||
|
|
26e50bb084 | ||
|
|
b351ab00a2 | ||
|
|
059d605a6d | ||
|
|
f8a4205ac4 | ||
|
|
25dd79fba7 | ||
|
|
0c8acd402a | ||
|
|
3e876e173a | ||
|
|
ae9c4244d6 | ||
|
|
3c7d6739b5 | ||
|
|
560195f9f4 | ||
|
|
9ce7464c79 | ||
|
|
f980d5d3be | ||
|
|
96122692cb | ||
|
|
b9c300d570 | ||
|
|
dfcc0f8863 | ||
|
|
f56ce9d3b1 | ||
|
|
bd25240123 | ||
|
|
0020aa8f59 | ||
|
|
e70e4fa41d | ||
|
|
881a9a1a08 | ||
|
|
a74e12809b | ||
|
+4 |
eaddd4a132 | ||
|
|
0c41d0bf51 | ||
|
|
90e281c8da | ||
|
|
90d638fba3 | ||
|
|
f5e9b02565 | ||
|
|
de15e5b449 | ||
|
+4 |
b6c7581a31 | ||
|
|
af8ffa0654 | ||
|
|
4d79b4a766 | ||
|
|
af21dc7df8 | ||
|
|
dc3f992e6e | ||
|
|
13a9359191 | ||
|
|
f07f9ee7a3 | ||
|
|
7396230223 | ||
|
|
ed14f58697 | ||
|
|
37714cb44e | ||
|
|
a5ce17009f | ||
|
|
b565a51ed9 | ||
|
|
6ca066983d | ||
|
|
40e040ca1a | ||
|
|
4289cb2634 | ||
|
|
c966e281d4 | ||
|
|
abb84f1c38 | ||
|
|
e507675860 | ||
|
|
3445469385 | ||
|
|
3193e8a712 | ||
|
|
e8af6a6b3b | ||
|
|
7f4fe4d064 | ||
|
|
a1f990584b | ||
|
|
54e51be665 | ||
|
|
0fea760143 | ||
|
|
c3eff6abdc | ||
|
|
c661d5c43a | ||
|
|
b665eaa015 | ||
|
|
b08665e598 | ||
|
|
883d757392 | ||
|
|
25df902ec4 | ||
|
|
5956dd79df | ||
|
|
70556d9386 | ||
|
|
9fa50774b4 | ||
|
|
731414a44f | ||
|
|
d42d08aa57 | ||
|
|
987b5f4bf4 | ||
|
|
665978a602 | ||
|
|
8baa864c35 | ||
|
|
53a22aa41b | ||
|
|
cf4d7afb9c | ||
|
|
e6b5923ff1 | ||
|
|
538093855b | ||
|
|
e6b8cbe657 | ||
|
|
af7d5e60b4 | ||
|
|
dbceb3067e | ||
|
|
425457cb16 | ||
|
|
e5bd18132c | ||
|
|
2f33867d07 | ||
|
|
fd71c56f16 | ||
|
|
e3c2116501 | ||
|
|
fb17339d89 | ||
|
|
9fd196642d | ||
|
|
98897a5379 | ||
|
|
5542329554 | ||
|
|
79332c0e5e | ||
|
|
50a55513d4 | ||
|
|
3bccdd6c9a | ||
|
|
76af80e332 | ||
|
|
7a880ae60c | ||
|
|
5bc0f9513b | ||
|
|
b77801ece9 | ||
|
|
7de92c598f | ||
|
|
693080aa12 | ||
|
|
25c388d0db | ||
|
|
b1722c8af9 | ||
|
|
b65a5fcd97 | ||
|
|
1c3cba281a | ||
|
|
800954f8ce | ||
|
|
f66a3c49c4 | ||
|
|
ef396ac84e | ||
|
|
7e7b27fdec | ||
|
|
9c90c1c455 | ||
|
|
b1df52b8ff | ||
|
|
e527b7c5f1 | ||
|
|
149b9d4c0f | ||
|
|
ef28a63ad3 | ||
|
|
e78558bc06 | ||
|
|
f63d7c4121 | ||
|
|
ef062fb397 | ||
|
|
a2ea7ca039 | ||
|
|
6876cd787b | ||
|
|
50a6892c3a | ||
|
|
1bcc7f78c7 | ||
|
|
2fd5b76ac1 | ||
|
|
62f42b3f24 | ||
|
|
2c58b424a1 | ||
|
|
381c518b23 | ||
|
|
ebf741114d | ||
|
|
648dde5e96 | ||
|
|
a3042e6332 | ||
|
|
e5fd3133f4 | ||
|
|
e1bbe57f9c | ||
|
|
d4783e8c14 | ||
|
|
736880e046 | ||
|
|
bd7a9b5fcf | ||
|
|
9a47bb2f80 | ||
|
|
d7ad2baf79 | ||
|
|
a951cc996b | ||
|
|
173e0d6f35 | ||
|
|
62bb830338 | ||
|
|
f7c6270f74 | ||
|
|
711fe6ba2c | ||
|
|
fbedb60371 | ||
|
|
974d2f1627 | ||
|
|
ed401728eb | ||
|
|
fc389a54c5 | ||
|
|
c8b372dba0 | ||
|
|
2333d75c56 | ||
|
|
2ef9a8a769 | ||
|
|
21ab9b9d8c | ||
|
|
79c1473378 | ||
|
|
93b8a74351 | ||
|
|
28185170b0 | ||
|
|
178883b4cc | ||
|
|
e9f9041b25 | ||
|
|
175290fa04 | ||
|
|
b0c4d8c541 | ||
|
|
0f643bca76 | ||
|
|
eeebedcfe8 | ||
|
|
2f682780fa | ||
|
|
ed83f5369e | ||
|
|
4ee1bd5f32 | ||
|
|
1c2bbed405 | ||
|
|
d573fc0e65 | ||
|
|
f8b249e649 | ||
|
|
fbcab757d5 | ||
|
|
c0e998ef6e | ||
|
|
84f25807db | ||
|
|
83b242be7b | ||
|
|
a12d740a5d | ||
|
|
3bbb014dc7 | ||
|
|
f040733e28 | ||
|
|
b0bf7ca486 | ||
|
|
14d83c8bac | ||
|
|
8b506dfa42 | ||
|
|
ac2258c2dc | ||
|
|
3c279edcf2 | ||
|
|
9ed8a5ed73 | ||
|
|
3d4ddf4a6f | ||
|
|
4e0273bb28 | ||
|
|
7056d2ae99 | ||
|
|
d8fbc00cb9 | ||
|
|
57c5f0ec87 | ||
|
|
e5bd80c719 | ||
|
|
25a33a454c | ||
|
|
bd30784b1d | ||
|
|
28fce0a890 | ||
|
|
e1eb582bea | ||
|
|
2042ee453b | ||
|
|
33c4e512f1 | ||
|
|
253e8a3f98 | ||
|
|
06b63d65d1 | ||
|
|
08f3133414 | ||
|
|
d412cddf39 | ||
|
|
671c5cdd84 | ||
|
|
554f060092 | ||
|
|
e243e8d8a3 | ||
|
|
1b935a367f | ||
|
|
2edd083a71 | ||
|
|
dd50a68bf2 | ||
|
|
e8dd3461e8 | ||
|
|
8dd4473432 | ||
|
|
b5bbbdd840 | ||
|
|
f0266e13c5 | ||
|
|
ae898652b2 | ||
|
|
c34f67495c | ||
|
|
815c536e05 | ||
|
|
fc64427ae1 | ||
|
|
11c518478e | ||
|
|
e823635ce1 | ||
|
|
98e74c8fde | ||
|
|
29bfa33d59 | ||
|
|
3ead0beeb1 | ||
|
|
2108c44c8b | ||
|
|
b0079e55b4 | ||
|
|
d9f54f8bd7 | ||
|
|
5a446f8200 | ||
|
|
f4d5e2f43d | ||
|
|
9121f24181 | ||
|
|
7dd507af04 | ||
|
|
3b9aad2ba7 | ||
|
|
ea9f74b581 | ||
|
|
e37aaa482d | ||
|
|
a3170f744c | ||
|
|
ced3780787 | ||
|
|
6faf26683c | ||
|
|
8ac9cbf733 | ||
|
|
098ed34469 | ||
|
|
6cf4d1002f | ||
|
|
a111d56ea3 | ||
|
|
8436470fcb | ||
|
|
17da0e4146 | ||
|
|
ea41e9ab4e | ||
|
|
5770b5feef | ||
|
|
b5259a3a85 | ||
|
|
596559efc9 | ||
|
|
b7b03f8594 | ||
|
|
61ef255809 | ||
|
|
08426376ac | ||
|
|
d0262c899e | ||
|
|
152433d88a | ||
|
|
dece58d1a5 | ||
|
|
70be474aac | ||
|
|
a852cbe7f2 | ||
|
|
7df38d35c1 | ||
|
|
ef29a5ee3d | ||
|
|
9a7fe7ef16 | ||
|
|
8c4ea5c898 | ||
|
|
d06bc2f2e1 | ||
|
|
534fea7104 | ||
|
|
bc2b9eec58 | ||
|
|
88c38ddeb3 | ||
|
|
602753b68a | ||
|
|
44ebfa3bb8 | ||
|
|
6bacf7f953 | ||
|
|
095962f13e | ||
|
|
0862fd74b0 | ||
|
|
e0139f91c8 | ||
|
|
64920ef648 | ||
|
|
7ba70869aa | ||
|
|
f67297688f | ||
|
|
0841b4c663 | ||
|
|
440602f52a | ||
|
|
510120410b | ||
|
|
4ef67fef3a | ||
|
|
45561bed9d | ||
|
|
7bd5e80323 | ||
|
|
7515eee0a8 | ||
|
|
452067db19 | ||
|
|
859920a81f | ||
|
|
34ce3cac70 | ||
|
|
12814b55d2 | ||
|
|
50206ae8a7 | ||
|
|
169184ac9b | ||
|
|
33bc58c9c2 | ||
|
|
65d66768c1 | ||
|
|
c960f7ae48 | ||
|
|
d5104a4268 | ||
|
|
9069c01f9c | ||
|
|
0ff41a1127 | ||
|
|
7192af41e4 | ||
|
|
5ec387b644 | ||
|
|
4be479fa06 | ||
|
|
e0d69204cd | ||
|
|
f2d6275da4 | ||
|
|
992ac38d0d | ||
|
|
f962e61315 | ||
|
|
b3aebb71ff | ||
|
|
98d3bcd079 | ||
|
|
1703df5c00 | ||
|
|
674495680d | ||
|
|
04f5fe5e38 | ||
|
|
1b7d0bd4e6 | ||
|
|
66183c1f0a | ||
|
|
130ad295d0 | ||
|
|
6612ba69b1 | ||
|
|
2dc015b360 | ||
|
|
2eb43b1e1f | ||
|
|
8633b2f1f7 | ||
|
|
c9f525a3b2 | ||
|
|
e224c77920 | ||
|
|
28b8215c9b | ||
|
|
98eedf14dc | ||
|
|
cd3ee5bd5d | ||
|
|
26e8f1f876 | ||
|
|
af55665ff2 | ||
|
|
bcd738d2e6 | ||
|
|
488fcd4f83 | ||
|
|
5d4d60bb95 | ||
|
|
7f4bf19186 | ||
|
|
07c573e52f | ||
|
|
660c7e4a43 | ||
|
|
5fafac0ca4 | ||
|
|
c41b62f47e | ||
|
|
f42c1b68a4 | ||
|
|
b90fe73c96 | ||
|
|
d19f47b458 | ||
|
|
86fd94767c | ||
|
|
d826ac7099 | ||
|
|
40e23ce8dc | ||
|
|
d50f096b14 | ||
|
|
1117b6e72d | ||
|
|
c5c5c71d15 | ||
|
|
a31c1d2c69 | ||
|
|
2352269ba9 | ||
|
|
985e71ebf4 | ||
|
|
4d57f04a26 | ||
|
|
ab3b305682 | ||
|
|
b8858708be | ||
|
|
0a6494abfb | ||
|
|
75b88a5416 | ||
|
|
e143dbce50 | ||
|
|
8ad131bb3b | ||
|
|
41eeb1f2e7 | ||
|
|
02c1bfc3e7 | ||
|
|
d042cbc62e | ||
|
|
03750b76ac | ||
|
|
1befd2a602 | ||
|
|
d1e33ba9ea | ||
|
|
7d793e12c8 | ||
|
|
1ce6e279f0 | ||
|
|
ec56f4e839 | ||
|
|
d5ababfed0 | ||
|
|
8225f98565 | ||
|
|
4c05316a7b | ||
|
|
66e588c8ca | ||
|
|
9a51c2f56a | ||
|
|
ee789db443 | ||
|
|
d360929af1 | ||
|
|
5f53748d07 | ||
|
|
e3cc4b83c8 | ||
|
|
b5acc8e392 | ||
|
|
f5ea61e93e | ||
|
|
a76a8876d1 | ||
|
|
be1f4b34f8 | ||
|
|
c19a822e1b | ||
|
|
8782787a9e | ||
|
|
4c6b8f9229 | ||
|
|
51dcf4ce84 | ||
|
|
27e484e7f8 | ||
|
|
9308287fea | ||
|
|
7ca5b726a2 | ||
|
|
0bdd1267fb | ||
|
|
3ea88dfc7f | ||
|
|
2275c5b1a3 | ||
|
|
1c7cf44af4 | ||
|
|
3325392cc5 | ||
|
|
fd2843b0fb | ||
|
|
1898a3f8a5 | ||
|
|
9c4f897b9a | ||
|
|
47b9d48f70 | ||
|
|
ce68f2cdc6 | ||
|
|
a8fa552b3a | ||
|
|
bd257777a0 | ||
|
|
e6715a2dbe | ||
|
|
8f46c9113c | ||
|
|
5aa4e23f54 | ||
|
|
5821511114 | ||
|
|
d6d9b04c41 | ||
|
|
540289e6c6 | ||
|
|
1d971d3240 | ||
|
|
02a9f0abca | ||
|
|
289f091bf9 | ||
|
|
1a4eb47e1d | ||
|
|
4c70bfa8b8 | ||
|
|
3a4756449a | ||
|
|
55b7ea04a7 | ||
|
|
ccfc8c6f15 | ||
|
|
4fb3fab82d | ||
|
|
3cea0dfb07 | ||
|
|
0d6db3a3f3 | ||
|
|
3d5a81bd30 | ||
|
|
208604a3a8 | ||
|
|
63bfba0bdb | ||
|
|
9948a51b14 | ||
|
|
0e0bb3582f | ||
|
|
546062d2cd | ||
|
|
aad0b3c157 | ||
|
|
4d4265f531 | ||
|
|
e138523123 | ||
|
|
a65e1f71b4 | ||
|
|
909c062ee1 | ||
|
|
f5322e45fc | ||
|
|
017f09f1e9 | ||
|
|
0ba66ab155 | ||
|
|
5cd267d755 | ||
|
|
d30946dabf | ||
|
|
b0e524213e | ||
|
|
b1adb5652e | ||
|
|
c825d5dcf6 | ||
|
|
2127d5850f | ||
|
|
ae9fcc2969 | ||
|
|
624db69f12 | ||
|
|
80a7843f45 | ||
|
|
cb55176612 | ||
|
|
5aa2524d33 | ||
|
|
2575a3a3ab | ||
|
|
f8f7b0ec1a | ||
|
|
d2ee486900 | ||
|
|
c44ddd9831 | ||
|
|
e645cbd8f8 | ||
|
|
485fc2c416 | ||
|
|
f09be969bb | ||
|
|
597a0b4d9f | ||
|
|
779cce3c61 | ||
|
|
b5d9a71cf9 | ||
|
|
c2af415450 | ||
|
|
89ce61cfea | ||
|
|
05c5327f47 | ||
|
|
3891c0a255 | ||
|
|
63b1d0c1ea | ||
|
|
75ed38fb3d | ||
|
|
63db9a7a2f | ||
|
|
19c80f0f0e | ||
|
|
c5a0bde3ec | ||
|
|
1261e5e5e8 | ||
|
|
e2ecd68556 | ||
|
|
bceb0eee9b | ||
|
|
173e818a62 | ||
|
|
84d8940dbf | ||
|
|
3e995e6a6d | ||
|
|
459c36f21b | ||
|
|
72adb5468c | ||
|
|
1194957fde | ||
|
|
68bd29eda2 | ||
|
|
f67a811f7f | ||
|
|
b9c122e7f4 | ||
|
|
396b39dff9 | ||
|
|
ac8bd12609 | ||
|
|
b55bef4438 | ||
|
|
2f9667de76 | ||
|
|
a7b6307d32 | ||
|
|
2883ad6764 | ||
|
|
0feff5b048 | ||
|
|
0bce6b35b4 | ||
|
|
89e23456f0 | ||
|
|
a39173c227 | ||
|
|
12e93d374f | ||
|
|
922f9242e4 | ||
|
|
7fc0a791a2 | ||
|
|
8d37116fec | ||
|
|
4b500f988d | ||
|
|
5ad906ea6a | ||
|
|
5b862a43e0 | ||
|
|
1e5cd69205 | ||
|
|
9081c46565 | ||
|
|
40b252be8c | ||
|
|
ba1357038a | ||
|
|
46d1f4c338 | ||
|
|
9c880dd650 | ||
|
|
01ba0e050f | ||
|
|
ccc4aae94e | ||
|
|
01242e13d7 | ||
|
|
938ee27e42 | ||
|
|
a101f72153 | ||
|
|
40642433d8 | ||
|
|
8979181d5e | ||
|
|
c17c6b5c35 | ||
|
|
e83a4090ac | ||
|
|
b71b9f80b9 | ||
|
|
ee87289917 | ||
|
|
5ad8c3e249 | ||
|
|
8b992513b8 | ||
|
|
eca0cdc7a9 | ||
|
|
779e6b8e0b | ||
|
|
c2428361c4 | ||
|
|
68e4d13f36 | ||
|
|
cb9f4bb100 | ||
|
|
8a398f3105 | ||
|
|
0f051d5886 | ||
|
|
e85d9a0d72 | ||
|
|
06dde4f503 | ||
|
|
83d4176785 | ||
|
|
c94951b2f8 | ||
|
|
a9cf8f6c5d | ||
|
|
64ddec0d67 | ||
|
|
da3b0caf5e | ||
|
|
4fedd43af5 | ||
|
|
a263f28e19 | ||
|
|
d53862f135 | ||
|
|
608958de1c | ||
|
|
7eb632eb34 | ||
|
|
33d4fd357c | ||
|
|
e55bd61c17 | ||
|
|
f2fc213d52 | ||
|
|
f814579ed2 | ||
|
|
71d299d0d3 | ||
|
|
e178451d04 | ||
|
|
9a6222f245 | ||
|
|
affe5ed30b | ||
|
|
4cc5401d7e | ||
|
|
36e840cd87 | ||
|
|
985b41c40b | ||
|
|
2e29ac2829 | ||
|
|
dbfb474eab | ||
|
|
d243de26ec | ||
|
|
894826771a | ||
|
|
a3386da5d6 | ||
|
|
318a3d0308 | ||
|
|
5bafb163cc | ||
|
|
52b1bc5b09 | ||
|
|
1873b22e96 | ||
|
|
9a8c853a2e | ||
|
|
e54383d0fe | ||
|
|
43c48ba4d7 | ||
|
|
8f9dbf269e | ||
|
|
cb9ee5903a | ||
|
|
cd406d2794 | ||
|
|
993a301468 | ||
|
|
399d3f8da5 | ||
|
|
f9d9ad7a38 | ||
|
|
2d29345f26 | ||
|
|
725f9e3dc4 | ||
|
|
4e1d060439 | ||
|
|
391007d02e | ||
|
|
e41965061c | ||
|
|
2b9eb06555 | ||
|
|
31f7752ba9 | ||
|
|
b23ea0397a | ||
|
|
c51cd42cb4 | ||
|
|
09ee8ea1f5 | ||
|
|
beda78e911 | ||
|
|
42d7623cc6 | ||
|
|
4bd388669a | ||
|
|
324b47507c | ||
|
|
d2baacdd4b | ||
|
|
57f358a96b | ||
|
|
19530e880a | ||
|
|
dbdbb098d5 | ||
|
|
2c8b47ce44 | ||
|
|
cf50d7c7b5 | ||
|
|
d9a0665b2c | ||
|
|
b818cc0766 | ||
|
|
90f94be2b3 | ||
|
|
24111facdd | ||
|
|
424d34a9c0 | ||
|
|
fbd2d31624 | ||
|
|
b54a0dc1e4 | ||
|
|
f27d669f87 | ||
|
|
fcf04629d3 | ||
|
|
6b0c6d0cde | ||
|
|
1063e021f2 | ||
|
|
303f548408 | ||
|
|
cc68f0e640 | ||
|
|
9b7b432e08 | ||
|
|
88863609e9 | ||
|
|
adc6c6c13b | ||
|
|
2de818530b | ||
|
|
7e4754392d | ||
|
|
01c857a67a | ||
|
|
2c2cc72150 | ||
|
|
f7b78b08fd | ||
|
|
f0e6f11c1c | ||
|
|
a19243068b | ||
|
|
323c51e095 | ||
|
|
bbc3f90928 | ||
|
|
1344c3b280 | ||
|
|
5897b28355 | ||
|
|
15aa8071f8 | ||
|
|
097095a69b | ||
|
|
daebe26089 | ||
|
|
c58170f5b8 | ||
|
|
3a7885819d | ||
|
|
5fc4dfaf7b | ||
|
|
953bcc33b1 | ||
|
|
bc14ad6a8f | ||
|
|
cc89b57c1f | ||
|
|
623c8ae803 | ||
|
|
dede190be2 | ||
|
|
a1513f06c3 | ||
|
|
3c7180bfd5 | ||
|
|
51f6ca2bed | ||
|
|
ae9a16a397 | ||
|
|
52a4bea88f | ||
|
|
1aaba80211 | ||
|
|
944db46d4f | ||
|
|
456684dfc3 | ||
|
|
40fa0f365c | ||
|
|
2cb71ad443 | ||
|
|
8a277da278 | ||
|
|
7dd802201f | ||
|
|
79b952ea08 | ||
|
|
397165a524 | ||
|
|
dc3f13991a | ||
|
|
8ef657531e | ||
|
|
b40a4c27d3 | ||
|
|
f0be15ded8 | ||
|
|
7fc161f781 | ||
|
|
2b54d205fe | ||
|
|
1940d05e64 | ||
|
|
905288423f | ||
|
|
62376f507b | ||
|
|
51c8dad753 | ||
|
|
540906fb8a | ||
|
|
b642f5c3e5 | ||
|
|
b36b077d42 | ||
|
|
fe9c2b0e4b | ||
|
|
548cadacff | ||
|
|
a1171877a4 | ||
|
|
f06cc339cc | ||
|
|
6bf8982559 | ||
|
|
364d7ebc40 | ||
|
|
7cc81e9a43 | ||
|
|
3409c519e2 | ||
|
|
5851b42af3 | ||
|
|
c5eae67ac9 | ||
|
|
865ee473ce | ||
|
|
08e8145975 | ||
|
|
ec0f20de03 | ||
|
|
40591a7c50 | ||
|
|
32d394d65b | ||
|
|
66fab8722c | ||
|
|
5a8a68cab8 | ||
|
|
689761bfcb | ||
|
|
2394e45ec7 | ||
|
|
01e6a3a9d9 | ||
|
|
07f4950cb3 | ||
|
|
368896d84d | ||
|
|
408f650b0c | ||
|
|
7c2e1fa3e2 | ||
|
|
1da66b9a8c | ||
|
|
4953762f4e | ||
|
|
97764c4a57 | ||
|
|
2ea85d3ba2 | ||
|
|
1f11300175 | ||
|
|
f317db525f | ||
|
|
3fa0538f72 | ||
|
|
fcfc96ca05 | ||
|
|
69c2b422de | ||
|
|
496baa9335 | ||
|
|
e8657cc3de | ||
|
|
e08c06cbc3 | ||
|
|
8ca54ddf94 | ||
|
|
3e073404cc | ||
|
|
0acabf5f73 | ||
|
|
38285aa1ac | ||
|
|
5341cd015b | ||
|
|
c32eebf57d | ||
|
|
554ba6b8f3 | ||
|
|
a69b8c1e96 | ||
|
|
6f3fcf2276 | ||
|
|
3df4bba280 | ||
|
|
7c0d2e1d98 | ||
|
|
a9336b74fd | ||
|
|
518937b87f | ||
|
|
e6ab9abf19 | ||
|
|
87a25e326c | ||
|
|
baf7d2c7c0 | ||
|
|
22dd0aa20c | ||
|
|
52e7492cbc | ||
|
|
7e9d00a5a6 | ||
|
|
ff9cf6c7a4 | ||
|
|
56593f20b0 | ||
|
|
b7b9b003c9 | ||
|
|
59639ca9b2 | ||
|
|
66b8c42a25 |
@@ -63,7 +63,7 @@ pnpm analyze-component <path> --json
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Before: Complex state logic in component
|
// ❌ Before: Complex state logic in component
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
||||||
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>(...)
|
const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>(...)
|
||||||
const [completionParams, setCompletionParams] = useState<FormValue>({})
|
const [completionParams, setCompletionParams] = useState<FormValue>({})
|
||||||
@@ -85,7 +85,7 @@ export const useModelConfig = (appId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Component becomes cleaner
|
// Component becomes cleaner
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
||||||
return <div>...</div>
|
return <div>...</div>
|
||||||
}
|
}
|
||||||
@@ -189,8 +189,6 @@ const Template = useMemo(() => {
|
|||||||
|
|
||||||
**Dify Convention**:
|
**Dify Convention**:
|
||||||
- This skill is for component decomposition, not query/mutation design.
|
- This skill is for component decomposition, not query/mutation design.
|
||||||
- When refactoring data fetching, follow `web/AGENTS.md`.
|
|
||||||
- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling.
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
- Do not introduce deprecated `useInvalid` / `useReset`.
|
||||||
- Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state.
|
- Do not add thin passthrough `useQuery` wrappers during refactoring; only extract a custom hook when it truly orchestrates multiple queries/mutations or shared derived state.
|
||||||
|
|
||||||
@@ -367,7 +365,7 @@ For each extraction:
|
|||||||
┌────────────────────────────────────────┐
|
┌────────────────────────────────────────┐
|
||||||
│ 1. Extract code │
|
│ 1. Extract code │
|
||||||
│ 2. Run: pnpm lint:fix │
|
│ 2. Run: pnpm lint:fix │
|
||||||
│ 3. Run: pnpm type-check:tsgo │
|
│ 3. Run: pnpm type-check │
|
||||||
│ 4. Run: pnpm test │
|
│ 4. Run: pnpm test │
|
||||||
│ 5. Test functionality manually │
|
│ 5. Test functionality manually │
|
||||||
│ 6. PASS? → Next extraction │
|
│ 6. PASS? → Next extraction │
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ const Template = useMemo(() => {
|
|||||||
**After** (complexity: ~3):
|
**After** (complexity: ~3):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
// Define lookup table outside component
|
// Define lookup table outside component
|
||||||
const TEMPLATE_MAP: Record<AppModeEnum, Record<string, FC<TemplateProps>>> = {
|
const TEMPLATE_MAP: Record<AppModeEnum, Record<string, ComponentType<TemplateProps>>> = {
|
||||||
[AppModeEnum.CHAT]: {
|
[AppModeEnum.CHAT]: {
|
||||||
[LanguagesSupported[1]]: TemplateChatZh,
|
[LanguagesSupported[1]]: TemplateChatZh,
|
||||||
[LanguagesSupported[7]]: TemplateChatJa,
|
[LanguagesSupported[7]]: TemplateChatJa,
|
||||||
|
|||||||
@@ -65,10 +65,10 @@ interface ConfigurationHeaderProps {
|
|||||||
onPublish: () => void
|
onPublish: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigurationHeader: FC<ConfigurationHeaderProps> = ({
|
function ConfigurationHeader({
|
||||||
isAdvancedMode,
|
isAdvancedMode,
|
||||||
onPublish,
|
onPublish,
|
||||||
}) => {
|
}: ConfigurationHeaderProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -136,7 +136,7 @@ const AppInfo = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ✅ After: Separate view components
|
// ✅ After: Separate view components
|
||||||
const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
function AppInfoExpanded({ appDetail, onAction }: AppInfoViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="expanded">
|
<div className="expanded">
|
||||||
{/* Clean, focused expanded view */}
|
{/* Clean, focused expanded view */}
|
||||||
@@ -144,7 +144,7 @@ const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppInfoCollapsed: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
function AppInfoCollapsed({ appDetail, onAction }: AppInfoViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="collapsed">
|
<div className="collapsed">
|
||||||
{/* Clean, focused collapsed view */}
|
{/* Clean, focused collapsed view */}
|
||||||
@@ -203,12 +203,12 @@ interface AppInfoModalsProps {
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppInfoModals: FC<AppInfoModalsProps> = ({
|
function AppInfoModals({
|
||||||
appDetail,
|
appDetail,
|
||||||
activeModal,
|
activeModal,
|
||||||
onClose,
|
onClose,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}) => {
|
}: AppInfoModalsProps) {
|
||||||
const handleEdit = async (data) => { /* logic */ }
|
const handleEdit = async (data) => { /* logic */ }
|
||||||
const handleDuplicate = async (data) => { /* logic */ }
|
const handleDuplicate = async (data) => { /* logic */ }
|
||||||
const handleDelete = async () => { /* logic */ }
|
const handleDelete = async () => { /* logic */ }
|
||||||
@@ -296,7 +296,7 @@ interface OperationItemProps {
|
|||||||
onAction: (id: string) => void
|
onAction: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const OperationItem: FC<OperationItemProps> = ({ operation, onAction }) => {
|
function OperationItem({ operation, onAction }: OperationItemProps) {
|
||||||
return (
|
return (
|
||||||
<div className="operation-item">
|
<div className="operation-item">
|
||||||
<span className="icon">{operation.icon}</span>
|
<span className="icon">{operation.icon}</span>
|
||||||
@@ -435,7 +435,7 @@ interface ChildProps {
|
|||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Child: FC<ChildProps> = ({ value, onChange, onSubmit }) => {
|
function Child({ value, onChange, onSubmit }: ChildProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input value={value} onChange={e => onChange(e.target.value)} />
|
<input value={value} onChange={e => onChange(e.target.value)} />
|
||||||
|
|||||||
@@ -112,13 +112,13 @@ export const useModelConfig = ({
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Before: 50+ lines of state management
|
// Before: 50+ lines of state management
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
const [modelConfig, setModelConfig] = useState<ModelConfig>(...)
|
||||||
// ... lots of related state and effects
|
// ... lots of related state and effects
|
||||||
}
|
}
|
||||||
|
|
||||||
// After: Clean component
|
// After: Clean component
|
||||||
const Configuration: FC = () => {
|
function Configuration() {
|
||||||
const {
|
const {
|
||||||
modelConfig,
|
modelConfig,
|
||||||
setModelConfig,
|
setModelConfig,
|
||||||
@@ -159,8 +159,6 @@ const Configuration: FC = () => {
|
|||||||
|
|
||||||
When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns.
|
When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns.
|
||||||
|
|
||||||
- Follow `web/AGENTS.md` first.
|
|
||||||
- Use `frontend-query-mutation` for contracts, query shape, data-fetching wrappers, query/mutation call-site patterns, conditional queries, invalidation, and mutation error handling.
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
- Do not introduce deprecated `useInvalid` / `useReset`.
|
||||||
- Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks.
|
- Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: e2e-cucumber-playwright
|
||||||
|
description: Write, update, or review Dify end-to-end tests under `e2e/` that use Cucumber, Gherkin, and Playwright. Use when the task involves `.feature` files, `features/step-definitions/`, `features/support/`, `DifyWorld`, scenario tags, locator/assertion choices, or E2E testing best practices for this repository.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dify E2E Cucumber + Playwright
|
||||||
|
|
||||||
|
Use this skill for Dify's repository-level E2E suite in `e2e/`. Use [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) as the canonical guide for local architecture and conventions, then apply Playwright/Cucumber best practices only where they fit the current suite.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Use this skill for `.feature` files, Cucumber step definitions, `DifyWorld`, hooks, tags, and E2E review work under `e2e/`.
|
||||||
|
- Do not use this skill for Vitest or React Testing Library work under `web/`; use `frontend-testing` instead.
|
||||||
|
- Do not use this skill for backend test or API review tasks under `api/`.
|
||||||
|
|
||||||
|
## Read Order
|
||||||
|
|
||||||
|
1. Read [`e2e/AGENTS.md`](../../../e2e/AGENTS.md) first.
|
||||||
|
2. Read only the files directly involved in the task:
|
||||||
|
- target `.feature` files under `e2e/features/`
|
||||||
|
- related step files under `e2e/features/step-definitions/`
|
||||||
|
- `e2e/features/support/hooks.ts` and `e2e/features/support/world.ts` when session lifecycle or shared state matters
|
||||||
|
- `e2e/scripts/run-cucumber.ts` and `e2e/cucumber.config.ts` when tags or execution flow matter
|
||||||
|
3. Read [`references/playwright-best-practices.md`](references/playwright-best-practices.md) only when locator, assertion, isolation, or waiting choices are involved.
|
||||||
|
4. Read [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md) only when scenario wording, step granularity, tags, or expression design are involved.
|
||||||
|
5. Re-check official Playwright or Cucumber docs with the available documentation tools before introducing a new framework pattern.
|
||||||
|
|
||||||
|
## Local Rules
|
||||||
|
|
||||||
|
- `e2e/` uses Cucumber for scenarios and Playwright as the browser layer.
|
||||||
|
- `DifyWorld` is the per-scenario context object. Type `this` as `DifyWorld` and use `async function`, not arrow functions.
|
||||||
|
- Keep glue organized by capability under `e2e/features/step-definitions/`; use `common/` only for broadly reusable steps.
|
||||||
|
- Browser session behavior comes from `features/support/hooks.ts`:
|
||||||
|
- default: authenticated session with shared storage state
|
||||||
|
- `@unauthenticated`: clean browser context
|
||||||
|
- `@authenticated`: readability/selective-run tag only unless implementation changes
|
||||||
|
- `@fresh`: only for `e2e:full*` flows
|
||||||
|
- Do not import Playwright Test runner patterns that bypass the current Cucumber + `DifyWorld` architecture unless the task is explicitly about changing that architecture.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Rebuild local context.
|
||||||
|
- Inspect the target feature area.
|
||||||
|
- Reuse an existing step when wording and behavior already match.
|
||||||
|
- Add a new step only for a genuinely new user action or assertion.
|
||||||
|
- Keep edits close to the current capability folder unless the step is broadly reusable.
|
||||||
|
2. Write behavior-first scenarios.
|
||||||
|
- Describe user-observable behavior, not DOM mechanics.
|
||||||
|
- Keep each scenario focused on one workflow or outcome.
|
||||||
|
- Keep scenarios independent and re-runnable.
|
||||||
|
3. Write step definitions in the local style.
|
||||||
|
- Keep one step to one user-visible action or one assertion.
|
||||||
|
- Prefer Cucumber Expressions such as `{string}` and `{int}`.
|
||||||
|
- Scope locators to stable containers when the page has repeated elements.
|
||||||
|
- Avoid page-object layers or extra helper abstractions unless repeated complexity clearly justifies them.
|
||||||
|
4. Use Playwright in the local style.
|
||||||
|
- Prefer user-facing locators: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then `getByTestId` for explicit contracts.
|
||||||
|
- Use web-first `expect(...)` assertions.
|
||||||
|
- Do not use `waitForTimeout`, manual polling, or raw visibility checks when a locator action or retrying assertion already expresses the behavior.
|
||||||
|
5. Validate narrowly.
|
||||||
|
- Run the narrowest tagged scenario or flow that exercises the change.
|
||||||
|
- Run `pnpm -C e2e check`.
|
||||||
|
- Broaden verification only when the change affects hooks, tags, setup, or shared step semantics.
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
- Does the scenario describe behavior rather than implementation?
|
||||||
|
- Does it fit the current session model, tags, and `DifyWorld` usage?
|
||||||
|
- Should an existing step be reused instead of adding a new one?
|
||||||
|
- Are locators user-facing and assertions web-first?
|
||||||
|
- Does the change introduce hidden coupling across scenarios, tags, or instance state?
|
||||||
|
- Does it document or implement behavior that differs from the real hooks or configuration?
|
||||||
|
|
||||||
|
Lead findings with correctness, flake risk, and architecture drift.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [`references/playwright-best-practices.md`](references/playwright-best-practices.md)
|
||||||
|
- [`references/cucumber-best-practices.md`](references/cucumber-best-practices.md)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "E2E Cucumber + Playwright"
|
||||||
|
short_description: "Write and review Dify E2E scenarios."
|
||||||
|
default_prompt: "Use $e2e-cucumber-playwright to write or review a Dify E2E scenario under e2e/."
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Cucumber Best Practices For Dify E2E
|
||||||
|
|
||||||
|
Use this reference when writing or reviewing Gherkin scenarios, step definitions, parameter expressions, and step reuse in Dify's `e2e/` suite.
|
||||||
|
|
||||||
|
Official sources:
|
||||||
|
|
||||||
|
- https://cucumber.io/docs/guides/10-minute-tutorial/
|
||||||
|
- https://cucumber.io/docs/cucumber/step-definitions/
|
||||||
|
- https://cucumber.io/docs/cucumber/cucumber-expressions/
|
||||||
|
|
||||||
|
## What Matters Most
|
||||||
|
|
||||||
|
### 1. Treat scenarios as executable specifications
|
||||||
|
|
||||||
|
Cucumber scenarios should describe examples of behavior, not test implementation recipes.
|
||||||
|
|
||||||
|
Apply it like this:
|
||||||
|
|
||||||
|
- write what the user does and what should happen
|
||||||
|
- avoid UI-internal wording such as selector details, DOM structure, or component names
|
||||||
|
- keep language concrete enough that the scenario reads like living documentation
|
||||||
|
|
||||||
|
### 2. Keep scenarios focused
|
||||||
|
|
||||||
|
A scenario should usually prove one workflow or business outcome. If a scenario wanders across several unrelated behaviors, split it.
|
||||||
|
|
||||||
|
In Dify's suite, this means:
|
||||||
|
|
||||||
|
- one capability-focused scenario per feature path
|
||||||
|
- no long setup chains when existing bootstrap or reusable steps already cover them
|
||||||
|
- no hidden dependency on another scenario's side effects
|
||||||
|
|
||||||
|
### 3. Reuse steps, but only when behavior really matches
|
||||||
|
|
||||||
|
Good reuse reduces duplication. Bad reuse hides meaning.
|
||||||
|
|
||||||
|
Prefer reuse when:
|
||||||
|
|
||||||
|
- the user action is genuinely the same
|
||||||
|
- the expected outcome is genuinely the same
|
||||||
|
- the wording stays natural across features
|
||||||
|
|
||||||
|
Write a new step when:
|
||||||
|
|
||||||
|
- the behavior is materially different
|
||||||
|
- reusing the old wording would make the scenario misleading
|
||||||
|
- a supposedly generic step would become an implementation-detail wrapper
|
||||||
|
|
||||||
|
### 4. Prefer Cucumber Expressions
|
||||||
|
|
||||||
|
Use Cucumber Expressions for parameters unless regex is clearly necessary.
|
||||||
|
|
||||||
|
Common examples:
|
||||||
|
|
||||||
|
- `{string}` for labels, names, and visible text
|
||||||
|
- `{int}` for counts
|
||||||
|
- `{float}` for decimal values
|
||||||
|
- `{word}` only when the value is truly a single token
|
||||||
|
|
||||||
|
Keep expressions readable. If a step needs complicated parsing logic, first ask whether the scenario wording should be simpler.
|
||||||
|
|
||||||
|
### 5. Keep step definitions thin and meaningful
|
||||||
|
|
||||||
|
Step definitions are glue between Gherkin and automation, not a second abstraction language.
|
||||||
|
|
||||||
|
For Dify:
|
||||||
|
|
||||||
|
- type `this` as `DifyWorld`
|
||||||
|
- use `async function`
|
||||||
|
- keep each step to one user-visible action or assertion
|
||||||
|
- rely on `DifyWorld` and existing support code for shared context
|
||||||
|
- avoid leaking cross-scenario state
|
||||||
|
|
||||||
|
### 6. Use tags intentionally
|
||||||
|
|
||||||
|
Tags should communicate run scope or session semantics, not become ad hoc metadata.
|
||||||
|
|
||||||
|
In Dify's current suite:
|
||||||
|
|
||||||
|
- capability tags group related scenarios
|
||||||
|
- `@unauthenticated` changes session behavior
|
||||||
|
- `@authenticated` is descriptive/selective, not a behavior switch by itself
|
||||||
|
- `@fresh` belongs to reset/full-install flows only
|
||||||
|
|
||||||
|
If a proposed tag implies behavior, verify that hooks or runner configuration actually implement it.
|
||||||
|
|
||||||
|
## Review Questions
|
||||||
|
|
||||||
|
- Does the scenario read like a real example of product behavior?
|
||||||
|
- Are the steps behavior-oriented instead of implementation-oriented?
|
||||||
|
- Is a reused step still truthful in this feature?
|
||||||
|
- Is a new tag documenting real behavior, or inventing semantics that the suite does not implement?
|
||||||
|
- Would a new reader understand the outcome without opening the step-definition file?
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Playwright Best Practices For Dify E2E
|
||||||
|
|
||||||
|
Use this reference when writing or reviewing locator, assertion, isolation, or synchronization logic for Dify's Cucumber-based E2E suite.
|
||||||
|
|
||||||
|
Official sources:
|
||||||
|
|
||||||
|
- https://playwright.dev/docs/best-practices
|
||||||
|
- https://playwright.dev/docs/locators
|
||||||
|
- https://playwright.dev/docs/test-assertions
|
||||||
|
- https://playwright.dev/docs/browser-contexts
|
||||||
|
|
||||||
|
## What Matters Most
|
||||||
|
|
||||||
|
### 1. Keep scenarios isolated
|
||||||
|
|
||||||
|
Playwright's model is built around clean browser contexts so one test does not leak into another. In Dify's suite, that principle maps to per-scenario session setup in `features/support/hooks.ts` and `DifyWorld`.
|
||||||
|
|
||||||
|
Apply it like this:
|
||||||
|
|
||||||
|
- do not depend on another scenario having run first
|
||||||
|
- do not persist ad hoc scenario state outside `DifyWorld`
|
||||||
|
- do not couple ordinary scenarios to `@fresh` behavior
|
||||||
|
- when a flow needs special auth/session semantics, express that through the existing tag model or explicit hook changes
|
||||||
|
|
||||||
|
### 2. Prefer user-facing locators
|
||||||
|
|
||||||
|
Playwright recommends built-in locators that reflect what users perceive on the page.
|
||||||
|
|
||||||
|
Preferred order in this repository:
|
||||||
|
|
||||||
|
1. `getByRole`
|
||||||
|
2. `getByLabel`
|
||||||
|
3. `getByPlaceholder`
|
||||||
|
4. `getByText`
|
||||||
|
5. `getByTestId` when an explicit test contract is the most stable option
|
||||||
|
|
||||||
|
Avoid raw CSS/XPath selectors unless no stable user-facing contract exists and adding one is not practical.
|
||||||
|
|
||||||
|
Also remember:
|
||||||
|
|
||||||
|
- repeated content usually needs scoping to a stable container
|
||||||
|
- exact text matching is often too brittle when role/name or label already exists
|
||||||
|
- `getByTestId` is acceptable when semantics are weak but the contract is intentional
|
||||||
|
|
||||||
|
### 3. Use web-first assertions
|
||||||
|
|
||||||
|
Playwright assertions auto-wait and retry. Prefer them over manual state inspection.
|
||||||
|
|
||||||
|
Prefer:
|
||||||
|
|
||||||
|
- `await expect(page).toHaveURL(...)`
|
||||||
|
- `await expect(locator).toBeVisible()`
|
||||||
|
- `await expect(locator).toBeHidden()`
|
||||||
|
- `await expect(locator).toBeEnabled()`
|
||||||
|
- `await expect(locator).toHaveText(...)`
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
|
||||||
|
- `expect(await locator.isVisible()).toBe(true)`
|
||||||
|
- custom polling loops for DOM state
|
||||||
|
- `waitForTimeout` as synchronization
|
||||||
|
|
||||||
|
If a condition genuinely needs custom retry logic, use Playwright's polling/assertion tools deliberately and keep that choice local and explicit.
|
||||||
|
|
||||||
|
### 4. Let actions wait for actionability
|
||||||
|
|
||||||
|
Locator actions already wait for the element to be actionable. Do not preface every click/fill with extra timing logic unless the action needs a specific visible/ready assertion for clarity.
|
||||||
|
|
||||||
|
Good pattern:
|
||||||
|
|
||||||
|
- assert a meaningful visible state when that is part of the behavior
|
||||||
|
- then click/fill/select via locator APIs
|
||||||
|
|
||||||
|
Bad pattern:
|
||||||
|
|
||||||
|
- stack arbitrary waits before every action
|
||||||
|
- wait on unstable implementation details instead of the visible state the user cares about
|
||||||
|
|
||||||
|
### 5. Match debugging to the current suite
|
||||||
|
|
||||||
|
Playwright's wider ecosystem supports traces and rich debugging tools. Dify's current suite already captures:
|
||||||
|
|
||||||
|
- full-page screenshots
|
||||||
|
- page HTML
|
||||||
|
- console errors
|
||||||
|
- page errors
|
||||||
|
|
||||||
|
Use the existing artifact flow by default. If a task is specifically about improving diagnostics, confirm the change fits the current Cucumber architecture before importing broader Playwright tooling.
|
||||||
|
|
||||||
|
## Review Questions
|
||||||
|
|
||||||
|
- Would this locator survive DOM refactors that do not change user-visible behavior?
|
||||||
|
- Is this assertion using Playwright's retrying semantics?
|
||||||
|
- Is any explicit wait masking a real readiness problem?
|
||||||
|
- Does this code preserve per-scenario isolation?
|
||||||
|
- Is a new abstraction really needed, or does it bypass the existing `DifyWorld` + step-definition model?
|
||||||
@@ -9,18 +9,18 @@ Category: Performance
|
|||||||
|
|
||||||
When rendering React Flow, prefer `useNodes`/`useEdges` for UI consumption and rely on `useStoreApi` inside callbacks that mutate or read node/edge state. Avoid manually pulling Flow data outside of these hooks.
|
When rendering React Flow, prefer `useNodes`/`useEdges` for UI consumption and rely on `useStoreApi` inside callbacks that mutate or read node/edge state. Avoid manually pulling Flow data outside of these hooks.
|
||||||
|
|
||||||
## Complex prop memoization
|
## Complex prop stability
|
||||||
|
|
||||||
IsUrgent: True
|
IsUrgent: False
|
||||||
Category: Performance
|
Category: Performance
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
Wrap complex prop values (objects, arrays, maps) in `useMemo` prior to passing them into child components to guarantee stable references and prevent unnecessary renders.
|
Only require stable object, array, or map props when there is a clear reason: the child is memoized, the value participates in effect/query dependencies, the value is part of a stable-reference API contract, or profiling/local behavior shows avoidable re-renders. Do not request `useMemo` for every inline object by default; `how-to-write-component` treats memoization as a targeted optimization.
|
||||||
|
|
||||||
Update this file when adding, editing, or removing Performance rules so the catalog remains accurate.
|
Update this file when adding, editing, or removing Performance rules so the catalog remains accurate.
|
||||||
|
|
||||||
Wrong:
|
Risky:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<HeavyComp
|
<HeavyComp
|
||||||
@@ -31,7 +31,7 @@ Wrong:
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
Right:
|
Better when stable identity matters:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const config = useMemo(() => ({
|
const config = useMemo(() => ({
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-query-mutation
|
|
||||||
description: Guide for implementing Dify frontend query and mutation patterns with TanStack Query and oRPC. Trigger when creating or updating contracts in web/contract, wiring router composition, consuming consoleQuery or marketplaceQuery in components or services, deciding whether to call queryOptions() directly or extract a helper or use-* hook, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy service calls to contract-first query and mutation helpers.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frontend Query & Mutation
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
- Keep contract as the single source of truth in `web/contract/*`.
|
|
||||||
- Prefer contract-shaped `queryOptions()` and `mutationOptions()`.
|
|
||||||
- Keep invalidation and mutation flow knowledge in the service layer.
|
|
||||||
- Keep abstractions minimal to preserve TypeScript inference.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. Identify the change surface.
|
|
||||||
- Read `references/contract-patterns.md` for contract files, router composition, client helpers, and query or mutation call-site shape.
|
|
||||||
- Read `references/runtime-rules.md` for conditional queries, invalidation, error handling, and legacy migrations.
|
|
||||||
- Read both references when a task spans contract shape and runtime behavior.
|
|
||||||
2. Implement the smallest abstraction that fits the task.
|
|
||||||
- Default to direct `useQuery(...)` or `useMutation(...)` calls with oRPC helpers at the call site.
|
|
||||||
- Extract a small shared query helper only when multiple call sites share the same extra options.
|
|
||||||
- Create `web/service/use-{domain}.ts` only for orchestration or shared domain behavior.
|
|
||||||
3. Preserve Dify conventions.
|
|
||||||
- Keep contract inputs in `{ params, query?, body? }` shape.
|
|
||||||
- Bind invalidation in the service-layer mutation definition.
|
|
||||||
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required.
|
|
||||||
|
|
||||||
## Files Commonly Touched
|
|
||||||
|
|
||||||
- `web/contract/console/*.ts`
|
|
||||||
- `web/contract/marketplace.ts`
|
|
||||||
- `web/contract/router.ts`
|
|
||||||
- `web/service/client.ts`
|
|
||||||
- `web/service/use-*.ts`
|
|
||||||
- component and hook call sites using `consoleQuery` or `marketplaceQuery`
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Use `references/contract-patterns.md` for contract shape, router registration, query and mutation helpers, and anti-patterns that degrade inference.
|
|
||||||
- Use `references/runtime-rules.md` for conditional queries, invalidation, `mutate` versus `mutateAsync`, and legacy migration rules.
|
|
||||||
|
|
||||||
Treat this skill as the single query and mutation entry point for Dify frontend work. Keep detailed rules in the reference files instead of duplicating them in project docs.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
interface:
|
|
||||||
display_name: "Frontend Query & Mutation"
|
|
||||||
short_description: "Dify TanStack Query and oRPC patterns"
|
|
||||||
default_prompt: "Use this skill when implementing or reviewing Dify frontend contracts, query and mutation call sites, conditional queries, invalidation, or legacy query/mutation migrations."
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# Contract Patterns
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- Intent
|
|
||||||
- Minimal structure
|
|
||||||
- Core workflow
|
|
||||||
- Query usage decision rule
|
|
||||||
- Mutation usage decision rule
|
|
||||||
- Anti-patterns
|
|
||||||
- Contract rules
|
|
||||||
- Type export
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
- Keep contract as the single source of truth in `web/contract/*`.
|
|
||||||
- Default query usage to call-site `useQuery(consoleQuery|marketplaceQuery.xxx.queryOptions(...))` when endpoint behavior maps 1:1 to the contract.
|
|
||||||
- Keep abstractions minimal and preserve TypeScript inference.
|
|
||||||
|
|
||||||
## Minimal Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
web/contract/
|
|
||||||
├── base.ts
|
|
||||||
├── router.ts
|
|
||||||
├── marketplace.ts
|
|
||||||
└── console/
|
|
||||||
├── billing.ts
|
|
||||||
└── ...other domains
|
|
||||||
web/service/client.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Workflow
|
|
||||||
|
|
||||||
1. Define contract in `web/contract/console/{domain}.ts` or `web/contract/marketplace.ts`.
|
|
||||||
- Use `base.route({...}).output(type<...>())` as the baseline.
|
|
||||||
- Add `.input(type<...>())` only when the request has `params`, `query`, or `body`.
|
|
||||||
- For `GET` without input, omit `.input(...)`; do not use `.input(type<unknown>())`.
|
|
||||||
2. Register contract in `web/contract/router.ts`.
|
|
||||||
- Import directly from domain files and nest by API prefix.
|
|
||||||
3. Consume from UI call sites via oRPC query utilities.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { consoleQuery } from '@/service/client'
|
|
||||||
|
|
||||||
const invoiceQuery = useQuery(consoleQuery.billing.invoices.queryOptions({
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
throwOnError: true,
|
|
||||||
select: invoice => invoice.url,
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Usage Decision Rule
|
|
||||||
|
|
||||||
1. Default to direct `*.queryOptions(...)` usage at the call site.
|
|
||||||
2. If 3 or more call sites share the same extra options, extract a small query helper, not a `use-*` passthrough hook.
|
|
||||||
3. Create `web/service/use-{domain}.ts` only for orchestration.
|
|
||||||
- Combine multiple queries or mutations.
|
|
||||||
- Share domain-level derived state or invalidation helpers.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const invoicesBaseQueryOptions = () =>
|
|
||||||
consoleQuery.billing.invoices.queryOptions({ retry: false })
|
|
||||||
|
|
||||||
const invoiceQuery = useQuery({
|
|
||||||
...invoicesBaseQueryOptions(),
|
|
||||||
throwOnError: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mutation Usage Decision Rule
|
|
||||||
|
|
||||||
1. Default to mutation helpers from `consoleQuery` or `marketplaceQuery`, for example `useMutation(consoleQuery.billing.bindPartnerStack.mutationOptions(...))`.
|
|
||||||
2. If the mutation flow is heavily custom, use oRPC clients as `mutationFn`, for example `consoleClient.xxx` or `marketplaceClient.xxx`, instead of handwritten non-oRPC mutation logic.
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
- Do not wrap `useQuery` with `options?: Partial<UseQueryOptions>`.
|
|
||||||
- Do not split local `queryKey` and `queryFn` when oRPC `queryOptions` already exists and fits the use case.
|
|
||||||
- Do not create thin `use-*` passthrough hooks for a single endpoint.
|
|
||||||
- These patterns can degrade inference, especially around `throwOnError` and `select`, and add unnecessary indirection.
|
|
||||||
|
|
||||||
## Contract Rules
|
|
||||||
|
|
||||||
- Input structure: always use `{ params, query?, body? }`.
|
|
||||||
- No-input `GET`: omit `.input(...)`; do not use `.input(type<unknown>())`.
|
|
||||||
- Path params: use `{paramName}` in the path and match it in the `params` object.
|
|
||||||
- Router nesting: group by API prefix, for example `/billing/*` becomes `billing: {}`.
|
|
||||||
- No barrel files: import directly from specific files.
|
|
||||||
- Types: import from `@/types/` and use the `type<T>()` helper.
|
|
||||||
- Mutations: prefer `mutationOptions`; use explicit `mutationKey` mainly for defaults, filtering, and devtools.
|
|
||||||
|
|
||||||
## Type Export
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
|
||||||
```
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# Runtime Rules
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- Conditional queries
|
|
||||||
- Cache invalidation
|
|
||||||
- Key API guide
|
|
||||||
- `mutate` vs `mutateAsync`
|
|
||||||
- Legacy migration
|
|
||||||
|
|
||||||
## Conditional Queries
|
|
||||||
|
|
||||||
Prefer contract-shaped `queryOptions(...)`.
|
|
||||||
When required input is missing, prefer `input: skipToken` instead of placeholder params or non-null assertions.
|
|
||||||
Use `enabled` only for extra business gating after the input itself is already valid.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { skipToken, useQuery } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
// Disable the query by skipping input construction.
|
|
||||||
function useAccessMode(appId: string | undefined) {
|
|
||||||
return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({
|
|
||||||
input: appId
|
|
||||||
? { params: { appId } }
|
|
||||||
: skipToken,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid runtime-only guards that bypass type checking.
|
|
||||||
function useBadAccessMode(appId: string | undefined) {
|
|
||||||
return useQuery(consoleQuery.accessControl.appAccessMode.queryOptions({
|
|
||||||
input: { params: { appId: appId! } },
|
|
||||||
enabled: !!appId,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cache Invalidation
|
|
||||||
|
|
||||||
Bind invalidation in the service-layer mutation definition.
|
|
||||||
Components may add UI feedback in call-site callbacks, but they should not decide which queries to invalidate.
|
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
- `.key()` for namespace or prefix invalidation
|
|
||||||
- `.queryKey(...)` only for exact cache reads or writes such as `getQueryData` and `setQueryData`
|
|
||||||
- `queryClient.invalidateQueries(...)` in mutation `onSuccess`
|
|
||||||
|
|
||||||
Do not use deprecated `useInvalid` from `use-base.ts`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Service layer owns cache invalidation.
|
|
||||||
export const useUpdateAccessMode = () => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation(consoleQuery.accessControl.updateAccessMode.mutationOptions({
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Component only adds UI behavior.
|
|
||||||
updateAccessMode({ appId, mode }, {
|
|
||||||
onSuccess: () => toast.success('...'),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Avoid putting invalidation knowledge in the component.
|
|
||||||
mutate({ appId, mode }, {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: consoleQuery.accessControl.appWhitelistSubjects.key(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key API Guide
|
|
||||||
|
|
||||||
- `.key(...)`
|
|
||||||
- Use for partial matching operations.
|
|
||||||
- Prefer it for invalidation, refetch, and cancel patterns.
|
|
||||||
- Example: `queryClient.invalidateQueries({ queryKey: consoleQuery.billing.key() })`
|
|
||||||
- `.queryKey(...)`
|
|
||||||
- Use for a specific query's full key.
|
|
||||||
- Prefer it for exact cache addressing and direct reads or writes.
|
|
||||||
- `.mutationKey(...)`
|
|
||||||
- Use for a specific mutation's full key.
|
|
||||||
- Prefer it for mutation defaults registration, mutation-status filtering, and devtools grouping.
|
|
||||||
|
|
||||||
## `mutate` vs `mutateAsync`
|
|
||||||
|
|
||||||
Prefer `mutate` by default.
|
|
||||||
Use `mutateAsync` only when Promise semantics are truly required, such as parallel mutations or sequential steps with result dependencies.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
|
|
||||||
- Event handlers should usually call `mutate(...)` with `onSuccess` or `onError`.
|
|
||||||
- Every `await mutateAsync(...)` must be wrapped in `try/catch`.
|
|
||||||
- Do not use `mutateAsync` when callbacks already express the flow clearly.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Default case.
|
|
||||||
mutation.mutate(data, {
|
|
||||||
onSuccess: result => router.push(result.url),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Promise semantics are required.
|
|
||||||
try {
|
|
||||||
const order = await createOrder.mutateAsync(orderData)
|
|
||||||
await confirmPayment.mutateAsync({ orderId: order.id, token })
|
|
||||||
router.push(`/orders/${order.id}`)
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : 'Unknown error')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Legacy Migration
|
|
||||||
|
|
||||||
When touching old code, migrate it toward these rules:
|
|
||||||
|
|
||||||
| Old pattern | New pattern |
|
|
||||||
|---|---|
|
|
||||||
| `useInvalid(key)` in service layer | `queryClient.invalidateQueries(...)` inside mutation `onSuccess` |
|
|
||||||
| component-triggered invalidation after mutation | move invalidation into the service-layer mutation definition |
|
|
||||||
| imperative fetch plus manual invalidation | wrap it in `useMutation(...mutationOptions(...))` |
|
|
||||||
| `await mutateAsync()` without `try/catch` | switch to `mutate(...)` or add `try/catch` |
|
|
||||||
@@ -5,7 +5,7 @@ description: Generate Vitest + React Testing Library tests for Dify frontend com
|
|||||||
|
|
||||||
# Dify Frontend Testing Skill
|
# Dify Frontend Testing Skill
|
||||||
|
|
||||||
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
This skill enables Codex to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
||||||
|
|
||||||
> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
|
> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
|
||||||
|
|
||||||
@@ -24,35 +24,27 @@ Apply this skill when the user:
|
|||||||
**Do NOT apply** when:
|
**Do NOT apply** when:
|
||||||
|
|
||||||
- User is asking about backend/API tests (Python/pytest)
|
- User is asking about backend/API tests (Python/pytest)
|
||||||
- User is asking about E2E tests (Playwright/Cypress)
|
- User is asking about E2E tests (Cucumber + Playwright under `e2e/`)
|
||||||
- User is only asking conceptual questions without code context
|
- User is only asking conceptual questions without code context
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### Tech Stack
|
|
||||||
|
|
||||||
| Tool | Version | Purpose |
|
|
||||||
|------|---------|---------|
|
|
||||||
| Vitest | 4.0.16 | Test runner |
|
|
||||||
| React Testing Library | 16.0 | Component testing |
|
|
||||||
| jsdom | - | Test environment |
|
|
||||||
| nock | 14.0 | HTTP mocking |
|
|
||||||
| TypeScript | 5.x | Type safety |
|
|
||||||
|
|
||||||
### Key Commands
|
### Key Commands
|
||||||
|
|
||||||
|
Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
# Watch mode
|
# Watch mode
|
||||||
pnpm test:watch
|
pnpm test --watch
|
||||||
|
|
||||||
# Run specific file
|
# Run specific file
|
||||||
pnpm test path/to/file.spec.tsx
|
pnpm test path/to/file.spec.tsx
|
||||||
|
|
||||||
# Generate coverage report
|
# Generate coverage report
|
||||||
pnpm test:coverage
|
pnpm test --coverage
|
||||||
|
|
||||||
# Analyze component complexity
|
# Analyze component complexity
|
||||||
pnpm analyze-component <path>
|
pnpm analyze-component <path>
|
||||||
@@ -200,7 +192,7 @@ When assigned to test a directory/path, test **ALL content** within that path:
|
|||||||
|
|
||||||
- ✅ **Import real project components** directly (including base components and siblings)
|
- ✅ **Import real project components** directly (including base components and siblings)
|
||||||
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
- ✅ **Only mock**: API services (`@/service/*`), `next/navigation`, complex context providers
|
||||||
- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
|
- ❌ **DO NOT mock** base components (`@/app/components/base/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)
|
||||||
- ❌ **DO NOT mock** sibling/child components in the same directory
|
- ❌ **DO NOT mock** sibling/child components in the same directory
|
||||||
|
|
||||||
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
|
> See [Test Structure Template](#test-structure-template) for correct import/mock patterns.
|
||||||
@@ -228,7 +220,10 @@ Every test should clearly separate:
|
|||||||
### 2. Black-Box Testing
|
### 2. Black-Box Testing
|
||||||
|
|
||||||
- Test observable behavior, not implementation details
|
- Test observable behavior, not implementation details
|
||||||
- Use semantic queries (getByRole, getByLabelText)
|
- Use semantic queries (`getByRole` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`)
|
||||||
|
- Treat `getByTestId` as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on `data-testid`.
|
||||||
|
- Remove production `data-testid` attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment.
|
||||||
|
- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons `aria-hidden`.
|
||||||
- Avoid testing internal state directly
|
- Avoid testing internal state directly
|
||||||
- **Prefer pattern matching over hardcoded strings** in assertions:
|
- **Prefer pattern matching over hardcoded strings** in assertions:
|
||||||
|
|
||||||
@@ -325,12 +320,12 @@ For more detailed information, refer to:
|
|||||||
### Reference Examples in Codebase
|
### Reference Examples in Codebase
|
||||||
|
|
||||||
- `web/utils/classnames.spec.ts` - Utility function tests
|
- `web/utils/classnames.spec.ts` - Utility function tests
|
||||||
- `web/app/components/base/button/index.spec.tsx` - Component tests
|
- `web/app/components/base/radio/__tests__/index.spec.tsx` - Component tests
|
||||||
- `web/__mocks__/provider-context.ts` - Mock factory example
|
- `web/__mocks__/provider-context.ts` - Mock factory example
|
||||||
|
|
||||||
### Project Configuration
|
### Project Configuration
|
||||||
|
|
||||||
- `web/vitest.config.ts` - Vitest configuration
|
- `web/vite.config.ts` - Vite/Vitest configuration
|
||||||
- `web/vitest.setup.ts` - Test environment setup
|
- `web/vitest.setup.ts` - Test environment setup
|
||||||
- `web/scripts/analyze-component.js` - Component analysis tool
|
- `web/scripts/analyze-component.js` - Component analysis tool
|
||||||
- Modules are not mocked automatically. Global mocks live in `web/vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
|
- Modules are not mocked automatically. Global mocks live in `web/vitest.setup.ts` (for example `react-i18next`, `next/image`); mock other modules like `ky` or `mime` locally in test files.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
|
|||||||
|
|
||||||
### Integration vs Mocking
|
### Integration vs Mocking
|
||||||
|
|
||||||
- [ ] **DO NOT mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
- [ ] **DO NOT mock base components or dify-ui primitives** (base `Loading`, `Input`, `Badge`; dify-ui `Button`, `Tooltip`, `Dialog`, etc.)
|
||||||
- [ ] Import real project components instead of mocking
|
- [ ] Import real project components instead of mocking
|
||||||
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
|
- [ ] Only mock: API calls, complex context providers, third-party libs with side effects
|
||||||
- [ ] Prefer integration testing when using single spec file
|
- [ ] Prefer integration testing when using single spec file
|
||||||
@@ -73,7 +73,7 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
|
|||||||
|
|
||||||
### Mocks
|
### Mocks
|
||||||
|
|
||||||
- [ ] **DO NOT mock base components** (`@/app/components/base/*`)
|
- [ ] **DO NOT mock base components or dify-ui primitives** (`@/app/components/base/*` or `@langgenius/dify-ui/*`)
|
||||||
- [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
|
- [ ] `vi.clearAllMocks()` in `beforeEach` (not `afterEach`)
|
||||||
- [ ] Shared mock state reset in `beforeEach`
|
- [ ] Shared mock state reset in `beforeEach`
|
||||||
- [ ] i18n uses global mock (auto-loaded in `web/vitest.setup.ts`); only override locally for custom translations
|
- [ ] i18n uses global mock (auto-loaded in `web/vitest.setup.ts`); only override locally for custom translations
|
||||||
@@ -127,7 +127,7 @@ For the current file being tested:
|
|||||||
- [ ] Run full directory test: `pnpm test path/to/directory/`
|
- [ ] Run full directory test: `pnpm test path/to/directory/`
|
||||||
- [ ] Check coverage report: `pnpm test:coverage`
|
- [ ] Check coverage report: `pnpm test:coverage`
|
||||||
- [ ] Run `pnpm lint:fix` on all test files
|
- [ ] Run `pnpm lint:fix` on all test files
|
||||||
- [ ] Run `pnpm type-check:tsgo`
|
- [ ] Run `pnpm type-check`
|
||||||
|
|
||||||
## Common Issues to Watch
|
## Common Issues to Watch
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,27 @@
|
|||||||
|
|
||||||
## ⚠️ Important: What NOT to Mock
|
## ⚠️ Important: What NOT to Mock
|
||||||
|
|
||||||
### DO NOT Mock Base Components
|
### DO NOT Mock Base Components or dify-ui Primitives
|
||||||
|
|
||||||
**Never mock components from `@/app/components/base/`** such as:
|
**Never mock components from `@/app/components/base/` or from `@langgenius/dify-ui/*`** such as:
|
||||||
|
|
||||||
- `Loading`, `Spinner`
|
- Legacy base (`@/app/components/base/*`): `Loading`, `Spinner`, `Input`, `Badge`, `Tag`
|
||||||
- `Button`, `Input`, `Select`
|
- dify-ui primitives (`@langgenius/dify-ui/*`): `Button`, `Tooltip`, `Dialog`, `Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `AlertDialog`, `Toast`
|
||||||
- `Tooltip`, `Modal`, `Dropdown`
|
|
||||||
- `Icon`, `Badge`, `Tag`
|
|
||||||
|
|
||||||
**Why?**
|
**Why?**
|
||||||
|
|
||||||
- Base components will have their own dedicated tests
|
- These components have their own dedicated tests
|
||||||
- Mocking them creates false positives (tests pass but real integration fails)
|
- Mocking them creates false positives (tests pass but real integration fails)
|
||||||
- Using real components tests actual integration behavior
|
- Using real components tests actual integration behavior
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ WRONG: Don't mock base components
|
// ❌ WRONG: Don't mock base components or dify-ui primitives
|
||||||
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
||||||
vi.mock('@/app/components/base/button', () => ({ children }: any) => <button>{children}</button>)
|
vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => <button>{children}</button> }))
|
||||||
|
|
||||||
// ✅ CORRECT: Import and use real base components
|
// ✅ CORRECT: Import and use the real components
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Button from '@/app/components/base/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
// They will render normally in tests
|
// They will render normally in tests
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ See [Zustand Store Testing](#zustand-store-testing) section for full details.
|
|||||||
|
|
||||||
| Location | Purpose |
|
| Location | Purpose |
|
||||||
|----------|---------|
|
|----------|---------|
|
||||||
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `next/image`, `zustand`) |
|
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `zustand`, clipboard, FloatingPortal, Monaco, localStorage`) |
|
||||||
| `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) |
|
| `web/__mocks__/zustand.ts` | Zustand mock implementation (auto-resets stores after each test) |
|
||||||
| `web/__mocks__/` | Reusable mock factories shared across multiple test files |
|
| `web/__mocks__/` | Reusable mock factories shared across multiple test files |
|
||||||
| Test file | Test-specific mocks, inline with `vi.mock()` |
|
| Test file | Test-specific mocks, inline with `vi.mock()` |
|
||||||
@@ -218,28 +216,21 @@ describe('Component', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. HTTP Mocking with Nock
|
### 5. HTTP and `fetch` Mocking
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import nock from 'nock'
|
|
||||||
|
|
||||||
const GITHUB_HOST = 'https://api.github.com'
|
|
||||||
const GITHUB_PATH = '/repos/owner/repo'
|
|
||||||
|
|
||||||
const mockGithubApi = (status: number, body: Record<string, unknown>, delayMs = 0) => {
|
|
||||||
return nock(GITHUB_HOST)
|
|
||||||
.get(GITHUB_PATH)
|
|
||||||
.delay(delayMs)
|
|
||||||
.reply(status, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('GithubComponent', () => {
|
describe('GithubComponent', () => {
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
nock.cleanAll()
|
vi.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display repo info', async () => {
|
it('should display repo info', async () => {
|
||||||
mockGithubApi(200, { name: 'dify', stars: 1000 })
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ name: 'dify', stars: 1000 }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@@ -249,7 +240,12 @@ describe('GithubComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle API error', async () => {
|
it('should handle API error', async () => {
|
||||||
mockGithubApi(500, { message: 'Server error' })
|
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
||||||
|
new Response(JSON.stringify({ message: 'Server error' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@@ -260,6 +256,8 @@ describe('GithubComponent', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Prefer mocking `@/service/*` modules or spying on `global.fetch` / `ky` clients with deterministic responses. Do not introduce an HTTP interception dependency such as `nock` or MSW unless it is already declared in the workspace or adding it is part of the task.
|
||||||
|
|
||||||
### 6. Context Providers
|
### 6. Context Providers
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -319,7 +317,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ✅ DO
|
### ✅ DO
|
||||||
|
|
||||||
1. **Use real base components** - Import from `@/app/components/base/` directly
|
1. **Use real base components and dify-ui primitives** - Import from `@/app/components/base/` or `@langgenius/dify-ui/*` directly
|
||||||
1. **Use real project components** - Prefer importing over mocking
|
1. **Use real project components** - Prefer importing over mocking
|
||||||
1. **Use real Zustand stores** - Set test state via `store.setState()`
|
1. **Use real Zustand stores** - Set test state via `store.setState()`
|
||||||
1. **Reset mocks in `beforeEach`**, not `afterEach`
|
1. **Reset mocks in `beforeEach`**, not `afterEach`
|
||||||
@@ -330,11 +328,11 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ❌ DON'T
|
### ❌ DON'T
|
||||||
|
|
||||||
1. **Don't mock base components** (`Loading`, `Button`, `Tooltip`, etc.)
|
1. **Don't mock base components or dify-ui primitives** (`Loading`, `Input`, `Button`, `Tooltip`, `Dialog`, etc.)
|
||||||
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
|
1. **Don't mock Zustand store modules** - Use real stores with `setState()`
|
||||||
1. Don't mock components you can import directly
|
1. Don't mock components you can import directly
|
||||||
1. Don't create overly simplified mocks that miss conditional logic
|
1. Don't create overly simplified mocks that miss conditional logic
|
||||||
1. Don't forget to clean up nock after each test
|
1. Don't leave HTTP mocks or service mock state leaking between tests
|
||||||
1. Don't use `any` types in mocks without necessity
|
1. Don't use `any` types in mocks without necessity
|
||||||
|
|
||||||
### Mock Decision Tree
|
### Mock Decision Tree
|
||||||
@@ -342,7 +340,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
```
|
```
|
||||||
Need to use a component in test?
|
Need to use a component in test?
|
||||||
│
|
│
|
||||||
├─ Is it from @/app/components/base/*?
|
├─ Is it from @/app/components/base/* or @langgenius/dify-ui/*?
|
||||||
│ └─ YES → Import real component, DO NOT mock
|
│ └─ YES → Import real component, DO NOT mock
|
||||||
│
|
│
|
||||||
├─ Is it a project component?
|
├─ Is it a project component?
|
||||||
|
|||||||
@@ -227,12 +227,12 @@ Failing tests compound:
|
|||||||
|
|
||||||
**Fix failures immediately before proceeding.**
|
**Fix failures immediately before proceeding.**
|
||||||
|
|
||||||
## Integration with Claude's Todo Feature
|
## Integration with Codex's Todo Feature
|
||||||
|
|
||||||
When using Claude for multi-file testing:
|
When using Codex for multi-file testing:
|
||||||
|
|
||||||
1. **Ask Claude to create a todo list** before starting
|
1. **Create a todo list** before starting
|
||||||
1. **Request one file at a time** or ensure Claude processes incrementally
|
1. **Process one file at a time**
|
||||||
1. **Verify each test passes** before asking for the next
|
1. **Verify each test passes** before asking for the next
|
||||||
1. **Mark todos complete** as you progress
|
1. **Mark todos complete** as you progress
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: how-to-write-component
|
||||||
|
description: React/TypeScript component style guide. Use when writing, refactoring, or reviewing React components, especially around props typing, state boundaries, shared local state with Jotai atoms, API types, query/mutation contracts, navigation, memoization, wrappers, and empty-state handling.
|
||||||
|
---
|
||||||
|
|
||||||
|
# How To Write A Component
|
||||||
|
|
||||||
|
Use this as the decision guide for React/TypeScript component structure. Existing code is reference material, not automatic precedent; when it conflicts with these rules, adapt the approach instead of reproducing the violation.
|
||||||
|
|
||||||
|
## Core Defaults
|
||||||
|
|
||||||
|
- Search before adding UI, hooks, helpers, or styling patterns. Reuse existing base components, feature components, hooks, utilities, and design styles when they fit.
|
||||||
|
- Group code by feature workflow, route, or ownership area: components, hooks, local types, query helpers, atoms, constants, and small utilities should live near the code that changes with them.
|
||||||
|
- Promote code to shared only when multiple verticals need the same stable primitive. Otherwise keep it local and compose shared primitives inside the owning feature.
|
||||||
|
- Follow Dify's CSS-first Tailwind v4 contract from `packages/dify-ui/README.md` and `packages/dify-ui/AGENTS.md`. Prefer design-system tokens, utilities, and radius mappings over generic Tailwind guidance.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Put local state, queries, mutations, handlers, and derived UI data in the lowest component that uses them. Extract a purpose-built owner component only when the logic has no natural home.
|
||||||
|
- Repeated TanStack query calls in sibling components are acceptable when each component independently consumes the data. Do not hoist a query only because it is duplicated; TanStack Query handles deduplication and cache sharing.
|
||||||
|
- Hoist state, queries, or callbacks to a parent only when the parent consumes the data, coordinates shared loading/error/empty UI, needs one consistent snapshot, or owns a workflow spanning children.
|
||||||
|
- Avoid prop drilling. One pass-through layer is acceptable; repeated forwarding means ownership should move down or into feature-scoped Jotai UI state. Keep server/cache state in query and API data flow.
|
||||||
|
- Keep callbacks in a parent only for workflow coordination such as form submission, shared selection, batch behavior, or navigation. Otherwise let the child or row own its action.
|
||||||
|
- Prefer uncontrolled DOM state and CSS variables before adding controlled props.
|
||||||
|
|
||||||
|
## Components, Props, And Types
|
||||||
|
|
||||||
|
- Type component signatures directly; do not use `FC` or `React.FC`.
|
||||||
|
- Prefer `function` for top-level components and module helpers. Use arrow functions for local callbacks, handlers, and lambda-style APIs.
|
||||||
|
- Prefer named exports. Use default exports only where the framework requires them, such as Next.js route files.
|
||||||
|
- Type simple one-off props inline. Use a named `Props` type only when reused, exported, complex, or clearer.
|
||||||
|
- Use API-generated or API-returned types at component boundaries. Keep small UI conversion helpers beside the component that needs them.
|
||||||
|
- Name values by their domain role and backend API contract, and keep that name stable across the call chain, especially IDs like `appInstanceId`. Normalize framework or route params at the boundary.
|
||||||
|
- Keep fallback and invariant checks at the lowest component that already handles that state; callers should pass raw values through instead of duplicating checks.
|
||||||
|
|
||||||
|
## Queries And Mutations
|
||||||
|
|
||||||
|
- Keep `web/contract/*` as the single source of truth for API shape; follow existing domain/router patterns and the `{ params, query?, body? }` input shape.
|
||||||
|
- Consume queries directly with `useQuery(consoleQuery.xxx.queryOptions(...))` or `useQuery(marketplaceQuery.xxx.queryOptions(...))`.
|
||||||
|
- Avoid pass-through hooks and thin `web/service/use-*` wrappers that only rename `queryOptions()` or `mutationOptions()`. Extract a small `queryOptions` helper only when repeated call-site options justify it.
|
||||||
|
- Keep feature hooks for real orchestration, workflow state, or shared domain behavior.
|
||||||
|
- For missing required query input, use `input: skipToken`; use `enabled` only for extra business gating after the input is valid.
|
||||||
|
- Consume mutations directly with `useMutation(consoleQuery.xxx.mutationOptions(...))` or `useMutation(marketplaceQuery.xxx.mutationOptions(...))`; use oRPC clients as `mutationFn` only for custom flows.
|
||||||
|
- Put shared cache behavior in `createTanstackQueryUtils(...experimental_defaults...)`; components may add UI feedback callbacks, but should not own shared invalidation rules.
|
||||||
|
- Do not use deprecated `useInvalid` or `useReset`.
|
||||||
|
- Prefer `mutate(...)`; use `mutateAsync(...)` only when Promise semantics are required, and wrap awaited calls in `try/catch`.
|
||||||
|
|
||||||
|
## Component Boundaries
|
||||||
|
|
||||||
|
- Use the first level below a page or tab to organize independent page sections when it adds real structure. This layer is layout/semantic first, not automatically the data owner.
|
||||||
|
- Split deeper components by the data and state each layer actually needs. Each component should access only necessary data, and ownership should stay at the lowest consumer.
|
||||||
|
- Keep cohesive forms, menu bodies, and one-off helpers local unless they need their own state, reuse, or semantic boundary.
|
||||||
|
- Separate hidden secondary surfaces from the trigger's main flow. For dialogs, dropdowns, popovers, and similar branches, extract a small local component that owns the trigger, open state, and hidden content when it would obscure the parent flow.
|
||||||
|
- Preserve composability by separating behavior ownership from layout ownership. A dropdown action may own its trigger, open state, and menu content; the caller owns placement such as slots, offsets, and alignment.
|
||||||
|
- Avoid unnecessary DOM hierarchy. Do not add wrapper elements unless they provide layout, semantics, accessibility, state ownership, or integration with a library API; prefer fragments or styling an existing element when possible.
|
||||||
|
- Avoid shallow wrappers and prop renaming unless the wrapper adds validation, orchestration, error handling, state ownership, or a real semantic boundary.
|
||||||
|
|
||||||
|
## You Might Not Need An Effect
|
||||||
|
|
||||||
|
- Use Effects only to synchronize with external systems such as browser APIs, non-React widgets, subscriptions, timers, analytics that must run because the component was shown, or imperative DOM integration.
|
||||||
|
- Do not use Effects to transform props or state for rendering. Calculate derived values during render, and use `useMemo` only when the calculation is actually expensive.
|
||||||
|
- Do not use Effects to handle user actions. Put action-specific logic in the event handler where the cause is known.
|
||||||
|
- Do not use Effects to copy one state value into another state value representing the same concept. Pick one source of truth and derive the rest during render.
|
||||||
|
- Do not reset or adjust state from props with an Effect. Prefer a `key` reset, storing a stable ID and deriving the selected object, or guarded same-component render-time adjustment when truly necessary.
|
||||||
|
- Prefer framework data APIs or TanStack Query for data fetching instead of writing request Effects in components.
|
||||||
|
- If an Effect still seems necessary, first name the external system it synchronizes with. If there is no external system, remove the Effect and restructure the state or event flow.
|
||||||
|
|
||||||
|
## Navigation And Performance
|
||||||
|
|
||||||
|
- Prefer `Link` for normal navigation. Use router APIs only for command-flow side effects such as mutation success, guarded redirects, or form submission.
|
||||||
|
- Avoid `memo`, `useMemo`, and `useCallback` unless there is a clear performance reason.
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/e2e-cucumber-playwright
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-query-mutation
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/how-to-write-component
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
[run]
|
[run]
|
||||||
omit =
|
omit =
|
||||||
|
api/conftest.py
|
||||||
api/tests/*
|
api/tests/*
|
||||||
api/migrations/*
|
api/migrations/*
|
||||||
api/core/rag/datasource/vdb/*
|
api/core/rag/datasource/vdb/*
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ cd web && pnpm install
|
|||||||
pipx install uv
|
pipx install uv
|
||||||
|
|
||||||
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
|
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
|
||||||
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution\"" >> ~/.bashrc
|
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention\"" >> ~/.bashrc
|
||||||
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev:inspect\"" >> ~/.bashrc
|
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev:inspect\"" >> ~/.bashrc
|
||||||
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
|
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
|
||||||
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
|
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/.pnpm-store
|
||||||
|
**/dist
|
||||||
|
**/.next
|
||||||
|
**/.turbo
|
||||||
|
**/.cache
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
|
**/.mypy_cache
|
||||||
|
**/.ruff_cache
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
*.md
|
||||||
|
!web/README.md
|
||||||
|
!api/README.md
|
||||||
@@ -5,3 +5,7 @@
|
|||||||
# them.
|
# them.
|
||||||
|
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Codegen output must stay byte-identical across platforms so
|
||||||
|
# `pnpm tree:check` in CI does not trip on CRLF rewrites.
|
||||||
|
*.generated.ts text eol=lf
|
||||||
|
|||||||
+38
-31
@@ -4,7 +4,10 @@
|
|||||||
# Owners can be @username, @org/team-name, or email addresses.
|
# Owners can be @username, @org/team-name, or email addresses.
|
||||||
# For more information, see: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
# For more information, see: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
* @crazywoola @laipz8200 @Yeuoly
|
* @crazywoola @laipz8200
|
||||||
|
|
||||||
|
# ESLint suppression file is maintained by autofix.ci pruning.
|
||||||
|
/eslint-suppressions.json
|
||||||
|
|
||||||
# CODEOWNERS file
|
# CODEOWNERS file
|
||||||
/.github/CODEOWNERS @laipz8200 @crazywoola
|
/.github/CODEOWNERS @laipz8200 @crazywoola
|
||||||
@@ -15,6 +18,10 @@
|
|||||||
# Docs
|
# Docs
|
||||||
/docs/ @crazywoola
|
/docs/ @crazywoola
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
/cli/ @langgenius/maintainers
|
||||||
|
/.github/workflows/cli-tests.yml @langgenius/maintainers
|
||||||
|
|
||||||
# Backend (default owner, more specific rules below will override)
|
# Backend (default owner, more specific rules below will override)
|
||||||
/api/ @QuantumGhost
|
/api/ @QuantumGhost
|
||||||
|
|
||||||
@@ -36,7 +43,6 @@
|
|||||||
/api/core/workflow/graph/ @laipz8200 @QuantumGhost
|
/api/core/workflow/graph/ @laipz8200 @QuantumGhost
|
||||||
/api/core/workflow/graph_events/ @laipz8200 @QuantumGhost
|
/api/core/workflow/graph_events/ @laipz8200 @QuantumGhost
|
||||||
/api/core/workflow/node_events/ @laipz8200 @QuantumGhost
|
/api/core/workflow/node_events/ @laipz8200 @QuantumGhost
|
||||||
/api/dify_graph/model_runtime/ @laipz8200 @QuantumGhost
|
|
||||||
|
|
||||||
# Backend - Workflow - Nodes (Agent, Iteration, Loop, LLM)
|
# Backend - Workflow - Nodes (Agent, Iteration, Loop, LLM)
|
||||||
/api/core/workflow/nodes/agent/ @Nov1c444
|
/api/core/workflow/nodes/agent/ @Nov1c444
|
||||||
@@ -83,39 +89,39 @@
|
|||||||
/api/tasks/deal_dataset_vector_index_task.py @JohnJyong
|
/api/tasks/deal_dataset_vector_index_task.py @JohnJyong
|
||||||
|
|
||||||
# Backend - Plugins
|
# Backend - Plugins
|
||||||
/api/core/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/core/plugin/ @WH-2099
|
||||||
/api/services/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/services/plugin/ @WH-2099
|
||||||
/api/controllers/console/workspace/plugin.py @Mairuis @Yeuoly @Stream29
|
/api/controllers/console/workspace/plugin.py @WH-2099
|
||||||
/api/controllers/inner_api/plugin/ @Mairuis @Yeuoly @Stream29
|
/api/controllers/inner_api/plugin/ @WH-2099
|
||||||
/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @Mairuis @Yeuoly @Stream29
|
/api/tasks/process_tenant_plugin_autoupgrade_check_task.py @WH-2099
|
||||||
|
|
||||||
# Backend - Trigger/Schedule/Webhook
|
# Backend - Trigger/Schedule/Webhook
|
||||||
/api/controllers/trigger/ @Mairuis @Yeuoly
|
/api/controllers/trigger/ @CourTeous33
|
||||||
/api/controllers/console/app/workflow_trigger.py @Mairuis @Yeuoly
|
/api/controllers/console/app/workflow_trigger.py @CourTeous33
|
||||||
/api/controllers/console/workspace/trigger_providers.py @Mairuis @Yeuoly
|
/api/controllers/console/workspace/trigger_providers.py @CourTeous33
|
||||||
/api/core/trigger/ @Mairuis @Yeuoly
|
/api/core/trigger/ @CourTeous33
|
||||||
/api/core/app/layers/trigger_post_layer.py @Mairuis @Yeuoly
|
/api/core/app/layers/trigger_post_layer.py @CourTeous33
|
||||||
/api/services/trigger/ @Mairuis @Yeuoly
|
/api/services/trigger/ @CourTeous33
|
||||||
/api/models/trigger.py @Mairuis @Yeuoly
|
/api/models/trigger.py @CourTeous33
|
||||||
/api/fields/workflow_trigger_fields.py @Mairuis @Yeuoly
|
/api/fields/workflow_trigger_fields.py @CourTeous33
|
||||||
/api/repositories/workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
/api/repositories/workflow_trigger_log_repository.py @CourTeous33
|
||||||
/api/repositories/sqlalchemy_workflow_trigger_log_repository.py @Mairuis @Yeuoly
|
/api/repositories/sqlalchemy_workflow_trigger_log_repository.py @CourTeous33
|
||||||
/api/libs/schedule_utils.py @Mairuis @Yeuoly
|
/api/libs/schedule_utils.py @CourTeous33
|
||||||
/api/services/workflow/scheduler.py @Mairuis @Yeuoly
|
/api/services/workflow/scheduler.py @CourTeous33
|
||||||
/api/schedule/trigger_provider_refresh_task.py @Mairuis @Yeuoly
|
/api/schedule/trigger_provider_refresh_task.py @CourTeous33
|
||||||
/api/schedule/workflow_schedule_task.py @Mairuis @Yeuoly
|
/api/schedule/workflow_schedule_task.py @CourTeous33
|
||||||
/api/tasks/trigger_processing_tasks.py @Mairuis @Yeuoly
|
/api/tasks/trigger_processing_tasks.py @CourTeous33
|
||||||
/api/tasks/trigger_subscription_refresh_tasks.py @Mairuis @Yeuoly
|
/api/tasks/trigger_subscription_refresh_tasks.py @CourTeous33
|
||||||
/api/tasks/workflow_schedule_tasks.py @Mairuis @Yeuoly
|
/api/tasks/workflow_schedule_tasks.py @CourTeous33
|
||||||
/api/tasks/workflow_cfs_scheduler/ @Mairuis @Yeuoly
|
/api/tasks/workflow_cfs_scheduler/ @CourTeous33
|
||||||
/api/events/event_handlers/sync_plugin_trigger_when_app_created.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_plugin_trigger_when_app_created.py @CourTeous33
|
||||||
/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @Mairuis @Yeuoly
|
/api/events/event_handlers/update_app_triggers_when_app_published_workflow_updated.py @CourTeous33
|
||||||
/api/events/event_handlers/sync_workflow_schedule_when_app_published.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_workflow_schedule_when_app_published.py @CourTeous33
|
||||||
/api/events/event_handlers/sync_webhook_when_app_created.py @Mairuis @Yeuoly
|
/api/events/event_handlers/sync_webhook_when_app_created.py @CourTeous33
|
||||||
|
|
||||||
# Backend - Async Workflow
|
# Backend - Async Workflow
|
||||||
/api/services/async_workflow_service.py @Mairuis @Yeuoly
|
/api/services/async_workflow_service.py @Mairuis
|
||||||
/api/tasks/async_workflow_tasks.py @Mairuis @Yeuoly
|
/api/tasks/async_workflow_tasks.py @Mairuis
|
||||||
|
|
||||||
# Backend - Billing
|
# Backend - Billing
|
||||||
/api/services/billing_service.py @hj24 @zyssyz123
|
/api/services/billing_service.py @hj24 @zyssyz123
|
||||||
@@ -160,6 +166,7 @@
|
|||||||
|
|
||||||
# Frontend - App - API Documentation
|
# Frontend - App - API Documentation
|
||||||
/web/app/components/develop/ @JzoNgKVO @iamjoel
|
/web/app/components/develop/ @JzoNgKVO @iamjoel
|
||||||
|
/web/app/components/develop/template/*.mdx @JzoNgKVO @iamjoel @RiskeyL
|
||||||
|
|
||||||
# Frontend - App - Logs and Annotations
|
# Frontend - App - Logs and Annotations
|
||||||
/web/app/components/app/workflow-log/ @JzoNgKVO @iamjoel
|
/web/app/components/app/workflow-log/ @JzoNgKVO @iamjoel
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
name: Setup Web Environment
|
name: Setup Web Environment
|
||||||
|
description: Set up Node.js, Vite+, pnpm, and web dependencies
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
- name: Setup Vite+
|
- name: Setup Vite+
|
||||||
uses: voidzero-dev/setup-vp@20553a7a7429c429a74894104a2835d7fed28a72 # v1.3.0
|
uses: voidzero-dev/setup-vp@ca1c46663915d6c1042ae23bd39ab85718bfb0fa # v1.10.0
|
||||||
with:
|
with:
|
||||||
working-directory: web
|
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: true
|
cache: true
|
||||||
run-install: true
|
run-install: true
|
||||||
|
|||||||
+111
-100
@@ -1,106 +1,6 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "pip"
|
|
||||||
directory: "/api"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
flask:
|
|
||||||
patterns:
|
|
||||||
- "flask"
|
|
||||||
- "flask-*"
|
|
||||||
- "werkzeug"
|
|
||||||
- "gunicorn"
|
|
||||||
google:
|
|
||||||
patterns:
|
|
||||||
- "google-*"
|
|
||||||
- "googleapis-*"
|
|
||||||
opentelemetry:
|
|
||||||
patterns:
|
|
||||||
- "opentelemetry-*"
|
|
||||||
pydantic:
|
|
||||||
patterns:
|
|
||||||
- "pydantic"
|
|
||||||
- "pydantic-*"
|
|
||||||
llm:
|
|
||||||
patterns:
|
|
||||||
- "langfuse"
|
|
||||||
- "langsmith"
|
|
||||||
- "litellm"
|
|
||||||
- "mlflow*"
|
|
||||||
- "opik"
|
|
||||||
- "weave*"
|
|
||||||
- "arize*"
|
|
||||||
- "tiktoken"
|
|
||||||
- "transformers"
|
|
||||||
database:
|
|
||||||
patterns:
|
|
||||||
- "sqlalchemy"
|
|
||||||
- "psycopg2*"
|
|
||||||
- "psycogreen"
|
|
||||||
- "redis*"
|
|
||||||
- "alembic*"
|
|
||||||
storage:
|
|
||||||
patterns:
|
|
||||||
- "boto3*"
|
|
||||||
- "botocore*"
|
|
||||||
- "azure-*"
|
|
||||||
- "bce-*"
|
|
||||||
- "cos-python-*"
|
|
||||||
- "esdk-obs-*"
|
|
||||||
- "google-cloud-storage"
|
|
||||||
- "opendal"
|
|
||||||
- "oss2"
|
|
||||||
- "supabase*"
|
|
||||||
- "tos*"
|
|
||||||
vdb:
|
|
||||||
patterns:
|
|
||||||
- "alibabacloud*"
|
|
||||||
- "chromadb"
|
|
||||||
- "clickhouse-*"
|
|
||||||
- "clickzetta-*"
|
|
||||||
- "couchbase"
|
|
||||||
- "elasticsearch"
|
|
||||||
- "opensearch-py"
|
|
||||||
- "oracledb"
|
|
||||||
- "pgvect*"
|
|
||||||
- "pymilvus"
|
|
||||||
- "pymochow"
|
|
||||||
- "pyobvector"
|
|
||||||
- "qdrant-client"
|
|
||||||
- "intersystems-*"
|
|
||||||
- "tablestore"
|
|
||||||
- "tcvectordb"
|
|
||||||
- "tidb-vector"
|
|
||||||
- "upstash-*"
|
|
||||||
- "volcengine-*"
|
|
||||||
- "weaviate-*"
|
|
||||||
- "xinference-*"
|
|
||||||
- "mo-vector"
|
|
||||||
- "mysql-connector-*"
|
|
||||||
dev:
|
|
||||||
patterns:
|
|
||||||
- "coverage"
|
|
||||||
- "dotenv-linter"
|
|
||||||
- "faker"
|
|
||||||
- "lxml-stubs"
|
|
||||||
- "basedpyright"
|
|
||||||
- "ruff"
|
|
||||||
- "pytest*"
|
|
||||||
- "types-*"
|
|
||||||
- "boto3-stubs"
|
|
||||||
- "hypothesis"
|
|
||||||
- "pandas-stubs"
|
|
||||||
- "scipy-stubs"
|
|
||||||
- "import-linter"
|
|
||||||
- "celery-types"
|
|
||||||
- "mypy*"
|
|
||||||
- "pyrefly"
|
|
||||||
python-packages:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: "uv"
|
- package-ecosystem: "uv"
|
||||||
directory: "/api"
|
directory: "/api"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
@@ -210,3 +110,114 @@ updates:
|
|||||||
github-actions-dependencies:
|
github-actions-dependencies:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
|
- package-ecosystem: "uv"
|
||||||
|
directory: "/api"
|
||||||
|
target-branch: "lts/1.13.x"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
flask:
|
||||||
|
patterns:
|
||||||
|
- "flask"
|
||||||
|
- "flask-*"
|
||||||
|
- "werkzeug"
|
||||||
|
- "gunicorn"
|
||||||
|
google:
|
||||||
|
patterns:
|
||||||
|
- "google-*"
|
||||||
|
- "googleapis-*"
|
||||||
|
opentelemetry:
|
||||||
|
patterns:
|
||||||
|
- "opentelemetry-*"
|
||||||
|
pydantic:
|
||||||
|
patterns:
|
||||||
|
- "pydantic"
|
||||||
|
- "pydantic-*"
|
||||||
|
llm:
|
||||||
|
patterns:
|
||||||
|
- "langfuse"
|
||||||
|
- "langsmith"
|
||||||
|
- "litellm"
|
||||||
|
- "mlflow*"
|
||||||
|
- "opik"
|
||||||
|
- "weave*"
|
||||||
|
- "arize*"
|
||||||
|
- "tiktoken"
|
||||||
|
- "transformers"
|
||||||
|
database:
|
||||||
|
patterns:
|
||||||
|
- "sqlalchemy"
|
||||||
|
- "psycopg2*"
|
||||||
|
- "psycogreen"
|
||||||
|
- "redis*"
|
||||||
|
- "alembic*"
|
||||||
|
storage:
|
||||||
|
patterns:
|
||||||
|
- "boto3*"
|
||||||
|
- "botocore*"
|
||||||
|
- "azure-*"
|
||||||
|
- "bce-*"
|
||||||
|
- "cos-python-*"
|
||||||
|
- "esdk-obs-*"
|
||||||
|
- "google-cloud-storage"
|
||||||
|
- "opendal"
|
||||||
|
- "oss2"
|
||||||
|
- "supabase*"
|
||||||
|
- "tos*"
|
||||||
|
vdb:
|
||||||
|
patterns:
|
||||||
|
- "alibabacloud*"
|
||||||
|
- "chromadb"
|
||||||
|
- "clickhouse-*"
|
||||||
|
- "clickzetta-*"
|
||||||
|
- "couchbase"
|
||||||
|
- "elasticsearch"
|
||||||
|
- "opensearch-py"
|
||||||
|
- "oracledb"
|
||||||
|
- "pgvect*"
|
||||||
|
- "pymilvus"
|
||||||
|
- "pymochow"
|
||||||
|
- "pyobvector"
|
||||||
|
- "qdrant-client"
|
||||||
|
- "intersystems-*"
|
||||||
|
- "tablestore"
|
||||||
|
- "tcvectordb"
|
||||||
|
- "tidb-vector"
|
||||||
|
- "upstash-*"
|
||||||
|
- "volcengine-*"
|
||||||
|
- "weaviate-*"
|
||||||
|
- "xinference-*"
|
||||||
|
- "mo-vector"
|
||||||
|
- "mysql-connector-*"
|
||||||
|
dev:
|
||||||
|
patterns:
|
||||||
|
- "coverage"
|
||||||
|
- "dotenv-linter"
|
||||||
|
- "faker"
|
||||||
|
- "lxml-stubs"
|
||||||
|
- "basedpyright"
|
||||||
|
- "ruff"
|
||||||
|
- "pytest*"
|
||||||
|
- "types-*"
|
||||||
|
- "boto3-stubs"
|
||||||
|
- "hypothesis"
|
||||||
|
- "pandas-stubs"
|
||||||
|
- "scipy-stubs"
|
||||||
|
- "import-linter"
|
||||||
|
- "celery-types"
|
||||||
|
- "mypy*"
|
||||||
|
- "pyrefly"
|
||||||
|
python-packages:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: "lts/1.13.x"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
groups:
|
||||||
|
github-actions-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|||||||
+7
-1
@@ -1,3 +1,9 @@
|
|||||||
web:
|
web:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'web/**'
|
- any-glob-to-any-file:
|
||||||
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||||
|
<!-- If this PR was created by an automated agent, add `From <Tool Name>` as the final line of the description. Example: `From Codex`. -->
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
|
||||||
- [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
- [ ] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
||||||
- [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
- [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
||||||
- [x] I've updated the documentation accordingly.
|
- [ ] I've updated the documentation accordingly.
|
||||||
- [x] I ran `make lint` and `make type-check` (backend) and `cd web && npx lint-staged` (frontend) to appease the lint gods
|
- [ ] I ran `make lint && make type-check` (backend) and `cd web && pnpm exec vp staged` (frontend) to appease the lint gods
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_SHA=${BASE_SHA:-}
|
||||||
|
HEAD_SHA=${HEAD_SHA:-}
|
||||||
|
MAIN_REF=${MAIN_REF:-origin/main}
|
||||||
|
REMEDIATION_HINT="Changes should be made from the main branch using git cherry-pick -x."
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf 'ERROR: %s\n' "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
|
||||||
|
error "BASE_SHA and HEAD_SHA are required. $REMEDIATION_HINT"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$BASE_SHA^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Base commit '$BASE_SHA' is not available in the local git checkout."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$HEAD_SHA^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Head commit '$HEAD_SHA' is not available in the local git checkout."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git rev-parse --verify "$MAIN_REF^{commit}" > /dev/null 2>&1; then
|
||||||
|
error "Main ref '$MAIN_REF' is not available in the local git checkout. $REMEDIATION_HINT"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
checked=0
|
||||||
|
|
||||||
|
while IFS= read -r commit_sha; do
|
||||||
|
[[ -n "$commit_sha" ]] || continue
|
||||||
|
|
||||||
|
checked=$((checked + 1))
|
||||||
|
subject=$(git log -1 --format=%s "$commit_sha")
|
||||||
|
source_sha=$(
|
||||||
|
git log -1 --format=%B "$commit_sha" \
|
||||||
|
| sed -nE 's/^\(cherry picked from commit ([0-9a-fA-F]{7,64})\)$/\1/p' \
|
||||||
|
| tail -n 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -z "$source_sha" ]]; then
|
||||||
|
error "Commit $commit_sha ($subject) is missing cherry-pick provenance. $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git cat-file -e "$source_sha^{commit}" 2> /dev/null; then
|
||||||
|
error "Commit $commit_sha ($subject) references source $source_sha, but that commit is not available locally. $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git merge-base --is-ancestor "$source_sha" "$MAIN_REF"; then
|
||||||
|
error "Commit $commit_sha ($subject) references source $source_sha, but that source is not reachable from main ($MAIN_REF). $REMEDIATION_HINT"
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done < <(git rev-list --reverse "$BASE_SHA..$HEAD_SHA")
|
||||||
|
|
||||||
|
if [[ "$failed" -ne 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$checked" -eq 0 ]]; then
|
||||||
|
echo "No PR commits to check."
|
||||||
|
else
|
||||||
|
echo "Verified $checked PR commit(s) include cherry-pick provenance from main."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const repoRoot = process.cwd()
|
||||||
|
const baseSha = process.env.BASE_SHA || ''
|
||||||
|
const headSha = process.env.HEAD_SHA || ''
|
||||||
|
const files = (process.env.CHANGED_FILES || '').split(/\s+/).filter(Boolean)
|
||||||
|
const outputPath = process.env.I18N_CHANGES_OUTPUT_PATH || '/tmp/i18n-changes.json'
|
||||||
|
|
||||||
|
const englishPath = fileStem => path.join(repoRoot, 'web', 'i18n', 'en-US', `${fileStem}.json`)
|
||||||
|
|
||||||
|
const readCurrentJson = (fileStem) => {
|
||||||
|
const filePath = englishPath(fileStem)
|
||||||
|
if (!fs.existsSync(filePath))
|
||||||
|
return null
|
||||||
|
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const readBaseJson = (fileStem) => {
|
||||||
|
if (!baseSha)
|
||||||
|
return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const relativePath = `web/i18n/en-US/${fileStem}.json`
|
||||||
|
const content = execFileSync('git', ['show', `${baseSha}:${relativePath}`], { encoding: 'utf8' })
|
||||||
|
return JSON.parse(content)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const compareJson = (beforeValue, afterValue) => JSON.stringify(beforeValue) === JSON.stringify(afterValue)
|
||||||
|
|
||||||
|
const changes = {}
|
||||||
|
|
||||||
|
for (const fileStem of files) {
|
||||||
|
const currentJson = readCurrentJson(fileStem)
|
||||||
|
const beforeJson = readBaseJson(fileStem) || {}
|
||||||
|
const afterJson = currentJson || {}
|
||||||
|
const added = {}
|
||||||
|
const updated = {}
|
||||||
|
const deleted = []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(afterJson)) {
|
||||||
|
if (!(key in beforeJson)) {
|
||||||
|
added[key] = value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!compareJson(beforeJson[key], value)) {
|
||||||
|
updated[key] = {
|
||||||
|
before: beforeJson[key],
|
||||||
|
after: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(beforeJson)) {
|
||||||
|
if (!(key in afterJson))
|
||||||
|
deleted.push(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
changes[fileStem] = {
|
||||||
|
fileDeleted: currentJson === null,
|
||||||
|
added,
|
||||||
|
updated,
|
||||||
|
deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
outputPath,
|
||||||
|
JSON.stringify({
|
||||||
|
baseSha,
|
||||||
|
headSha,
|
||||||
|
files,
|
||||||
|
changes,
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
name: Anti-Slop PR Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, edited, synchronize]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
anti-slop:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
close-pr: false
|
|
||||||
failure-add-pr-labels: "needs-revision"
|
|
||||||
+144
-39
@@ -14,18 +14,17 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
api-unit:
|
||||||
name: API Tests
|
name: API Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
COVERAGE_FILE: coverage-unit
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -36,7 +35,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -49,48 +48,154 @@ jobs:
|
|||||||
run: uv sync --project api --dev
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
- name: Run dify config tests
|
- name: Run dify config tests
|
||||||
run: uv run --project api dev/pytest/pytest_config_tests.py
|
run: uv run --project api pytest api/tests/unit_tests/configs/test_env_consistency.py
|
||||||
|
|
||||||
- name: Set up dotenvs
|
- name: Run Unit Tests
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
- name: Set up Sandbox
|
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
|
||||||
with:
|
|
||||||
compose-file: |
|
|
||||||
docker/docker-compose.middleware.yaml
|
|
||||||
services: |
|
|
||||||
db_postgres
|
|
||||||
redis
|
|
||||||
sandbox
|
|
||||||
ssrf_proxy
|
|
||||||
|
|
||||||
- name: setup test config
|
|
||||||
run: |
|
|
||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
|
||||||
|
|
||||||
- name: Run API Tests
|
|
||||||
env:
|
|
||||||
STORAGE_TYPE: opendal
|
|
||||||
OPENDAL_SCHEME: fs
|
|
||||||
OPENDAL_FS_ROOT: /tmp/dify-storage
|
|
||||||
run: |
|
run: |
|
||||||
uv run --project api pytest \
|
uv run --project api pytest \
|
||||||
|
-p no:benchmark \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-20}" \
|
||||||
|
-n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers
|
||||||
|
# Controller tests register Flask routes at import time, so keep them out of xdist.
|
||||||
|
uv run --project api pytest \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-20}" \
|
||||||
|
--cov-append \
|
||||||
|
api/tests/unit_tests/controllers
|
||||||
|
|
||||||
|
- name: Upload unit coverage data
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: api-coverage-unit
|
||||||
|
path: coverage-unit
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
api-integration:
|
||||||
|
name: API Integration Tests
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
env:
|
||||||
|
COVERAGE_FILE: coverage-integration
|
||||||
|
STORAGE_TYPE: opendal
|
||||||
|
OPENDAL_SCHEME: fs
|
||||||
|
OPENDAL_FS_ROOT: /tmp/dify-storage
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.12"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Check UV lockfile
|
||||||
|
run: uv lock --project api --check
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Run Integration Tests
|
||||||
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
-p no:benchmark \
|
||||||
|
--start-middleware \
|
||||||
-n auto \
|
-n auto \
|
||||||
--timeout "${PYTEST_TIMEOUT:-180}" \
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
api/tests/integration_tests/workflow \
|
api/tests/integration_tests/workflow \
|
||||||
api/tests/integration_tests/tools \
|
api/tests/integration_tests/tools \
|
||||||
api/tests/test_containers_integration_tests \
|
api/tests/test_containers_integration_tests
|
||||||
api/tests/unit_tests
|
|
||||||
|
- name: Upload integration coverage data
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: api-coverage-integration
|
||||||
|
path: coverage-integration
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
api-coverage:
|
||||||
|
name: API Coverage
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- api-unit
|
||||||
|
- api-integration
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
COVERAGE_FILE: .coverage
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: "3.12"
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Download coverage data
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
path: coverage-data
|
||||||
|
pattern: api-coverage-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Combine coverage
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "### API Coverage" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
unit_coverage="$(find coverage-data -type f -name coverage-unit -print -quit)"
|
||||||
|
integration_coverage="$(find coverage-data -type f -name coverage-integration -print -quit)"
|
||||||
|
: "${unit_coverage:?coverage-unit artifact not found}"
|
||||||
|
: "${integration_coverage:?coverage-integration artifact not found}"
|
||||||
|
|
||||||
|
report_file="$(mktemp)"
|
||||||
|
uv run --project api coverage combine "$unit_coverage" "$integration_coverage"
|
||||||
|
uv run --project api coverage report --show-missing | tee "$report_file"
|
||||||
|
echo "Summary: \`$(tail -n 1 "$report_file")\`" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "<details><summary>Coverage report</summary>"
|
||||||
|
echo ""
|
||||||
|
echo '```'
|
||||||
|
cat "$report_file"
|
||||||
|
echo '```'
|
||||||
|
echo "</details>"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
uv run --project api coverage xml -o coverage.xml
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: ${{ env.CODECOV_TOKEN != '' && matrix.python-version == '3.12' }}
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage.xml
|
files: ./coverage.xml
|
||||||
disable_search: true
|
disable_search: true
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ name: autofix.ci
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
permissions:
|
permissions:
|
||||||
@@ -10,13 +13,19 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
autofix:
|
autofix:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- name: Complete merge group check
|
||||||
|
if: github.event_name == 'merge_group'
|
||||||
|
run: echo "autofix.ci updates pull request branches, not merge group refs."
|
||||||
|
|
||||||
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Check Docker Compose inputs
|
- name: Check Docker Compose inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: docker-compose-changes
|
id: docker-compose-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
docker/generate_docker_compose
|
docker/generate_docker_compose
|
||||||
@@ -24,30 +33,48 @@ jobs:
|
|||||||
docker/docker-compose-template.yaml
|
docker/docker-compose-template.yaml
|
||||||
docker/docker-compose.yaml
|
docker/docker-compose.yaml
|
||||||
- name: Check web inputs
|
- name: Check web inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: web-changes
|
id: web-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
web/**
|
web/**
|
||||||
|
packages/**
|
||||||
|
package.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
.nvmrc
|
||||||
- name: Check api inputs
|
- name: Check api inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
id: api-changes
|
id: api-changes
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
api/**
|
api/**
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- name: Check dify-agent inputs
|
||||||
|
if: github.event_name != 'merge_group'
|
||||||
|
id: dify-agent-changes
|
||||||
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
dify-agent/**/*.py
|
||||||
|
dify-agent/pyproject.toml
|
||||||
|
dify-agent/uv.lock
|
||||||
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
|
||||||
- name: Generate Docker Compose
|
- name: Generate Docker Compose
|
||||||
if: steps.docker-compose-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
./generate_docker_compose
|
./generate_docker_compose
|
||||||
|
|
||||||
- if: steps.api-changes.outputs.any_changed == 'true'
|
- if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd api
|
cd api
|
||||||
uv sync --dev
|
uv sync --dev
|
||||||
@@ -58,14 +85,25 @@ jobs:
|
|||||||
# Format code
|
# Format code
|
||||||
uv run ruff format ..
|
uv run ruff format ..
|
||||||
|
|
||||||
|
- if: github.event_name != 'merge_group' && steps.dify-agent-changes.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
cd dify-agent
|
||||||
|
uv sync --dev
|
||||||
|
# fmt first to avoid line too long
|
||||||
|
uv run ruff format .
|
||||||
|
# Fix lint errors
|
||||||
|
uv run ruff check --fix .
|
||||||
|
# Format code
|
||||||
|
uv run ruff format .
|
||||||
|
|
||||||
- name: count migration progress
|
- name: count migration progress
|
||||||
if: steps.api-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd api
|
cd api
|
||||||
./cnt_base.sh
|
./cnt_base.sh
|
||||||
|
|
||||||
- name: ast-grep
|
- name: ast-grep
|
||||||
if: steps.api-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
# ast-grep exits 1 if no matches are found; allow idempotent runs.
|
# ast-grep exits 1 if no matches are found; allow idempotent runs.
|
||||||
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
uvx --from ast-grep-cli ast-grep --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all || true
|
||||||
@@ -95,13 +133,23 @@ jobs:
|
|||||||
find . -name "*.py.bak" -type f -delete
|
find . -name "*.py.bak" -type f -delete
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
if: steps.web-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group'
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Generate API docs
|
||||||
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
cd api
|
||||||
|
uv run dev/generate_swagger_markdown_docs.py --swagger-dir ../packages/contracts/openapi --markdown-dir openapi/markdown --keep-swagger-json
|
||||||
|
|
||||||
|
- name: Generate frontend contracts
|
||||||
|
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
||||||
|
run: pnpm --dir packages/contracts gen-api-contract-from-openapi
|
||||||
|
|
||||||
- name: ESLint autofix
|
- name: ESLint autofix
|
||||||
if: steps.web-changes.outputs.any_changed == 'true'
|
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd web
|
|
||||||
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
|
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
|
||||||
|
|
||||||
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3
|
- if: github.event_name != 'merge_group'
|
||||||
|
uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
|
||||||
|
|||||||
@@ -24,27 +24,42 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}
|
runs-on: ${{ matrix.runs_on }}
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: "build-api-amd64"
|
- service_name: "build-api-amd64"
|
||||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||||
context: "api"
|
artifact_context: "api"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-api-arm64"
|
- service_name: "build-api-arm64"
|
||||||
image_name_env: "DIFY_API_IMAGE_NAME"
|
image_name_env: "DIFY_API_IMAGE_NAME"
|
||||||
context: "api"
|
artifact_context: "api"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-web-amd64"
|
- service_name: "build-web-amd64"
|
||||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||||
context: "web"
|
artifact_context: "web"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
- service_name: "build-web-arm64"
|
- service_name: "build-web-arm64"
|
||||||
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
image_name_env: "DIFY_WEB_IMAGE_NAME"
|
||||||
context: "web"
|
artifact_context: "web"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
@@ -53,16 +68,13 @@ jobs:
|
|||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up Depot CLI
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
@@ -72,17 +84,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||||
|
context: ${{ matrix.build_context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: |
|
build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||||
COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
|
||||||
ENABLE_PROD_SOURCEMAP=${{ matrix.context == 'web' && github.ref_name == 'deploy/dev' }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true
|
outputs: type=image,name=${{ env[matrix.image_name_env] }},push-by-digest=true,name-canonical=true,push=true
|
||||||
cache-from: type=gha,scope=${{ matrix.service_name }}
|
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.service_name }}
|
|
||||||
|
|
||||||
- name: Export digest
|
- name: Export digest
|
||||||
env:
|
env:
|
||||||
@@ -93,16 +103,40 @@ jobs:
|
|||||||
touch "/tmp/digests/${sanitized_digest}"
|
touch "/tmp/digests/${sanitized_digest}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.context }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ matrix.artifact_context }}-${{ env.PLATFORM_PAIR }}
|
||||||
path: /tmp/digests/*
|
path: /tmp/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
fork-build-validate:
|
||||||
|
if: github.repository != 'langgenius/dify'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- service_name: "validate-api-amd64"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
|
- service_name: "validate-web-amd64"
|
||||||
|
build_context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
|
- name: Validate Docker image
|
||||||
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
|
with:
|
||||||
|
push: false
|
||||||
|
context: ${{ matrix.build_context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
|
platforms: linux/amd64
|
||||||
|
|
||||||
create-manifest:
|
create-manifest:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -122,7 +156,7 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: CLI Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'difyctl-v*'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cli-release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: build standalone binaries (all targets)
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
if: github.repository == 'langgenius/dify'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: ./cli
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup web environment
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Read cli/package.json
|
||||||
|
id: manifest
|
||||||
|
run: |
|
||||||
|
version=$(node -p "require('./package.json').version")
|
||||||
|
channel=$(node -p "require('./package.json').difyctl.channel")
|
||||||
|
minDify=$(node -p "require('./package.json').difyctl.compat.minDify")
|
||||||
|
maxDify=$(node -p "require('./package.json').difyctl.compat.maxDify")
|
||||||
|
{
|
||||||
|
echo "version=$version"
|
||||||
|
echo "channel=$channel"
|
||||||
|
echo "minDify=$minDify"
|
||||||
|
echo "maxDify=$maxDify"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Validate manifest
|
||||||
|
run: scripts/release-validate-manifest.sh
|
||||||
|
|
||||||
|
- name: Install cross-arch native prebuilds
|
||||||
|
# Re-installs node_modules with every @napi-rs/keyring platform variant
|
||||||
|
# so `bun build --compile` can embed the right .node into each target.
|
||||||
|
working-directory: ./
|
||||||
|
run: NPM_CONFIG_USERCONFIG="$PWD/cli/scripts/cross-arch.npmrc" pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Compile standalone binaries (all targets)
|
||||||
|
env:
|
||||||
|
CLI_VERSION: ${{ steps.manifest.outputs.version }}
|
||||||
|
DIFYCTL_CHANNEL: ${{ steps.manifest.outputs.channel }}
|
||||||
|
DIFYCTL_MIN_DIFY: ${{ steps.manifest.outputs.minDify }}
|
||||||
|
DIFYCTL_MAX_DIFY: ${{ steps.manifest.outputs.maxDify }}
|
||||||
|
run: |
|
||||||
|
DIFYCTL_COMMIT="$(git rev-parse HEAD)" \
|
||||||
|
DIFYCTL_BUILD_DATE="$(git log -1 --format=%cI HEAD)" \
|
||||||
|
pnpm build:bin
|
||||||
|
|
||||||
|
- name: Generate sha256 checksum file
|
||||||
|
env:
|
||||||
|
CLI_VERSION: ${{ steps.manifest.outputs.version }}
|
||||||
|
run: scripts/release-write-checksums.sh
|
||||||
|
|
||||||
|
- name: Publish GitHub Release
|
||||||
|
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||||
|
with:
|
||||||
|
tag_name: difyctl-v${{ steps.manifest.outputs.version }}
|
||||||
|
name: difyctl ${{ steps.manifest.outputs.version }}
|
||||||
|
prerelease: ${{ steps.manifest.outputs.channel != 'stable' }}
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
files: |
|
||||||
|
cli/dist/bin/difyctl-v*
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: CLI Smoke (live dify)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dify_version:
|
||||||
|
description: "Dify image tag to test against (e.g. 1.7.0)"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
cli_ref:
|
||||||
|
description: "Git ref to build the cli from (default: current branch)"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- name: Checkout cli ref
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.cli_ref || github.ref }}
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup web environment
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Bring up dify
|
||||||
|
env:
|
||||||
|
DIFY_VERSION: ${{ inputs.dify_version }}
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
|
cp .env.example .env
|
||||||
|
DIFY_API_IMAGE_TAG="$DIFY_VERSION" \
|
||||||
|
DIFY_WEB_IMAGE_TAG="$DIFY_VERSION" \
|
||||||
|
docker compose up -d api worker web db redis
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -fsS http://localhost:5001/health >/dev/null 2>&1; then
|
||||||
|
echo "dify api ready after ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Run smoke against live dify
|
||||||
|
working-directory: ./cli
|
||||||
|
run: pnpm exec tsx scripts/run-smoke.ts --base-url http://localhost:5001
|
||||||
|
|
||||||
|
- name: Dump dify logs on failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
cd docker
|
||||||
|
docker compose logs api worker web --tail=200
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
name: CLI Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: cli-tests-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: CLI Tests (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [depot-ubuntu-24.04, windows-latest, macos-latest]
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: ./cli
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup web environment
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: CI pipeline (typecheck, lint, coverage, build)
|
||||||
|
run: pnpm ci
|
||||||
|
|
||||||
|
- name: Report coverage
|
||||||
|
if: ${{ env.CODECOV_TOKEN != '' && matrix.os == 'depot-ubuntu-24.04' }}
|
||||||
|
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||||
|
with:
|
||||||
|
directory: cli/coverage
|
||||||
|
flags: cli
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
||||||
@@ -9,7 +9,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
db-migration-test-postgres:
|
db-migration-test-postgres:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@@ -37,10 +37,10 @@ jobs:
|
|||||||
- name: Prepare middleware env
|
- name: Prepare middleware env
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
run: uv run --directory api flask upgrade-db
|
run: uv run --directory api flask upgrade-db
|
||||||
|
|
||||||
db-migration-test-mysql:
|
db-migration-test-mysql:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@@ -87,14 +87,14 @@ jobs:
|
|||||||
- name: Prepare middleware env for MySQL
|
- name: Prepare middleware env for MySQL
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
cp middleware.env.example middleware.env
|
cp envs/middleware.env.example middleware.env
|
||||||
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
sed -i 's/DB_TYPE=postgresql/DB_TYPE=mysql/' middleware.env
|
||||||
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
sed -i 's/DB_HOST=db_postgres/DB_HOST=db_mysql/' middleware.env
|
||||||
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
sed -i 's/DB_PORT=5432/DB_PORT=3306/' middleware.env
|
||||||
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
|
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=mysql/' middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@@ -110,6 +110,28 @@ jobs:
|
|||||||
sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
|
sed -i 's/DB_PORT=5432/DB_PORT=3306/' .env
|
||||||
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env
|
sed -i 's/DB_USERNAME=postgres/DB_USERNAME=root/' .env
|
||||||
|
|
||||||
|
# hoverkraft-tech/[email protected] only waits for `docker compose up -d`
|
||||||
|
# to return (container processes started); it does not wait on healthcheck
|
||||||
|
# status. mysql:8.0's first-time init takes 15-30s, so without an explicit
|
||||||
|
# wait the migration runs while InnoDB is still initialising and gets
|
||||||
|
# killed with "Lost connection during query". Poll a real SELECT until it
|
||||||
|
# succeeds.
|
||||||
|
- name: Wait for MySQL to accept queries
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if docker run --rm --network host mysql:8.0 \
|
||||||
|
mysql -h 127.0.0.1 -P 3306 -uroot -pdifyai123456 \
|
||||||
|
-e 'SELECT 1' >/dev/null 2>&1; then
|
||||||
|
echo "MySQL ready after ${i}s"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo "MySQL not ready after 60s; dumping container logs:"
|
||||||
|
docker compose -f docker/docker-compose.middleware.yaml --profile mysql logs --tail=200 db_mysql
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Run DB Migration
|
- name: Run DB Migration
|
||||||
env:
|
env:
|
||||||
DEBUG: true
|
DEBUG: true
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Deploy Agent Dev
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Build and Push API & Web"]
|
|
||||||
branches:
|
|
||||||
- "deploy/agent-dev"
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.event.workflow_run.head_branch == 'deploy/agent-dev'
|
|
||||||
steps:
|
|
||||||
- name: Deploy to server
|
|
||||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.AGENT_DEV_SSH_HOST }}
|
|
||||||
username: ${{ secrets.SSH_USER }}
|
|
||||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
script: |
|
|
||||||
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
|
|
||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/dev'
|
github.event.workflow_run.head_branch == 'deploy/dev'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'build/feat/hitl'
|
github.event.workflow_run.head_branch == 'build/feat/hitl'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Deploy SaaS
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build and Push API & Web"]
|
||||||
|
branches:
|
||||||
|
- "deploy/saas"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
if: |
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.head_branch == 'deploy/saas'
|
||||||
|
steps:
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SAAS_DEV_SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
script: |
|
||||||
|
${{ vars.SSH_SCRIPT_SAAS_DEV || secrets.SSH_SCRIPT_SAAS_DEV }}
|
||||||
@@ -6,6 +6,12 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
paths:
|
paths:
|
||||||
- api/Dockerfile
|
- api/Dockerfile
|
||||||
|
- api/Dockerfile.dockerignore
|
||||||
|
- api/pyproject.toml
|
||||||
|
- api/uv.lock
|
||||||
|
- dify-agent/pyproject.toml
|
||||||
|
- dify-agent/README.md
|
||||||
|
- dify-agent/src/**
|
||||||
- web/Dockerfile
|
- web/Dockerfile
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -14,35 +20,69 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
build-docker:
|
||||||
runs-on: ubuntu-latest
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: ${{ matrix.runs_on }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- service_name: "api-amd64"
|
- service_name: "api-amd64"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
context: "api"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
- service_name: "api-arm64"
|
- service_name: "api-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
context: "api"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
- service_name: "web-amd64"
|
- service_name: "web-amd64"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
context: "web"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
- service_name: "web-arm64"
|
- service_name: "web-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
context: "web"
|
runs_on: depot-ubuntu-24.04-4
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up Depot CLI
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
||||||
|
with:
|
||||||
|
project: ${{ vars.DEPOT_PROJECT_ID }}
|
||||||
|
push: false
|
||||||
|
context: ${{ matrix.context }}
|
||||||
|
file: ${{ matrix.file }}
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
build-docker-fork:
|
||||||
|
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- service_name: "api-amd64"
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "api/Dockerfile"
|
||||||
|
- service_name: "web-amd64"
|
||||||
|
context: "{{defaultContext}}"
|
||||||
|
file: "web/Dockerfile"
|
||||||
|
steps:
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
context: "{{defaultContext}}:${{ matrix.context }}"
|
context: ${{ matrix.context }}
|
||||||
file: "${{ matrix.file }}"
|
file: ${{ matrix.file }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: linux/amd64
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
yq eval '.services.weaviate.ports += ["8080:8080"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.weaviate.ports += ["50051:50051"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.qdrant.ports += ["6333:6333"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.chroma.ports += ["8000:8000"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["milvus-standalone"].ports += ["19530:19530"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.pgvector.ports += ["5433:5432"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["pgvecto-rs"].ports += ["5431:5432"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services["elasticsearch"].ports += ["9200:9200"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.couchbase-server.ports += ["8091-8096:8091-8096"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.couchbase-server.ports += ["11210:11210"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.tidb.ports += ["4000:4000"]' -i docker/tidb/docker-compose.yaml
|
|
||||||
yq eval '.services.oceanbase.ports += ["2881:2881"]' -i docker/docker-compose.yaml
|
|
||||||
yq eval '.services.opengauss.ports += ["6600:6600"]' -i docker/docker-compose.yaml
|
|
||||||
|
|
||||||
echo "Ports exposed for sandbox, weaviate (HTTP 8080, gRPC 50051), tidb, qdrant, chroma, milvus, pgvector, pgvecto-rs, elasticsearch, couchbase, opengauss"
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Hotfix Cherry-Pick Provenance
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'lts/**'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- ready_for_review
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: hotfix-cherry-pick-${{ github.event.pull_request.number || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-cherry-pick-provenance:
|
||||||
|
name: Require cherry-pick provenance
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch PR base, PR head, and main
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
git fetch --no-tags --prune origin \
|
||||||
|
"+refs/heads/main:refs/remotes/origin/main" \
|
||||||
|
"+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" \
|
||||||
|
"+refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head"
|
||||||
|
|
||||||
|
- name: Load checker from main
|
||||||
|
run: git show origin/main:.github/scripts/check-hotfix-cherry-picks.sh > "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||||
|
|
||||||
|
- name: Check PR commits
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
MAIN_REF: origin/main
|
||||||
|
run: bash "$RUNNER_TEMP/check-hotfix-cherry-picks.sh"
|
||||||
@@ -7,8 +7,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|||||||
+437
-18
@@ -3,10 +3,14 @@ name: Main CI Pipeline
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: write
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
checks: write
|
checks: write
|
||||||
@@ -17,12 +21,29 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
pre_job:
|
||||||
|
name: Skip Duplicate Checks
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip || 'false' }}
|
||||||
|
steps:
|
||||||
|
- id: skip_check
|
||||||
|
continue-on-error: true
|
||||||
|
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1
|
||||||
|
with:
|
||||||
|
cancel_others: 'true'
|
||||||
|
concurrent_skipping: same_content_newer
|
||||||
|
|
||||||
# Check which paths were changed to determine which tests to run
|
# Check which paths were changed to determine which tests to run
|
||||||
check-changes:
|
check-changes:
|
||||||
name: Check Changed Files
|
name: Check Changed Files
|
||||||
runs-on: ubuntu-latest
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
api-changed: ${{ steps.changes.outputs.api }}
|
api-changed: ${{ steps.changes.outputs.api }}
|
||||||
|
cli-changed: ${{ steps.changes.outputs.cli }}
|
||||||
|
e2e-changed: ${{ steps.changes.outputs.e2e }}
|
||||||
web-changed: ${{ steps.changes.outputs.web }}
|
web-changed: ${{ steps.changes.outputs.web }}
|
||||||
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
vdb-changed: ${{ steps.changes.outputs.vdb }}
|
||||||
migration-changed: ${{ steps.changes.outputs.migration }}
|
migration-changed: ${{ steps.changes.outputs.migration }}
|
||||||
@@ -34,49 +55,447 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
api:
|
api:
|
||||||
- 'api/**'
|
- 'api/**'
|
||||||
- 'docker/**'
|
|
||||||
- '.github/workflows/api-tests.yml'
|
- '.github/workflows/api-tests.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/volumes/sandbox/conf/**'
|
||||||
|
cli:
|
||||||
|
- 'cli/**'
|
||||||
|
- 'packages/tsconfig/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- 'eslint.config.mjs'
|
||||||
|
- '.npmrc'
|
||||||
|
- '.nvmrc'
|
||||||
|
- '.github/workflows/cli-tests.yml'
|
||||||
|
- '.github/workflows/cli-docker-build.yml'
|
||||||
|
- '.github/actions/setup-web/**'
|
||||||
web:
|
web:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
- '.github/workflows/web-tests.yml'
|
- '.github/workflows/web-tests.yml'
|
||||||
- '.github/actions/setup-web/**'
|
- '.github/actions/setup-web/**'
|
||||||
|
e2e:
|
||||||
|
- 'api/**'
|
||||||
|
- 'api/pyproject.toml'
|
||||||
|
- 'api/uv.lock'
|
||||||
|
- 'e2e/**'
|
||||||
|
- 'web/**'
|
||||||
|
- 'packages/**'
|
||||||
|
- 'package.json'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
|
- 'pnpm-workspace.yaml'
|
||||||
|
- '.nvmrc'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- '.github/workflows/web-e2e.yml'
|
||||||
|
- '.github/actions/setup-web/**'
|
||||||
vdb:
|
vdb:
|
||||||
- 'api/core/rag/datasource/**'
|
- 'api/core/rag/datasource/**'
|
||||||
- 'docker/**'
|
- 'api/tests/integration_tests/vdb/**'
|
||||||
|
- 'api/conftest.py'
|
||||||
|
- 'api/tests/pytest_dify.py'
|
||||||
|
- 'api/providers/vdb/*/tests/**'
|
||||||
- '.github/workflows/vdb-tests.yml'
|
- '.github/workflows/vdb-tests.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.pytest.ports.yaml'
|
||||||
|
- 'docker/docker-compose.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/certbot/**'
|
||||||
|
- 'docker/couchbase-server/**'
|
||||||
|
- 'docker/elasticsearch/**'
|
||||||
|
- 'docker/iris/**'
|
||||||
|
- 'docker/nginx/**'
|
||||||
|
- 'docker/pgvector/**'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/startupscripts/**'
|
||||||
|
- 'docker/tidb/**'
|
||||||
|
- 'docker/volumes/**'
|
||||||
- 'api/uv.lock'
|
- 'api/uv.lock'
|
||||||
- 'api/pyproject.toml'
|
- 'api/pyproject.toml'
|
||||||
migration:
|
migration:
|
||||||
- 'api/migrations/**'
|
- 'api/migrations/**'
|
||||||
|
- 'api/.env.example'
|
||||||
- '.github/workflows/db-migration-test.yml'
|
- '.github/workflows/db-migration-test.yml'
|
||||||
|
- 'docker/.env.example'
|
||||||
|
- 'docker/envs/middleware.env.example'
|
||||||
|
- 'docker/docker-compose.middleware.yaml'
|
||||||
|
- 'docker/docker-compose-template.yaml'
|
||||||
|
- 'docker/generate_docker_compose'
|
||||||
|
- 'docker/ssrf_proxy/**'
|
||||||
|
- 'docker/volumes/sandbox/conf/**'
|
||||||
|
|
||||||
# Run tests in parallel
|
# Run tests in parallel while always emitting stable required checks.
|
||||||
api-tests:
|
api-tests-run:
|
||||||
name: API Tests
|
name: Run API Tests
|
||||||
needs: check-changes
|
needs:
|
||||||
if: needs.check-changes.outputs.api-changed == 'true'
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed == 'true'
|
||||||
uses: ./.github/workflows/api-tests.yml
|
uses: ./.github/workflows/api-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
web-tests:
|
api-tests-skip:
|
||||||
name: Web Tests
|
name: Skip API Tests
|
||||||
needs: check-changes
|
needs:
|
||||||
if: needs.check-changes.outputs.web-changed == 'true'
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.api-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped API tests
|
||||||
|
run: echo "No API-related changes detected; skipping API tests."
|
||||||
|
|
||||||
|
api-tests:
|
||||||
|
name: API Tests
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- api-tests-run
|
||||||
|
- api-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize API Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.api-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.api-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.api-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "API tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "API tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "API tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "API tests were skipped because no API-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "API tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
cli-tests-run:
|
||||||
|
name: Run CLI Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.cli-changed == 'true'
|
||||||
|
uses: ./.github/workflows/cli-tests.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
cli-tests-skip:
|
||||||
|
name: Skip CLI Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.cli-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped CLI tests
|
||||||
|
run: echo "No CLI-related changes detected; skipping CLI tests."
|
||||||
|
|
||||||
|
cli-tests:
|
||||||
|
name: CLI Tests
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- cli-tests-run
|
||||||
|
- cli-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize CLI Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.cli-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.cli-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.cli-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "CLI tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "CLI tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CLI tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "CLI tests were skipped because no CLI-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "CLI tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
web-tests-run:
|
||||||
|
name: Run Web Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed == 'true'
|
||||||
uses: ./.github/workflows/web-tests.yml
|
uses: ./.github/workflows/web-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
web-tests-skip:
|
||||||
|
name: Skip Web Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.web-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped web tests
|
||||||
|
run: echo "No web-related changes detected; skipping web tests."
|
||||||
|
|
||||||
|
web-tests:
|
||||||
|
name: Web Tests
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- web-tests-run
|
||||||
|
- web-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize Web Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.web-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.web-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.web-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "Web tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web tests were skipped because no web-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
web-e2e-run:
|
||||||
|
name: Run Web Full-Stack E2E
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed == 'true'
|
||||||
|
uses: ./.github/workflows/web-e2e.yml
|
||||||
|
|
||||||
|
web-e2e-skip:
|
||||||
|
name: Skip Web Full-Stack E2E
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.e2e-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped web full-stack e2e
|
||||||
|
run: echo "No E2E-related changes detected; skipping web full-stack E2E."
|
||||||
|
|
||||||
|
web-e2e:
|
||||||
|
name: Web Full-Stack E2E
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- web-e2e-run
|
||||||
|
- web-e2e-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize Web Full-Stack E2E status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.e2e-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.web-e2e-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.web-e2e-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "Web full-stack E2E was skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web full-stack E2E ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web full-stack E2E was required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "Web full-stack E2E was skipped because no E2E-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Web full-stack E2E was not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
style-check:
|
style-check:
|
||||||
name: Style Check
|
name: Style Check
|
||||||
|
needs: pre_job
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true'
|
||||||
uses: ./.github/workflows/style.yml
|
uses: ./.github/workflows/style.yml
|
||||||
|
|
||||||
|
vdb-tests-run:
|
||||||
|
name: Run VDB Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed == 'true'
|
||||||
|
uses: ./.github/workflows/vdb-tests.yml
|
||||||
|
|
||||||
|
vdb-tests-skip:
|
||||||
|
name: Skip VDB Tests
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.vdb-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped VDB tests
|
||||||
|
run: echo "No VDB-related changes detected; skipping VDB tests."
|
||||||
|
|
||||||
vdb-tests:
|
vdb-tests:
|
||||||
name: VDB Tests
|
name: VDB Tests
|
||||||
needs: check-changes
|
if: ${{ always() }}
|
||||||
if: needs.check-changes.outputs.vdb-changed == 'true'
|
needs:
|
||||||
uses: ./.github/workflows/vdb-tests.yml
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- vdb-tests-run
|
||||||
|
- vdb-tests-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize VDB Tests status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.vdb-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.vdb-tests-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.vdb-tests-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "VDB tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "VDB tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VDB tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "VDB tests were skipped because no VDB-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VDB tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
db-migration-test-run:
|
||||||
|
name: Run DB Migration Test
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed == 'true'
|
||||||
|
uses: ./.github/workflows/db-migration-test.yml
|
||||||
|
|
||||||
|
db-migration-test-skip:
|
||||||
|
name: Skip DB Migration Test
|
||||||
|
needs:
|
||||||
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
if: needs.pre_job.outputs.should_skip != 'true' && needs.check-changes.outputs.migration-changed != 'true'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Report skipped DB migration tests
|
||||||
|
run: echo "No migration-related changes detected; skipping DB migration tests."
|
||||||
|
|
||||||
db-migration-test:
|
db-migration-test:
|
||||||
name: DB Migration Test
|
name: DB Migration Test
|
||||||
needs: check-changes
|
if: ${{ always() }}
|
||||||
if: needs.check-changes.outputs.migration-changed == 'true'
|
needs:
|
||||||
uses: ./.github/workflows/db-migration-test.yml
|
- pre_job
|
||||||
|
- check-changes
|
||||||
|
- db-migration-test-run
|
||||||
|
- db-migration-test-skip
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Finalize DB Migration Test status
|
||||||
|
env:
|
||||||
|
SHOULD_SKIP_WORKFLOW: ${{ needs.pre_job.outputs.should_skip }}
|
||||||
|
TESTS_CHANGED: ${{ needs.check-changes.outputs.migration-changed }}
|
||||||
|
RUN_RESULT: ${{ needs.db-migration-test-run.result }}
|
||||||
|
SKIP_RESULT: ${{ needs.db-migration-test-skip.result }}
|
||||||
|
run: |
|
||||||
|
if [[ "$SHOULD_SKIP_WORKFLOW" == 'true' ]]; then
|
||||||
|
echo "DB migration tests were skipped because this workflow run duplicated a successful or newer run."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TESTS_CHANGED" == 'true' ]]; then
|
||||||
|
if [[ "$RUN_RESULT" == 'success' ]]; then
|
||||||
|
echo "DB migration tests ran successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DB migration tests were required but finished with result: $RUN_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$SKIP_RESULT" == 'success' ]]; then
|
||||||
|
echo "DB migration tests were skipped because no migration-related files changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DB migration tests were not required, but the skip job finished with result: $SKIP_RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
comment:
|
comment:
|
||||||
name: Comment PR with pyrefly diff
|
name: Comment PR with pyrefly diff
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download pyrefly diff artifact
|
- name: Download pyrefly diff artifact
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
run: unzip -o pyrefly_diff.zip
|
run: unzip -o pyrefly_diff.zip
|
||||||
|
|
||||||
- name: Post comment
|
- name: Post comment
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@@ -76,13 +76,29 @@ jobs:
|
|||||||
diff += '\\n\\n... (truncated) ...';
|
diff += '\\n\\n... (truncated) ...';
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = diff.trim()
|
if (diff.trim()) {
|
||||||
? '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>'
|
const body = '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>';
|
||||||
: '### Pyrefly Diff\nNo changes detected.';
|
const marker = '### Pyrefly Diff';
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const existing = comments.find((comment) => comment.body.startsWith(marker));
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
if (existing) {
|
||||||
issue_number: prNumber,
|
await github.rest.issues.updateComment({
|
||||||
owner: context.repo.owner,
|
comment_id: existing.id,
|
||||||
repo: context.repo.repo,
|
owner: context.repo.owner,
|
||||||
body,
|
repo: context.repo.repo,
|
||||||
});
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pyrefly-diff:
|
pyrefly-diff:
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python & UV
|
- name: Setup Python & UV
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
|
|
||||||
@@ -50,12 +50,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
|
diff -u /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true
|
||||||
|
|
||||||
|
- name: Check if line counts match
|
||||||
|
id: line_count_check
|
||||||
|
run: |
|
||||||
|
base_lines=$(wc -l < /tmp/pyrefly_base.txt)
|
||||||
|
pr_lines=$(wc -l < /tmp/pyrefly_pr.txt)
|
||||||
|
if [ "$base_lines" -eq "$pr_lines" ]; then
|
||||||
|
echo "same=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "same=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Save PR number
|
- name: Save PR number
|
||||||
run: |
|
run: |
|
||||||
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
||||||
|
|
||||||
- name: Upload pyrefly diff
|
- name: Upload pyrefly diff
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: pyrefly_diff
|
name: pyrefly_diff
|
||||||
path: |
|
path: |
|
||||||
@@ -63,8 +74,8 @@ jobs:
|
|||||||
pr_number.txt
|
pr_number.txt
|
||||||
|
|
||||||
- name: Comment PR with pyrefly diff
|
- name: Comment PR with pyrefly diff
|
||||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && steps.line_count_check.outputs.same == 'false' }}
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
@@ -92,9 +103,26 @@ jobs:
|
|||||||
].join('\n')
|
].join('\n')
|
||||||
: '### Pyrefly Diff\nNo changes detected.';
|
: '### Pyrefly Diff\nNo changes detected.';
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
const marker = '### Pyrefly Diff';
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
issue_number: prNumber,
|
issue_number: prNumber,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body,
|
|
||||||
});
|
});
|
||||||
|
const existing = comments.find((comment) => comment.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
name: Comment with Pyrefly Type Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- Pyrefly Type Coverage
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Comment PR with type coverage
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout default branch (trusted code)
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Setup Python & UV
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Download type coverage artifact
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: ${{ github.event.workflow_run.id }},
|
||||||
|
});
|
||||||
|
const match = artifacts.data.artifacts.find((artifact) =>
|
||||||
|
artifact.name === 'pyrefly_type_coverage'
|
||||||
|
);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('pyrefly_type_coverage artifact not found');
|
||||||
|
}
|
||||||
|
const download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: match.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
fs.writeFileSync('pyrefly_type_coverage.zip', Buffer.from(download.data));
|
||||||
|
|
||||||
|
- name: Unzip artifact
|
||||||
|
run: unzip -o pyrefly_type_coverage.zip
|
||||||
|
|
||||||
|
- name: Render coverage markdown from structured data
|
||||||
|
id: render
|
||||||
|
run: |
|
||||||
|
comment_body="$(uv run --directory api python libs/pyrefly_type_coverage.py \
|
||||||
|
--base "$GITHUB_WORKSPACE/base_report.json" \
|
||||||
|
< "$GITHUB_WORKSPACE/pr_report.json")"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Pyrefly Type Coverage"
|
||||||
|
echo ""
|
||||||
|
echo "$comment_body"
|
||||||
|
} > /tmp/type_coverage_comment.md
|
||||||
|
|
||||||
|
- name: Post comment
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
|
||||||
|
let prNumber = null;
|
||||||
|
try {
|
||||||
|
prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10);
|
||||||
|
} catch (err) {
|
||||||
|
const prs = context.payload.workflow_run.pull_requests || [];
|
||||||
|
if (prs.length > 0 && prs[0].number) {
|
||||||
|
prNumber = prs[0].number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!prNumber) {
|
||||||
|
throw new Error('PR number not found in artifact or workflow_run payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing comment if one exists, otherwise create new
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const marker = '### Pyrefly Type Coverage';
|
||||||
|
const existing = comments.find(c => c.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
name: Pyrefly Type Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'api/**/*.py'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pyrefly-type-coverage:
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR branch
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Python & UV
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Run pyrefly report on PR branch
|
||||||
|
run: |
|
||||||
|
uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_pr.tmp && \
|
||||||
|
mv /tmp/pyrefly_report_pr.tmp /tmp/pyrefly_report_pr.json || \
|
||||||
|
echo '{}' > /tmp/pyrefly_report_pr.json
|
||||||
|
|
||||||
|
- name: Save helper script from base branch
|
||||||
|
run: |
|
||||||
|
git show ${{ github.event.pull_request.base.sha }}:api/libs/pyrefly_type_coverage.py > /tmp/pyrefly_type_coverage.py 2>/dev/null \
|
||||||
|
|| cp api/libs/pyrefly_type_coverage.py /tmp/pyrefly_type_coverage.py
|
||||||
|
|
||||||
|
- name: Checkout base branch
|
||||||
|
run: git checkout ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- name: Run pyrefly report on base branch
|
||||||
|
run: |
|
||||||
|
uv run --directory api --dev pyrefly report 2>/dev/null > /tmp/pyrefly_report_base.tmp && \
|
||||||
|
mv /tmp/pyrefly_report_base.tmp /tmp/pyrefly_report_base.json || \
|
||||||
|
echo '{}' > /tmp/pyrefly_report_base.json
|
||||||
|
|
||||||
|
- name: Generate coverage comparison
|
||||||
|
id: coverage
|
||||||
|
run: |
|
||||||
|
comment_body="$(uv run --directory api python /tmp/pyrefly_type_coverage.py \
|
||||||
|
--base /tmp/pyrefly_report_base.json \
|
||||||
|
< /tmp/pyrefly_report_pr.json)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Pyrefly Type Coverage"
|
||||||
|
echo ""
|
||||||
|
echo "$comment_body"
|
||||||
|
} | tee -a "$GITHUB_STEP_SUMMARY" > /tmp/type_coverage_comment.md
|
||||||
|
|
||||||
|
# Save structured data for the fork-PR comment workflow
|
||||||
|
cp /tmp/pyrefly_report_pr.json pr_report.json
|
||||||
|
cp /tmp/pyrefly_report_base.json base_report.json
|
||||||
|
# Keep fork-PR comments correct while the trusted workflow_run job is
|
||||||
|
# still using the default-branch renderer, which resolves --base from api/.
|
||||||
|
cp /tmp/pyrefly_report_base.json api/base_report.json
|
||||||
|
|
||||||
|
- name: Save PR number
|
||||||
|
run: |
|
||||||
|
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
||||||
|
|
||||||
|
- name: Upload type coverage artifact
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: pyrefly_type_coverage
|
||||||
|
path: |
|
||||||
|
pr_report.json
|
||||||
|
base_report.json
|
||||||
|
api/base_report.json
|
||||||
|
pr_number.txt
|
||||||
|
|
||||||
|
- name: Comment PR with type coverage
|
||||||
|
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const marker = '### Pyrefly Type Coverage';
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = fs.readFileSync('/tmp/type_coverage_comment.md', { encoding: 'utf8' });
|
||||||
|
} catch {
|
||||||
|
body = `${marker}\n\n_Coverage report unavailable._`;
|
||||||
|
}
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
|
||||||
|
// Update existing comment if one exists, otherwise create new
|
||||||
|
const { data: comments } = await github.rest.issues.listComments({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
});
|
||||||
|
const existing = comments.find(c => c.body.startsWith(marker));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
comment_id: existing.id,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,15 +7,22 @@ on:
|
|||||||
- edited
|
- edited
|
||||||
- reopened
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
merge_group:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [checks_requested]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Complete merge group check
|
||||||
|
if: github.event_name == 'merge_group'
|
||||||
|
run: echo "Semantic PR title validation is handled on pull requests."
|
||||||
- name: Check title
|
- name: Check title
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
@@ -23,8 +23,8 @@ jobs:
|
|||||||
days-before-issue-stale: 15
|
days-before-issue-stale: 15
|
||||||
days-before-issue-close: 3
|
days-before-issue-close: 3
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
stale-issue-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
|
stale-issue-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||||
stale-pr-message: "Close due to it's no longer active, if you have any questions, you can reopen it."
|
stale-pr-message: "Closed due to inactivity. If you have any questions, you can reopen it."
|
||||||
stale-issue-label: 'no-issue-activity'
|
stale-issue-label: 'no-issue-activity'
|
||||||
stale-pr-label: 'no-pr-activity'
|
stale-pr-label: 'no-pr-activity'
|
||||||
any-of-labels: 'duplicate,question,invalid,wontfix,no-issue-activity,no-pr-activity,enhancement,cant-reproduce,help-wanted'
|
any-of-labels: '🌚 invalid,🙋♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'
|
||||||
|
|||||||
+73
-29
@@ -15,7 +15,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
python-style:
|
python-style:
|
||||||
name: Python Style
|
name: Python Style
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
api/**
|
api/**
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@@ -47,9 +47,13 @@ jobs:
|
|||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: uv run --directory api --dev lint-imports
|
run: uv run --directory api --dev lint-imports
|
||||||
|
|
||||||
|
- name: Run Response Contract Linter
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: uv run --project api --dev python api/dev/lint_response_contracts.py --fail-on-mismatch
|
||||||
|
|
||||||
- name: Run Type Checks
|
- name: Run Type Checks
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
run: make type-check
|
run: make type-check-core
|
||||||
|
|
||||||
- name: Dotenv check
|
- name: Dotenv check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@@ -57,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
web-style:
|
web-style:
|
||||||
name: Web Style
|
name: Web Style
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
@@ -73,10 +77,62 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
web/**
|
web/**
|
||||||
|
e2e/**
|
||||||
|
sdks/nodejs-client/**
|
||||||
|
packages/**
|
||||||
|
package.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
.nvmrc
|
||||||
|
.github/workflows/style.yml
|
||||||
|
.github/actions/setup-web/**
|
||||||
|
|
||||||
|
- name: Setup web environment
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Web tsslint
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
run: vp run lint:tss
|
||||||
|
|
||||||
|
- name: Web dead code check
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
run: vp run knip
|
||||||
|
|
||||||
|
ts-common-style:
|
||||||
|
name: TS Common
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check changed files
|
||||||
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
web/**
|
||||||
|
cli/**
|
||||||
|
e2e/**
|
||||||
|
sdks/nodejs-client/**
|
||||||
|
packages/**
|
||||||
|
package.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
pnpm-workspace.yaml
|
||||||
|
.nvmrc
|
||||||
|
eslint.config.mjs
|
||||||
.github/workflows/style.yml
|
.github/workflows/style.yml
|
||||||
.github/actions/setup-web/**
|
.github/actions/setup-web/**
|
||||||
|
|
||||||
@@ -87,43 +143,31 @@ jobs:
|
|||||||
- name: Restore ESLint cache
|
- name: Restore ESLint cache
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
id: eslint-cache-restore
|
id: eslint-cache-restore
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: web/.eslintcache
|
path: .eslintcache
|
||||||
key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'web/pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
|
${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
|
||||||
|
|
||||||
- name: Web style check
|
- name: Style check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
|
||||||
run: vp run lint:ci
|
run: vp run lint:ci
|
||||||
|
|
||||||
- name: Web tsslint
|
- name: Type check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
|
||||||
run: vp run lint:tss
|
|
||||||
|
|
||||||
- name: Web type check
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
working-directory: ./web
|
|
||||||
run: vp run type-check
|
run: vp run type-check
|
||||||
|
|
||||||
- name: Web dead code check
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
working-directory: ./web
|
|
||||||
run: vp run knip
|
|
||||||
|
|
||||||
- name: Save ESLint cache
|
- name: Save ESLint cache
|
||||||
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: web/.eslintcache
|
path: .eslintcache
|
||||||
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
||||||
|
|
||||||
superlinter:
|
superlinter:
|
||||||
name: SuperLinter
|
name: SuperLinter
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -134,7 +178,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
**.sh
|
**.sh
|
||||||
@@ -145,7 +189,7 @@ jobs:
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
|
|
||||||
- name: Super-linter
|
- name: Super-linter
|
||||||
uses: super-linter/super-linter/slim@61abc07d755095a68f4987d1c2c3d1d64408f1f9 # v8.5.0
|
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
env:
|
env:
|
||||||
BASH_SEVERITY: warning
|
BASH_SEVERITY: warning
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- sdks/**
|
- sdks/**
|
||||||
|
- package.json
|
||||||
|
- pnpm-lock.yaml
|
||||||
|
- pnpm-workspace.yaml
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: sdk-tests-${{ github.head_ref || github.run_id }}
|
group: sdk-tests-${{ github.head_ref || github.run_id }}
|
||||||
@@ -14,7 +17,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: unit test for Node.js SDK
|
name: unit test for Node.js SDK
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -26,7 +29,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: ''
|
cache: ''
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
name: Translate i18n Files with Claude Code
|
name: Translate i18n Files with Claude Code
|
||||||
|
|
||||||
# Note: claude-code-action doesn't support push events directly.
|
# Note: claude-code-action doesn't support push events directly.
|
||||||
# Push events are handled by trigger-i18n-sync.yml which sends repository_dispatch.
|
# Push events are bridged by trigger-i18n-sync.yml via repository_dispatch.
|
||||||
# See: https://github.com/langgenius/dify/issues/30743
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
repository_dispatch:
|
repository_dispatch:
|
||||||
types: [i18n-sync]
|
types: [i18n-sync]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
files:
|
files:
|
||||||
description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
|
description: 'Specific files to translate (space-separated, e.g., "app common"). Required for full mode; leave empty in incremental mode to use en-US files changed since HEAD~1.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
languages:
|
languages:
|
||||||
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
|
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported target languages except en-US.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
mode:
|
mode:
|
||||||
description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
|
description: 'Sync mode: incremental (compare with previous en-US revision) or full (sync all keys in scope)'
|
||||||
required: false
|
required: false
|
||||||
default: 'incremental'
|
default: incremental
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- incremental
|
- incremental
|
||||||
@@ -30,11 +28,15 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: translate-i18n-${{ github.event_name }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
translate:
|
translate:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
timeout-minutes: 60
|
timeout-minutes: 120
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -51,380 +53,293 @@ jobs:
|
|||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Detect changed files and generate diff
|
- name: Prepare sync context
|
||||||
id: detect_changes
|
id: context
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
DEFAULT_TARGET_LANGS=$(awk "
|
||||||
# Manual trigger
|
/value: '/ {
|
||||||
if [ -n "${{ github.event.inputs.files }}" ]; then
|
value=\$2
|
||||||
echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
|
gsub(/[',]/, \"\", value)
|
||||||
else
|
}
|
||||||
# Get all JSON files in en-US directory
|
/supported: true/ && value != \"en-US\" {
|
||||||
files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
|
printf \"%s \", value
|
||||||
echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
|
}
|
||||||
fi
|
" web/i18n-config/languages.ts | sed 's/[[:space:]]*$//')
|
||||||
echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# For manual trigger with incremental mode, get diff from last commit
|
generate_changes_json() {
|
||||||
# For full mode, we'll do a complete check anyway
|
node .github/scripts/generate-i18n-changes.mjs
|
||||||
if [ "${{ github.event.inputs.mode }}" == "full" ]; then
|
}
|
||||||
echo "Full mode: will check all keys" > /tmp/i18n-diff.txt
|
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
|
||||||
if [ -s /tmp/i18n-diff.txt ]; then
|
|
||||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
|
|
||||||
# Triggered by push via trigger-i18n-sync.yml workflow
|
|
||||||
# Validate required payload fields
|
|
||||||
if [ -z "${{ github.event.client_payload.changed_files }}" ]; then
|
|
||||||
echo "Error: repository_dispatch payload missing required 'changed_files' field" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "CHANGED_FILES=${{ github.event.client_payload.changed_files }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
|
|
||||||
echo "SYNC_MODE=${{ github.event.client_payload.sync_mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Decode the base64-encoded diff from the trigger workflow
|
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
||||||
if [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then
|
BASE_SHA="${{ github.event.client_payload.base_sha }}"
|
||||||
if ! echo "${{ github.event.client_payload.diff_base64 }}" | base64 -d > /tmp/i18n-diff.txt 2>&1; then
|
HEAD_SHA="${{ github.event.client_payload.head_sha }}"
|
||||||
echo "Warning: Failed to decode base64 diff payload" >&2
|
CHANGED_FILES="${{ github.event.client_payload.changed_files }}"
|
||||||
echo "" > /tmp/i18n-diff.txt
|
TARGET_LANGS="$DEFAULT_TARGET_LANGS"
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}"
|
||||||
elif [ -s /tmp/i18n-diff.txt ]; then
|
|
||||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then
|
||||||
else
|
printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.json
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
CHANGES_AVAILABLE="true"
|
||||||
fi
|
CHANGES_SOURCE="embedded"
|
||||||
|
elif [ -n "$BASE_SHA" ] && [ -n "$CHANGED_FILES" ]; then
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
generate_changes_json
|
||||||
|
CHANGES_AVAILABLE="true"
|
||||||
|
CHANGES_SOURCE="recomputed"
|
||||||
else
|
else
|
||||||
echo "" > /tmp/i18n-diff.txt
|
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
||||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
CHANGES_AVAILABLE="false"
|
||||||
|
CHANGES_SOURCE="unavailable"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Unsupported event type: ${{ github.event_name }}"
|
BASE_SHA=""
|
||||||
exit 1
|
HEAD_SHA=$(git rev-parse HEAD)
|
||||||
|
if [ -n "${{ github.event.inputs.languages }}" ]; then
|
||||||
|
TARGET_LANGS="${{ github.event.inputs.languages }}"
|
||||||
|
else
|
||||||
|
TARGET_LANGS="$DEFAULT_TARGET_LANGS"
|
||||||
|
fi
|
||||||
|
SYNC_MODE="${{ github.event.inputs.mode || 'incremental' }}"
|
||||||
|
if [ -n "${{ github.event.inputs.files }}" ]; then
|
||||||
|
CHANGED_FILES="${{ github.event.inputs.files }}"
|
||||||
|
elif [ "$SYNC_MODE" = "incremental" ]; then
|
||||||
|
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
|
||||||
|
if [ -n "$BASE_SHA" ]; then
|
||||||
|
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
else
|
||||||
|
CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
|
fi
|
||||||
|
elif [ "$SYNC_MODE" = "full" ]; then
|
||||||
|
echo "workflow_dispatch full mode requires the files input to stay within CI limits." >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
CHANGED_FILES=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SYNC_MODE" = "incremental" ] && [ -n "$CHANGED_FILES" ]; then
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
generate_changes_json
|
||||||
|
CHANGES_AVAILABLE="true"
|
||||||
|
CHANGES_SOURCE="local"
|
||||||
|
else
|
||||||
|
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
||||||
|
CHANGES_AVAILABLE="false"
|
||||||
|
CHANGES_SOURCE="unavailable"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Truncate diff if too large (keep first 50KB)
|
FILE_ARGS=""
|
||||||
if [ -f /tmp/i18n-diff.txt ]; then
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
FILE_ARGS="--file $CHANGED_FILES"
|
||||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
|
LANG_ARGS=""
|
||||||
|
if [ -n "$TARGET_LANGS" ]; then
|
||||||
|
LANG_ARGS="--lang $TARGET_LANGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "DEFAULT_TARGET_LANGS=$DEFAULT_TARGET_LANGS"
|
||||||
|
echo "BASE_SHA=$BASE_SHA"
|
||||||
|
echo "HEAD_SHA=$HEAD_SHA"
|
||||||
|
echo "CHANGED_FILES=$CHANGED_FILES"
|
||||||
|
echo "TARGET_LANGS=$TARGET_LANGS"
|
||||||
|
echo "SYNC_MODE=$SYNC_MODE"
|
||||||
|
echo "CHANGES_AVAILABLE=$CHANGES_AVAILABLE"
|
||||||
|
echo "CHANGES_SOURCE=$CHANGES_SOURCE"
|
||||||
|
echo "FILE_ARGS=$FILE_ARGS"
|
||||||
|
echo "LANG_ARGS=$LANG_ARGS"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
echo "Files: ${CHANGED_FILES:-<none>}"
|
||||||
|
echo "Languages: ${TARGET_LANGS:-<none>}"
|
||||||
|
echo "Mode: $SYNC_MODE"
|
||||||
|
|
||||||
- name: Run Claude Code for Translation Sync
|
- name: Run Claude Code for Translation Sync
|
||||||
if: steps.detect_changes.outputs.CHANGED_FILES != ''
|
if: steps.context.outputs.CHANGED_FILES != ''
|
||||||
uses: anthropics/claude-code-action@ff9acae5886d41a99ed4ec14b7dc147d55834722 # v1.0.77
|
uses: anthropics/claude-code-action@1dc994ee7a008f0ecc866d9ac23ef036b7229f84 # v1.0.127
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Allow github-actions bot to trigger this workflow via repository_dispatch
|
|
||||||
# See: https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
allowed_bots: 'github-actions[bot]'
|
allowed_bots: 'github-actions[bot]'
|
||||||
|
show_full_output: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
prompt: |
|
prompt: |
|
||||||
You are a professional i18n synchronization engineer for the Dify project.
|
You are the i18n sync agent for the Dify repository.
|
||||||
Your task is to keep all language translations in sync with the English source (en-US).
|
Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`.
|
||||||
|
|
||||||
## CRITICAL TOOL RESTRICTIONS
|
Use absolute paths at all times:
|
||||||
- Use **Read** tool to read files (NOT cat or bash)
|
- Repo root: `${{ github.workspace }}`
|
||||||
- Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
|
- Web directory: `${{ github.workspace }}/web`
|
||||||
- Use **Bash** ONLY for: git commands, gh commands, pnpm commands
|
- Language config: `${{ github.workspace }}/web/i18n-config/languages.ts`
|
||||||
- Run bash commands ONE BY ONE, never combine with && or ||
|
|
||||||
- NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
|
|
||||||
|
|
||||||
## WORKING DIRECTORY & ABSOLUTE PATHS
|
Inputs:
|
||||||
Claude Code sandbox working directory may vary. Always use absolute paths:
|
- Files in scope: `${{ steps.context.outputs.CHANGED_FILES }}`
|
||||||
- For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
|
- Target languages: `${{ steps.context.outputs.TARGET_LANGS }}`
|
||||||
- For git: `git -C ${{ github.workspace }} <command>`
|
- Sync mode: `${{ steps.context.outputs.SYNC_MODE }}`
|
||||||
- For gh: `gh --repo ${{ github.repository }} <command>`
|
- Base SHA: `${{ steps.context.outputs.BASE_SHA }}`
|
||||||
- For file paths: `${{ github.workspace }}/web/i18n/`
|
- Head SHA: `${{ steps.context.outputs.HEAD_SHA }}`
|
||||||
|
- Scoped file args: `${{ steps.context.outputs.FILE_ARGS }}`
|
||||||
|
- Scoped language args: `${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- Structured change set available: `${{ steps.context.outputs.CHANGES_AVAILABLE }}`
|
||||||
|
- Structured change set source: `${{ steps.context.outputs.CHANGES_SOURCE }}`
|
||||||
|
- Structured change set file: `/tmp/i18n-changes.json`
|
||||||
|
|
||||||
## EFFICIENCY RULES
|
Tool rules:
|
||||||
- **ONE Edit per language file** - batch all key additions into a single Edit
|
- Use Read for repository files.
|
||||||
- Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
|
- Use Edit for JSON updates.
|
||||||
- Translate ALL keys for a language mentally first, then do ONE Edit
|
- Use Bash only for `vp`.
|
||||||
|
- Do not use Bash for `git`, `gh`, or branch management.
|
||||||
## Context
|
|
||||||
- Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
|
||||||
- Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }}
|
|
||||||
- Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
|
||||||
- Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json
|
|
||||||
- Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts
|
|
||||||
- Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }}
|
|
||||||
|
|
||||||
## CRITICAL DESIGN: Verify First, Then Sync
|
|
||||||
|
|
||||||
You MUST follow this three-phase approach:
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 1: VERIFY - Analyze and Generate Change Report ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 1.1: Analyze Git Diff (for incremental mode)
|
|
||||||
Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff.
|
|
||||||
|
|
||||||
Parse the diff to categorize changes:
|
|
||||||
- Lines with `+` (not `+++`): Added or modified values
|
|
||||||
- Lines with `-` (not `---`): Removed or old values
|
|
||||||
- Identify specific keys for each category:
|
|
||||||
* ADD: Keys that appear only in `+` lines (new keys)
|
|
||||||
* UPDATE: Keys that appear in both `-` and `+` lines (value changed)
|
|
||||||
* DELETE: Keys that appear only in `-` lines (removed keys)
|
|
||||||
|
|
||||||
### Step 1.2: Read Language Configuration
|
|
||||||
Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`.
|
|
||||||
Extract all languages with `supported: true`.
|
|
||||||
|
|
||||||
### Step 1.3: Run i18n:check for Each Language
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile
|
|
||||||
```
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
|
||||||
```
|
|
||||||
|
|
||||||
This will report:
|
|
||||||
- Missing keys (need to ADD)
|
|
||||||
- Extra keys (need to DELETE)
|
|
||||||
|
|
||||||
### Step 1.4: Generate Change Report
|
|
||||||
|
|
||||||
Create a structured report identifying:
|
|
||||||
```
|
|
||||||
╔══════════════════════════════════════════════════════════════╗
|
|
||||||
║ I18N SYNC CHANGE REPORT ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ Files to process: [list] ║
|
|
||||||
║ Languages to sync: [list] ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ ADD (New Keys): ║
|
|
||||||
║ - [filename].[key]: "English value" ║
|
|
||||||
║ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ UPDATE (Modified Keys - MUST re-translate): ║
|
|
||||||
║ - [filename].[key]: "Old value" → "New value" ║
|
|
||||||
║ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ DELETE (Extra Keys): ║
|
|
||||||
║ - [language]/[filename].[key] ║
|
|
||||||
║ ... ║
|
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANT**: For UPDATE detection, compare git diff to find keys where
|
|
||||||
the English value changed. These MUST be re-translated even if target
|
|
||||||
language already has a translation (it's now stale!).
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 2: SYNC - Execute Changes Based on Report ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 2.1: Process ADD Operations (BATCH per language file)
|
|
||||||
|
|
||||||
**CRITICAL WORKFLOW for efficiency:**
|
|
||||||
1. First, translate ALL new keys for ALL languages mentally
|
|
||||||
2. Then, for EACH language file, do ONE Edit operation:
|
|
||||||
- Read the file once
|
|
||||||
- Insert ALL new keys at the beginning (right after the opening `{`)
|
|
||||||
- Don't worry about alphabetical order - lint:fix will sort them later
|
|
||||||
|
|
||||||
Example Edit (adding 3 keys to zh-Hans/app.json):
|
|
||||||
```
|
|
||||||
old_string: '{\n "accessControl"'
|
|
||||||
new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"'
|
|
||||||
```
|
|
||||||
|
|
||||||
**IMPORTANT**:
|
|
||||||
- ONE Edit per language file (not one Edit per key!)
|
|
||||||
- Always use the Edit tool. NEVER use bash scripts, node, or jq.
|
|
||||||
|
|
||||||
### Step 2.2: Process UPDATE Operations
|
|
||||||
|
|
||||||
**IMPORTANT: Special handling for zh-Hans and ja-JP**
|
|
||||||
If zh-Hans or ja-JP files were ALSO modified in the same push:
|
|
||||||
- Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files
|
|
||||||
- If found, it means someone manually translated them. Apply these rules:
|
|
||||||
|
|
||||||
1. **Missing keys**: Still ADD them (completeness required)
|
|
||||||
2. **Existing translations**: Compare with the NEW English value:
|
|
||||||
- If translation is **completely wrong** or **unrelated** → Update it
|
|
||||||
- If translation is **roughly correct** (captures the meaning) → Keep it, respect manual work
|
|
||||||
- When in doubt, **keep the manual translation**
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- English changed: "Save" → "Save Changes"
|
|
||||||
- Manual translation: "保存更改" → Keep it (correct meaning)
|
|
||||||
- Manual translation: "删除" → Update it (completely wrong)
|
|
||||||
|
|
||||||
For other languages:
|
|
||||||
Use Edit tool to replace the old value with the new translation.
|
|
||||||
You can batch multiple updates in one Edit if they are adjacent.
|
|
||||||
|
|
||||||
### Step 2.3: Process DELETE Operations
|
|
||||||
For extra keys reported by i18n:check:
|
|
||||||
- Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove`
|
|
||||||
- Or manually remove from target language JSON files
|
|
||||||
|
|
||||||
## Translation Guidelines
|
|
||||||
|
|
||||||
- PRESERVE all placeholders exactly as-is:
|
|
||||||
- `{{variable}}` - Mustache interpolation
|
|
||||||
- `${variable}` - Template literal
|
|
||||||
- `<tag>content</tag>` - HTML tags
|
|
||||||
- `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values)
|
|
||||||
|
|
||||||
**CRITICAL: Variable names and tag names MUST stay in English - NEVER translate them**
|
|
||||||
|
|
||||||
✅ CORRECT examples:
|
|
||||||
- English: "{{count}} items" → Japanese: "{{count}} 個のアイテム"
|
|
||||||
- English: "{{name}} updated" → Korean: "{{name}} 업데이트됨"
|
|
||||||
- English: "<email>{{email}}</email>" → Chinese: "<email>{{email}}</email>"
|
|
||||||
- English: "<CustomLink>Marketplace</CustomLink>" → Japanese: "<CustomLink>マーケットプレイス</CustomLink>"
|
|
||||||
|
|
||||||
❌ WRONG examples (NEVER do this - will break the application):
|
|
||||||
- "{{count}}" → "{{カウント}}" ❌ (variable name translated to Japanese)
|
|
||||||
- "{{name}}" → "{{이름}}" ❌ (variable name translated to Korean)
|
|
||||||
- "{{email}}" → "{{邮箱}}" ❌ (variable name translated to Chinese)
|
|
||||||
- "<email>" → "<メール>" ❌ (tag name translated)
|
|
||||||
- "<CustomLink>" → "<自定义链接>" ❌ (component name translated)
|
|
||||||
|
|
||||||
- Use appropriate language register (formal/informal) based on existing translations
|
|
||||||
- Match existing translation style in each language
|
|
||||||
- Technical terms: check existing conventions per language
|
|
||||||
- For CJK languages: no spaces between characters unless necessary
|
|
||||||
- For RTL languages (ar-TN, fa-IR): ensure proper text handling
|
|
||||||
|
|
||||||
## Output Format Requirements
|
|
||||||
- Alphabetical key ordering (if original file uses it)
|
|
||||||
- 2-space indentation
|
|
||||||
- Trailing newline at end of file
|
|
||||||
- Valid JSON (use proper escaping for special characters)
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 3: RE-VERIFY - Confirm All Issues Resolved ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
### Step 3.1: Run Lint Fix (IMPORTANT!)
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json'
|
|
||||||
```
|
|
||||||
This ensures:
|
|
||||||
- JSON keys are sorted alphabetically (jsonc/sort-keys rule)
|
|
||||||
- Valid i18n keys (dify-i18n/valid-i18n-keys rule)
|
|
||||||
- No extra keys (dify-i18n/no-extra-keys rule)
|
|
||||||
|
|
||||||
### Step 3.2: Run Final i18n Check
|
|
||||||
```bash
|
|
||||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3.3: Fix Any Remaining Issues
|
|
||||||
If check reports issues:
|
|
||||||
- Go back to PHASE 2 for unresolved items
|
|
||||||
- Repeat until check passes
|
|
||||||
|
|
||||||
### Step 3.4: Generate Final Summary
|
|
||||||
```
|
|
||||||
╔══════════════════════════════════════════════════════════════╗
|
|
||||||
║ SYNC COMPLETED SUMMARY ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ Language │ Added │ Updated │ Deleted │ Status ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ zh-Hans │ 5 │ 2 │ 1 │ ✓ Complete ║
|
|
||||||
║ ja-JP │ 5 │ 2 │ 1 │ ✓ Complete ║
|
|
||||||
║ ... │ ... │ ... │ ... │ ... ║
|
|
||||||
╠══════════════════════════════════════════════════════════════╣
|
|
||||||
║ i18n:check │ PASSED - All keys in sync ║
|
|
||||||
╚══════════════════════════════════════════════════════════════╝
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mode-Specific Behavior
|
|
||||||
|
|
||||||
**SYNC_MODE = "incremental"** (default):
|
|
||||||
- Focus on keys identified from git diff
|
|
||||||
- Also check i18n:check output for any missing/extra keys
|
|
||||||
- Efficient for small changes
|
|
||||||
|
|
||||||
**SYNC_MODE = "full"**:
|
|
||||||
- Compare ALL keys between en-US and each language
|
|
||||||
- Run i18n:check to identify all discrepancies
|
|
||||||
- Use for first-time sync or fixing historical issues
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
1. Always run i18n:check BEFORE and AFTER making changes
|
|
||||||
2. The check script is the source of truth for missing/extra keys
|
|
||||||
3. For UPDATE scenario: git diff is the source of truth for changed values
|
|
||||||
4. Create a single commit with all translation changes
|
|
||||||
5. If any translation fails, continue with others and report failures
|
|
||||||
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
║ PHASE 4: COMMIT AND CREATE PR ║
|
|
||||||
═══════════════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
After all translations are complete and verified:
|
|
||||||
|
|
||||||
### Step 4.1: Check for changes
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} status --porcelain
|
|
||||||
```
|
|
||||||
|
|
||||||
If there are changes:
|
|
||||||
|
|
||||||
### Step 4.2: Create a new branch and commit
|
|
||||||
Run these git commands ONE BY ONE (not combined with &&).
|
|
||||||
**IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands:
|
|
||||||
|
|
||||||
1. First, get the timestamp:
|
|
||||||
```bash
|
|
||||||
date +%Y%m%d-%H%M%S
|
|
||||||
```
|
|
||||||
(Note the output, e.g., "20260115-143052")
|
|
||||||
|
|
||||||
2. Then create branch using the timestamp value:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052
|
|
||||||
```
|
|
||||||
(Replace "20260115-143052" with the actual timestamp from step 1)
|
|
||||||
|
|
||||||
3. Stage changes:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} add web/i18n/
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Commit:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Push:
|
|
||||||
```bash
|
|
||||||
git -C ${{ github.workspace }} push origin HEAD
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4.3: Create Pull Request
|
|
||||||
```bash
|
|
||||||
gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary
|
|
||||||
|
|
||||||
This PR was automatically generated to sync i18n translation files.
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
|
||||||
- Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- [x] \`i18n:check\` passed
|
|
||||||
- [x] \`lint:fix\` applied
|
|
||||||
|
|
||||||
🤖 Generated with Claude Code GitHub Action" --base main
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Required execution plan:
|
||||||
|
1. Resolve target languages.
|
||||||
|
- Use the provided `Target languages` value as the source of truth.
|
||||||
|
- If it is unexpectedly empty, read `${{ github.workspace }}/web/i18n-config/languages.ts` and use every language with `supported: true` except `en-US`.
|
||||||
|
2. Stay strictly in scope.
|
||||||
|
- Only process the files listed in `Files in scope`.
|
||||||
|
- Only process the resolved target languages, never `en-US`.
|
||||||
|
- Do not touch unrelated i18n files.
|
||||||
|
- Do not modify `${{ github.workspace }}/web/i18n/en-US/`.
|
||||||
|
3. Resolve source changes.
|
||||||
|
- If `Structured change set available` is `true`, read `/tmp/i18n-changes.json` and use it as the source of truth for file-level and key-level changes.
|
||||||
|
- For each file entry:
|
||||||
|
- `added` contains new English keys that need translations.
|
||||||
|
- `updated` contains stale keys whose English source changed; re-translate using the `after` value.
|
||||||
|
- `deleted` contains keys that should be removed from locale files.
|
||||||
|
- `fileDeleted: true` means the English file no longer exists; remove the matching locale file if present.
|
||||||
|
- Read the current English JSON file for any file that still exists so wording, placeholders, and surrounding terminology stay accurate.
|
||||||
|
- If `Structured change set available` is `false`, treat this as a scoped full sync and use the current English files plus scoped checks as the source of truth.
|
||||||
|
4. Run a scoped pre-check before editing:
|
||||||
|
- `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- Use this command as the source of truth for missing and extra keys inside the current scope.
|
||||||
|
5. Apply translations.
|
||||||
|
- For every target language and scoped file:
|
||||||
|
- If `fileDeleted` is `true`, remove the locale file if it exists and skip the rest of that file.
|
||||||
|
- If the locale file does not exist yet, create it with `Write` and then continue with `Edit` as needed.
|
||||||
|
- ADD missing keys.
|
||||||
|
- UPDATE stale translations when the English value changed.
|
||||||
|
- DELETE removed keys. Prefer `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }} --auto-remove` for extra keys so deletions stay in scope.
|
||||||
|
- Preserve placeholders exactly: `{{variable}}`, `${variable}`, HTML tags, component tags, and variable names.
|
||||||
|
- Match the existing terminology and register used by each locale.
|
||||||
|
- Prefer one Edit per file when stable, but prioritize correctness over batching.
|
||||||
|
6. Verify only the edited files.
|
||||||
|
- Run `vp run dify-web#lint:fix --quiet -- <relative edited i18n file paths under web/>`
|
||||||
|
- Run `vp run dify-web#i18n:check ${{ steps.context.outputs.FILE_ARGS }} ${{ steps.context.outputs.LANG_ARGS }}`
|
||||||
|
- If verification fails, fix the remaining problems before continuing.
|
||||||
|
7. Stop after the scoped locale files are updated and verification passes.
|
||||||
|
- Do not create branches, commits, or pull requests.
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--max-turns 150
|
--max-turns 120
|
||||||
--allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep"
|
--allowedTools "Read,Write,Edit,Bash(vp *),Bash(vp:*),Glob,Grep"
|
||||||
|
|
||||||
|
- name: Prepare branch metadata
|
||||||
|
id: pr_meta
|
||||||
|
if: steps.context.outputs.CHANGED_FILES != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git -C "${{ github.workspace }}" status --porcelain -- web/i18n/)" ]; then
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCOPE_HASH=$(printf '%s|%s|%s' "${{ steps.context.outputs.CHANGED_FILES }}" "${{ steps.context.outputs.TARGET_LANGS }}" "${{ steps.context.outputs.SYNC_MODE }}" | sha256sum | cut -c1-8)
|
||||||
|
HEAD_SHORT=$(printf '%s' "${{ steps.context.outputs.HEAD_SHA }}" | cut -c1-12)
|
||||||
|
BRANCH_NAME="chore/i18n-sync-${HEAD_SHORT}-${SCOPE_HASH}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "has_changes=true"
|
||||||
|
echo "branch_name=$BRANCH_NAME"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Commit translation changes
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git -C "${{ github.workspace }}" checkout -B "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
git -C "${{ github.workspace }}" add web/i18n/
|
||||||
|
git -C "${{ github.workspace }}" commit -m "chore(i18n): sync translations with en-US"
|
||||||
|
|
||||||
|
- name: Push translation branch
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if git -C "${{ github.workspace }}" ls-remote --exit-code --heads origin "${{ steps.pr_meta.outputs.branch_name }}" >/dev/null 2>&1; then
|
||||||
|
git -C "${{ github.workspace }}" push --force-with-lease origin "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
else
|
||||||
|
git -C "${{ github.workspace }}" push --set-upstream origin "${{ steps.pr_meta.outputs.branch_name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create or update translation PR
|
||||||
|
if: steps.pr_meta.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BRANCH_NAME: ${{ steps.pr_meta.outputs.branch_name }}
|
||||||
|
FILES_IN_SCOPE: ${{ steps.context.outputs.CHANGED_FILES }}
|
||||||
|
TARGET_LANGS: ${{ steps.context.outputs.TARGET_LANGS }}
|
||||||
|
SYNC_MODE: ${{ steps.context.outputs.SYNC_MODE }}
|
||||||
|
CHANGES_SOURCE: ${{ steps.context.outputs.CHANGES_SOURCE }}
|
||||||
|
BASE_SHA: ${{ steps.context.outputs.BASE_SHA }}
|
||||||
|
HEAD_SHA: ${{ steps.context.outputs.HEAD_SHA }}
|
||||||
|
REPO_NAME: ${{ github.repository }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
PR_BODY_FILE=/tmp/i18n-pr-body.md
|
||||||
|
LANG_COUNT=$(printf '%s\n' "$TARGET_LANGS" | wc -w | tr -d ' ')
|
||||||
|
if [ "$LANG_COUNT" = "0" ]; then
|
||||||
|
LANG_COUNT="0"
|
||||||
|
fi
|
||||||
|
export LANG_COUNT
|
||||||
|
|
||||||
|
node <<'NODE' > "$PR_BODY_FILE"
|
||||||
|
const fs = require('node:fs')
|
||||||
|
|
||||||
|
const changesPath = '/tmp/i18n-changes.json'
|
||||||
|
const changes = fs.existsSync(changesPath)
|
||||||
|
? JSON.parse(fs.readFileSync(changesPath, 'utf8'))
|
||||||
|
: { changes: {} }
|
||||||
|
|
||||||
|
const filesInScope = (process.env.FILES_IN_SCOPE || '').split(/\s+/).filter(Boolean)
|
||||||
|
const lines = [
|
||||||
|
'## Summary',
|
||||||
|
'',
|
||||||
|
`- **Files synced**: \`${process.env.FILES_IN_SCOPE || '<none>'}\``,
|
||||||
|
`- **Languages updated**: ${process.env.TARGET_LANGS || '<none>'} (${process.env.LANG_COUNT} languages)`,
|
||||||
|
`- **Sync mode**: ${process.env.SYNC_MODE}${process.env.BASE_SHA ? ` (base: \`${process.env.BASE_SHA.slice(0, 10)}\`, head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)` : ` (head: \`${process.env.HEAD_SHA.slice(0, 10)}\`)`}`,
|
||||||
|
'',
|
||||||
|
'### Key changes',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const fileName of filesInScope) {
|
||||||
|
const fileChange = changes.changes?.[fileName] || { added: {}, updated: {}, deleted: [], fileDeleted: false }
|
||||||
|
const addedKeys = Object.keys(fileChange.added || {})
|
||||||
|
const updatedKeys = Object.keys(fileChange.updated || {})
|
||||||
|
const deletedKeys = fileChange.deleted || []
|
||||||
|
lines.push(`- \`${fileName}\`: +${addedKeys.length} / ~${updatedKeys.length} / -${deletedKeys.length}${fileChange.fileDeleted ? ' (file deleted in en-US)' : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'## Verification',
|
||||||
|
'',
|
||||||
|
`- \`vp run dify-web#i18n:check --file ${process.env.FILES_IN_SCOPE} --lang ${process.env.TARGET_LANGS}\``,
|
||||||
|
`- \`vp run dify-web#lint:fix --quiet -- <edited i18n files under web/>\``,
|
||||||
|
'',
|
||||||
|
'## Notes',
|
||||||
|
'',
|
||||||
|
'- This PR was generated from structured en-US key changes produced by `trigger-i18n-sync.yml`.',
|
||||||
|
`- Structured change source: ${process.env.CHANGES_SOURCE || 'unknown'}.`,
|
||||||
|
'- Branch name is deterministic for the head SHA and scope, so reruns update the same PR instead of opening duplicates.',
|
||||||
|
'',
|
||||||
|
'🤖 Generated with [Claude Code](https://claude.com/claude-code)'
|
||||||
|
)
|
||||||
|
|
||||||
|
process.stdout.write(lines.join('\n'))
|
||||||
|
NODE
|
||||||
|
|
||||||
|
EXISTING_PR_NUMBER=$(gh pr list --repo "$REPO_NAME" --head "$BRANCH_NAME" --state open --json number --jq '.[0].number')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_PR_NUMBER" ] && [ "$EXISTING_PR_NUMBER" != "null" ]; then
|
||||||
|
gh pr edit "$EXISTING_PR_NUMBER" --repo "$REPO_NAME" --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
|
||||||
|
else
|
||||||
|
gh pr create --repo "$REPO_NAME" --head "$BRANCH_NAME" --base main --title "chore(i18n): sync translations with en-US" --body-file "$PR_BODY_FILE"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
name: Trigger i18n Sync on Push
|
name: Trigger i18n Sync on Push
|
||||||
|
|
||||||
# This workflow bridges the push event to repository_dispatch
|
|
||||||
# because claude-code-action doesn't support push events directly.
|
|
||||||
# See: https://github.com/langgenius/dify/issues/30743
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
@@ -13,10 +9,14 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: trigger-i18n-sync-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
trigger:
|
trigger:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -25,42 +25,66 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Detect changed files and generate diff
|
- name: Detect changed files and build structured change set
|
||||||
id: detect
|
id: detect
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BEFORE_SHA="${{ github.event.before }}"
|
BASE_SHA="${{ github.event.before }}"
|
||||||
# Handle edge case: force push may have null/zero SHA
|
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||||
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
|
||||||
BEFORE_SHA="HEAD~1"
|
|
||||||
fi
|
fi
|
||||||
|
HEAD_SHA="${{ github.sha }}"
|
||||||
|
|
||||||
# Detect changed i18n files
|
if [ -n "$BASE_SHA" ]; then
|
||||||
changed=$(git diff --name-only "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
|
CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'web/i18n/en-US/*.json' 2>/dev/null | sed -n 's@^.*/@@p' | sed 's/\.json$//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
echo "changed_files=$changed" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
# Generate diff for context
|
|
||||||
git diff "$BEFORE_SHA" "${{ github.sha }}" -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
|
||||||
|
|
||||||
# Truncate if too large (keep first 50KB to match receiving workflow)
|
|
||||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
|
||||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
|
||||||
|
|
||||||
# Base64 encode the diff for safe JSON transport (portable, single-line)
|
|
||||||
diff_base64=$(base64 < /tmp/i18n-diff.txt | tr -d '\n')
|
|
||||||
echo "diff_base64=$diff_base64" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
if [ -n "$changed" ]; then
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Detected changed files: $changed"
|
|
||||||
else
|
else
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
CHANGED_FILES=$(find web/i18n/en-US -maxdepth 1 -type f -name '*.json' -print | sed -n 's@^.*/@@p' | sed 's/\.json$//' | sort | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
||||||
echo "No i18n changes detected"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export BASE_SHA HEAD_SHA CHANGED_FILES
|
||||||
|
node .github/scripts/generate-i18n-changes.mjs
|
||||||
|
|
||||||
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
|
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "changed_files=$CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Trigger i18n sync workflow
|
- name: Trigger i18n sync workflow
|
||||||
if: steps.detect.outputs.has_changes == 'true'
|
if: steps.detect.outputs.has_changes == 'true'
|
||||||
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ steps.detect.outputs.base_sha }}
|
||||||
|
HEAD_SHA: ${{ steps.detect.outputs.head_sha }}
|
||||||
|
CHANGED_FILES: ${{ steps.detect.outputs.changed_files }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
event-type: i18n-sync
|
script: |
|
||||||
client-payload: '{"changed_files": "${{ steps.detect.outputs.changed_files }}", "diff_base64": "${{ steps.detect.outputs.diff_base64 }}", "sync_mode": "incremental", "trigger_sha": "${{ github.sha }}"}'
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const changesJson = fs.readFileSync('/tmp/i18n-changes.json', 'utf8')
|
||||||
|
const changesBase64 = Buffer.from(changesJson).toString('base64')
|
||||||
|
const maxEmbeddedChangesChars = 48000
|
||||||
|
const changesEmbedded = changesBase64.length <= maxEmbeddedChangesChars
|
||||||
|
|
||||||
|
if (!changesEmbedded) {
|
||||||
|
console.log(`Structured change set too large to embed safely (${changesBase64.length} chars). Downstream workflow will regenerate it from git history.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.repos.createDispatchEvent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
event_type: 'i18n-sync',
|
||||||
|
client_payload: {
|
||||||
|
changed_files: process.env.CHANGED_FILES,
|
||||||
|
changes_base64: changesEmbedded ? changesBase64 : '',
|
||||||
|
changes_embedded: changesEmbedded,
|
||||||
|
sync_mode: 'incremental',
|
||||||
|
base_sha: process.env.BASE_SHA,
|
||||||
|
head_sha: process.env.HEAD_SHA,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Run Full VDB Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: vdb-tests-full-${{ github.ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Full VDB Tests
|
||||||
|
if: github.repository == 'langgenius/dify'
|
||||||
|
runs-on: depot-ubuntu-24.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version:
|
||||||
|
- "3.12"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Free Disk Space
|
||||||
|
uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3.2.2
|
||||||
|
with:
|
||||||
|
remove_dotnet: true
|
||||||
|
remove_haskell: true
|
||||||
|
remove_tool_cache: true
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Check UV lockfile
|
||||||
|
run: uv lock --project api --check
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
# - name: Set up Vector Store (TiDB)
|
||||||
|
# uses: hoverkraft-tech/[email protected]
|
||||||
|
# with:
|
||||||
|
# compose-file: docker/tidb/docker-compose.yaml
|
||||||
|
# services: |
|
||||||
|
# tidb
|
||||||
|
# tiflash
|
||||||
|
|
||||||
|
# - name: Check VDB Ready (TiDB)
|
||||||
|
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||||
|
|
||||||
|
- name: Test Vector Stores
|
||||||
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
--start-vdb \
|
||||||
|
--vdb-services "weaviate,qdrant,couchbase-server,etcd,minio,milvus-standalone,pgvecto-rs,pgvector,chroma,elasticsearch,oceanbase" \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
|
api/providers/vdb/*/tests/integration_tests
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
name: Run VDB Tests
|
name: Run VDB Smoke Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
group: vdb-tests-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: VDB Tests
|
name: VDB Smoke Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -31,7 +33,7 @@ jobs:
|
|||||||
remove_tool_cache: true
|
remove_tool_cache: true
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@@ -43,14 +45,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: uv sync --project api --dev
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
- name: Set up dotenvs
|
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
# - name: Set up Vector Store (TiDB)
|
# - name: Set up Vector Store (TiDB)
|
||||||
# uses: hoverkraft-tech/[email protected]
|
# uses: hoverkraft-tech/[email protected]
|
||||||
# with:
|
# with:
|
||||||
@@ -59,32 +53,15 @@ jobs:
|
|||||||
# tidb
|
# tidb
|
||||||
# tiflash
|
# tiflash
|
||||||
|
|
||||||
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
|
|
||||||
uses: hoverkraft-tech/compose-action@4894d2492015c1774ee5a13a95b1072093087ec3 # v2.5.0
|
|
||||||
with:
|
|
||||||
compose-file: |
|
|
||||||
docker/docker-compose.yaml
|
|
||||||
services: |
|
|
||||||
weaviate
|
|
||||||
qdrant
|
|
||||||
couchbase-server
|
|
||||||
etcd
|
|
||||||
minio
|
|
||||||
milvus-standalone
|
|
||||||
pgvecto-rs
|
|
||||||
pgvector
|
|
||||||
chroma
|
|
||||||
elasticsearch
|
|
||||||
oceanbase
|
|
||||||
|
|
||||||
- name: setup test config
|
|
||||||
run: |
|
|
||||||
echo $(pwd)
|
|
||||||
ls -lah .
|
|
||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
|
||||||
|
|
||||||
# - name: Check VDB Ready (TiDB)
|
# - name: Check VDB Ready (TiDB)
|
||||||
# run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
|
# run: uv run --project api python api/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
||||||
|
|
||||||
- name: Test Vector Stores
|
- name: Test Vector Stores
|
||||||
run: uv run --project api bash dev/pytest/pytest_vdb.sh
|
run: |
|
||||||
|
uv run --project api pytest \
|
||||||
|
--start-vdb \
|
||||||
|
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||||
|
api/providers/vdb/vdb-chroma/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-pgvector/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-qdrant/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-weaviate/tests/integration_tests
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Web Full-Stack E2E
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: web-e2e-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Web Full-Stack E2E
|
||||||
|
runs-on: depot-ubuntu-24.04-4
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Setup web dependencies
|
||||||
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
|
- name: Setup UV and Python
|
||||||
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
python-version: "3.12"
|
||||||
|
cache-dependency-glob: api/uv.lock
|
||||||
|
|
||||||
|
- name: Install API dependencies
|
||||||
|
run: uv sync --project api --dev
|
||||||
|
|
||||||
|
- name: Install Playwright browser
|
||||||
|
working-directory: ./e2e
|
||||||
|
run: vp run e2e:install
|
||||||
|
|
||||||
|
- name: Run isolated source-api and built-web Cucumber E2E tests
|
||||||
|
working-directory: ./e2e
|
||||||
|
env:
|
||||||
|
E2E_ADMIN_EMAIL: [email protected]
|
||||||
|
E2E_ADMIN_NAME: E2E Admin
|
||||||
|
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||||
|
E2E_FORCE_WEB_BUILD: "1"
|
||||||
|
E2E_INIT_PASSWORD: E2eInit12345
|
||||||
|
run: vp run e2e:full
|
||||||
|
|
||||||
|
- name: Upload Cucumber report
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: cucumber-report
|
||||||
|
path: e2e/cucumber-report
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload E2E logs
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: e2e-logs
|
||||||
|
path: e2e/.logs
|
||||||
|
retention-days: 7
|
||||||
@@ -16,14 +16,14 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
env:
|
env:
|
||||||
VITEST_COVERAGE_SCOPE: app-components
|
VITEST_COVERAGE_SCOPE: app-components
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
shardIndex: [1, 2, 3, 4, 5, 6]
|
shardIndex: [1, 2, 3, 4]
|
||||||
shardTotal: [6]
|
shardTotal: [4]
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -39,11 +39,11 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
|
run: vp test run --reporter=blob --reporter=minimal --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
|
||||||
|
|
||||||
- name: Upload blob report
|
- name: Upload blob report
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: blob-report-${{ matrix.shardIndex }}
|
||||||
path: web/.vitest-reports/*
|
path: web/.vitest-reports/*
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
name: Merge Test Reports
|
name: Merge Test Reports
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
needs: [test]
|
needs: [test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
defaults:
|
defaults:
|
||||||
@@ -66,7 +66,6 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
@@ -84,19 +83,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
if: ${{ env.CODECOV_TOKEN != '' }}
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
directory: web/coverage
|
directory: web/coverage
|
||||||
flags: web
|
flags: web
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
||||||
|
|
||||||
web-build:
|
dify-ui-test:
|
||||||
name: Web Build
|
name: dify-ui Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: depot-ubuntu-24.04-4
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
shell: bash
|
||||||
|
working-directory: ./packages/dify-ui
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -104,20 +106,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
|
||||||
id: changed-files
|
|
||||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
web/**
|
|
||||||
.github/workflows/web-tests.yml
|
|
||||||
.github/actions/setup-web/**
|
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Setup web environment
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
uses: ./.github/actions/setup-web
|
uses: ./.github/actions/setup-web
|
||||||
|
|
||||||
- name: Web build check
|
- name: Install Chromium for Browser Mode
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
run: vp exec playwright install --with-deps chromium
|
||||||
working-directory: ./web
|
|
||||||
run: vp run build
|
- name: Run dify-ui tests
|
||||||
|
run: vp test run --coverage --silent=passed-only
|
||||||
|
|
||||||
|
- name: Report coverage
|
||||||
|
if: ${{ env.CODECOV_TOKEN != '' }}
|
||||||
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
|
with:
|
||||||
|
directory: packages/dify-ui/coverage
|
||||||
|
flags: dify-ui
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
||||||
|
|||||||
+23
-3
@@ -115,6 +115,12 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# cli/ has a src/env/ module (DIFY_* registry) — don't treat it as a venv
|
||||||
|
!/cli/src/env/
|
||||||
|
!/cli/src/commands/env/
|
||||||
|
# cli/scripts/lib/ holds TS build helpers (resolve-buildinfo etc.) — don't treat as Python lib/
|
||||||
|
!/cli/scripts/lib/
|
||||||
.conda/
|
.conda/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
@@ -203,26 +209,30 @@ sdks/python-client/dify_client.egg-info
|
|||||||
|
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/launch.json.template
|
!.vscode/launch.json.template
|
||||||
|
!.vscode/settings.example.json
|
||||||
!.vscode/README.md
|
!.vscode/README.md
|
||||||
api/.vscode
|
api/.vscode
|
||||||
# vscode Code History Extension
|
# vscode Code History Extension
|
||||||
.history
|
.history
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
web/migration/
|
|
||||||
|
|
||||||
# pnpm
|
# pnpm
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
|
node_modules
|
||||||
|
.vite-hooks/_
|
||||||
|
|
||||||
# plugin migrate
|
# plugin migrate
|
||||||
plugins.jsonl
|
plugins.jsonl
|
||||||
|
|
||||||
|
# generated API OpenAPI specs
|
||||||
|
packages/contracts/openapi/
|
||||||
|
|
||||||
# mise
|
# mise
|
||||||
mise.toml
|
mise.toml
|
||||||
|
|
||||||
|
|
||||||
# AI Assistant
|
# AI Assistant
|
||||||
.sisyphus/
|
|
||||||
.roo/
|
.roo/
|
||||||
/.claude/worktrees/
|
/.claude/worktrees/
|
||||||
api/.env.backup
|
api/.env.backup
|
||||||
@@ -236,9 +246,19 @@ scripts/stress-test/reports/
|
|||||||
.playwright-mcp/
|
.playwright-mcp/
|
||||||
.serena/
|
.serena/
|
||||||
|
|
||||||
|
# vitest browser mode attachments (failure screenshots, traces, etc.)
|
||||||
|
.vitest-attachments/
|
||||||
|
**/__screenshots__/
|
||||||
|
|
||||||
# settings
|
# settings
|
||||||
*.local.json
|
*.local.json
|
||||||
*.local.md
|
*.local.md
|
||||||
|
*.local.toml
|
||||||
|
|
||||||
# Code Agent Folder
|
# Code Agent Folder
|
||||||
.qoder/*
|
.qoder/*
|
||||||
|
.context/
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Vitest local reports
|
||||||
|
web/.vitest-reports/
|
||||||
|
|||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# get the list of modified files
|
||||||
|
files=$(git diff --cached --name-only)
|
||||||
|
|
||||||
|
# check if api or web directory is modified
|
||||||
|
|
||||||
|
api_modified=false
|
||||||
|
web_modified=false
|
||||||
|
skip_web_checks=false
|
||||||
|
|
||||||
|
git_path() {
|
||||||
|
git rev-parse --git-path "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -f "$(git_path MERGE_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path CHERRY_PICK_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path REVERT_HEAD)" ] || \
|
||||||
|
[ -f "$(git_path SQUASH_MSG)" ] || \
|
||||||
|
[ -d "$(git_path rebase-merge)" ] || \
|
||||||
|
[ -d "$(git_path rebase-apply)" ]; then
|
||||||
|
skip_web_checks=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
for file in $files
|
||||||
|
do
|
||||||
|
# Use POSIX compliant pattern matching
|
||||||
|
case "$file" in
|
||||||
|
api/*.py)
|
||||||
|
# set api_modified flag to true
|
||||||
|
api_modified=true
|
||||||
|
;;
|
||||||
|
web/*)
|
||||||
|
# set web_modified flag to true
|
||||||
|
web_modified=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# run linters based on the modified modules
|
||||||
|
|
||||||
|
if $api_modified; then
|
||||||
|
echo "Running Ruff linter on api module"
|
||||||
|
|
||||||
|
# run Ruff linter auto-fixing
|
||||||
|
uv run --project api --dev ruff check --fix ./api
|
||||||
|
|
||||||
|
# run Ruff linter checks
|
||||||
|
uv run --project api --dev ruff check ./api || status=$?
|
||||||
|
|
||||||
|
status=${status:-0}
|
||||||
|
|
||||||
|
if [ $status -ne 0 ]; then
|
||||||
|
echo "Ruff linter on api module error, exit code: $status"
|
||||||
|
echo "Please run 'dev/reformat' to fix the fixable linting errors."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $skip_web_checks; then
|
||||||
|
echo "Git operation in progress, skipping web checks"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
vp staged
|
||||||
Vendored
+2
-13
@@ -2,21 +2,10 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: Flask API",
|
"name": "Python: API (gevent)",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "flask",
|
"program": "${workspaceFolder}/api/app.py",
|
||||||
"env": {
|
|
||||||
"FLASK_APP": "app.py",
|
|
||||||
"FLASK_ENV": "development"
|
|
||||||
},
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--host=0.0.0.0",
|
|
||||||
"--port=5001",
|
|
||||||
"--no-debugger",
|
|
||||||
"--no-reload"
|
|
||||||
],
|
|
||||||
"jinja": true,
|
"jinja": true,
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"cwd": "${workspaceFolder}/api",
|
"cwd": "${workspaceFolder}/api",
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
{
|
{
|
||||||
// Disable the default formatter, use eslint instead
|
"cucumber.features": [
|
||||||
"prettier.enable": false,
|
"e2e/features/**/*.feature",
|
||||||
"editor.formatOnSave": false,
|
],
|
||||||
|
"cucumber.glue": [
|
||||||
|
"e2e/features/**/*.ts",
|
||||||
|
],
|
||||||
|
|
||||||
|
"tailwindCSS.experimental.configFile": "web/app/styles/globals.css",
|
||||||
|
|
||||||
// Auto fix
|
// Auto fix
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.organizeImports": "never"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Silent the stylistic rules in your IDE, but still auto fix them
|
// Silent the stylistic rules in your IDE, but still auto fix them
|
||||||
@@ -9,6 +9,7 @@ The codebase is split into:
|
|||||||
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
||||||
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
||||||
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
||||||
|
- **Dify Agent Backend** (`/dify-agent`): Backend services for managing and executing agent
|
||||||
|
|
||||||
## Backend Workflow
|
## Backend Workflow
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ The codebase is split into:
|
|||||||
## Language Style
|
## Language Style
|
||||||
|
|
||||||
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
|
- **Python**: Keep type hints on functions and attributes, and implement relevant special methods (e.g., `__repr__`, `__str__`). Prefer `TypedDict` over `dict` or `Mapping` for type safety and better code documentation.
|
||||||
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check:tsgo`, and avoid `any` types.
|
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check`, and avoid `any` types.
|
||||||
|
|
||||||
## General Practices
|
## General Practices
|
||||||
|
|
||||||
|
|||||||
@@ -97,8 +97,3 @@ Feel free to reach out if you encounter any issues during the setup process.
|
|||||||
## Getting Help
|
## Getting Help
|
||||||
|
|
||||||
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
If you ever get stuck or get a burning question while contributing, simply shoot your queries our way via the related GitHub issue, or hop onto our [Discord](https://discord.gg/8Tpq4AcN9c) for a quick chat.
|
||||||
|
|
||||||
## Automated Agent Contributions
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> If you are an automated agent, just add 🤖🤖🤖 to the end of the PR title to opt-in.
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ DOCKER_REGISTRY=langgenius
|
|||||||
WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
|
WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
|
||||||
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
||||||
VERSION=latest
|
VERSION=latest
|
||||||
|
DOCKER_DIR=docker
|
||||||
|
DOCKER_MIDDLEWARE_ENV=$(DOCKER_DIR)/middleware.env
|
||||||
|
DOCKER_MIDDLEWARE_ENV_EXAMPLE=$(DOCKER_DIR)/envs/middleware.env.example
|
||||||
|
DOCKER_MIDDLEWARE_PROJECT=dify-middlewares-dev
|
||||||
|
|
||||||
# Default target - show help
|
# Default target - show help
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@@ -17,15 +21,20 @@ dev-setup: prepare-docker prepare-web prepare-api
|
|||||||
# Step 1: Prepare Docker middleware
|
# Step 1: Prepare Docker middleware
|
||||||
prepare-docker:
|
prepare-docker:
|
||||||
@echo "🐳 Setting up Docker middleware..."
|
@echo "🐳 Setting up Docker middleware..."
|
||||||
@cp -n docker/middleware.env.example docker/middleware.env 2>/dev/null || echo "Docker middleware.env already exists"
|
@if [ ! -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
||||||
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev up -d
|
cp "$(DOCKER_MIDDLEWARE_ENV_EXAMPLE)" "$(DOCKER_MIDDLEWARE_ENV)"; \
|
||||||
|
echo "Docker middleware.env created"; \
|
||||||
|
else \
|
||||||
|
echo "Docker middleware.env already exists"; \
|
||||||
|
fi
|
||||||
|
@cd $(DOCKER_DIR) && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p $(DOCKER_MIDDLEWARE_PROJECT) up -d
|
||||||
@echo "✅ Docker middleware started"
|
@echo "✅ Docker middleware started"
|
||||||
|
|
||||||
# Step 2: Prepare web environment
|
# Step 2: Prepare web environment
|
||||||
prepare-web:
|
prepare-web:
|
||||||
@echo "🌐 Setting up web environment..."
|
@echo "🌐 Setting up web environment..."
|
||||||
@cp -n web/.env.example web/.env 2>/dev/null || echo "Web .env already exists"
|
@cp -n web/.env.example web/.env.local 2>/dev/null || echo "Web .env.local already exists"
|
||||||
@cd web && pnpm install
|
@pnpm install
|
||||||
@echo "✅ Web environment prepared (not started)"
|
@echo "✅ Web environment prepared (not started)"
|
||||||
|
|
||||||
# Step 3: Prepare API environment
|
# Step 3: Prepare API environment
|
||||||
@@ -39,12 +48,18 @@ prepare-api:
|
|||||||
# Clean dev environment
|
# Clean dev environment
|
||||||
dev-clean:
|
dev-clean:
|
||||||
@echo "⚠️ Stopping Docker containers..."
|
@echo "⚠️ Stopping Docker containers..."
|
||||||
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev down
|
@if [ -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
||||||
|
cd $(DOCKER_DIR) && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p $(DOCKER_MIDDLEWARE_PROJECT) down; \
|
||||||
|
else \
|
||||||
|
echo "Docker middleware.env does not exist, skipping compose down"; \
|
||||||
|
fi
|
||||||
@echo "🗑️ Removing volumes..."
|
@echo "🗑️ Removing volumes..."
|
||||||
@rm -rf docker/volumes/db
|
@rm -rf docker/volumes/db
|
||||||
|
@rm -rf docker/volumes/mysql
|
||||||
@rm -rf docker/volumes/redis
|
@rm -rf docker/volumes/redis
|
||||||
@rm -rf docker/volumes/plugin_daemon
|
@rm -rf docker/volumes/plugin_daemon
|
||||||
@rm -rf docker/volumes/weaviate
|
@rm -rf docker/volumes/weaviate
|
||||||
|
@rm -rf docker/volumes/sandbox/dependencies
|
||||||
@rm -rf api/storage
|
@rm -rf api/storage
|
||||||
@echo "✅ Cleanup complete"
|
@echo "✅ Cleanup complete"
|
||||||
|
|
||||||
@@ -60,34 +75,84 @@ check:
|
|||||||
@echo "✅ Code check complete"
|
@echo "✅ Code check complete"
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "🔧 Running ruff format, check with fixes, import linter, and dotenv-linter..."
|
@echo "🔧 Running ruff format, check with fixes, response contract lint, import linter, and dotenv-linter..."
|
||||||
@uv run --project api --dev ruff format ./api
|
@uv run --project api --dev ruff format ./api
|
||||||
@uv run --project api --dev ruff check --fix ./api
|
@uv run --project api --dev ruff check --fix ./api
|
||||||
|
@$(MAKE) api-contract-lint
|
||||||
@uv run --directory api --dev lint-imports
|
@uv run --directory api --dev lint-imports
|
||||||
@uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
|
@uv run --project api --dev dotenv-linter ./api/.env.example ./web/.env.example
|
||||||
@echo "✅ Linting complete"
|
@echo "✅ Linting complete"
|
||||||
|
|
||||||
|
api-contract-lint:
|
||||||
|
@echo "🔎 Linting Flask response contracts..."
|
||||||
|
@uv run --project api --dev python api/dev/lint_response_contracts.py
|
||||||
|
@echo "✅ Response contract lint complete"
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
@echo "📝 Running type checks (basedpyright + pyrefly + mypy)..."
|
@echo "📝 Running type checks (pyrefly + mypy)..."
|
||||||
@./dev/basedpyright-check $(PATH_TO_CHECK)
|
@./dev/pyrefly-check-local $(PATH_TO_CHECK)
|
||||||
@./dev/pyrefly-check-local
|
@uv --directory api run mypy --exclude-gitignore --exclude '(^|/)conftest\.py$$' --exclude 'tests/' --exclude 'migrations/' --exclude 'dev/generate_swagger_specs.py' --exclude 'dev/generate_fastopenapi_specs.py' --check-untyped-defs --disable-error-code=import-untyped .
|
||||||
@uv --directory api run mypy --exclude-gitignore --exclude 'tests/' --exclude 'migrations/' --check-untyped-defs --disable-error-code=import-untyped .
|
|
||||||
@echo "✅ Type checks complete"
|
@echo "✅ Type checks complete"
|
||||||
|
|
||||||
|
type-check-core:
|
||||||
|
@echo "📝 Running core type checks (pyrefly + mypy)..."
|
||||||
|
@./dev/pyrefly-check-local $(PATH_TO_CHECK)
|
||||||
|
@uv --directory api run mypy --exclude-gitignore --exclude '(^|/)conftest\.py$$' --exclude 'tests/' --exclude 'migrations/' --exclude 'dev/generate_swagger_specs.py' --exclude 'dev/generate_fastopenapi_specs.py' --check-untyped-defs --disable-error-code=import-untyped .
|
||||||
|
@echo "✅ Core type checks complete"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "🧪 Running backend unit tests..."
|
@echo "🧪 Running backend unit tests..."
|
||||||
@if [ -n "$(TARGET_TESTS)" ]; then \
|
@if [ -n "$(TARGET_TESTS)" ]; then \
|
||||||
echo "Target: $(TARGET_TESTS)"; \
|
echo "Target: $(TARGET_TESTS)"; \
|
||||||
uv run --project api --dev pytest $(TARGET_TESTS); \
|
uv run --project api --dev pytest $(TARGET_TESTS); \
|
||||||
else \
|
else \
|
||||||
PYTEST_XDIST_ARGS="-n auto" uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
|
echo "Running backend unit tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --timeout "$${PYTEST_TIMEOUT:-20}" -n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers; \
|
||||||
|
uv run --project api --dev pytest --timeout "$${PYTEST_TIMEOUT:-20}" --cov-append \
|
||||||
|
api/tests/unit_tests/controllers; \
|
||||||
|
fi
|
||||||
|
@echo "✅ Unit tests complete"
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
@echo "🧪 Running full backend test suite..."
|
||||||
|
@if [ -n "$(TARGET_TESTS)" ]; then \
|
||||||
|
echo "Target: $(TARGET_TESTS)"; \
|
||||||
|
uv run --project api --dev pytest $(TARGET_TESTS); \
|
||||||
|
else \
|
||||||
|
echo "Running backend unit tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --timeout "$${PYTEST_TIMEOUT:-20}" -n auto \
|
||||||
|
api/tests/unit_tests \
|
||||||
|
api/providers/vdb/*/tests/unit_tests \
|
||||||
|
api/providers/trace/*/tests/unit_tests \
|
||||||
|
--ignore=api/tests/unit_tests/controllers; \
|
||||||
|
uv run --project api --dev pytest --timeout "$${PYTEST_TIMEOUT:-20}" --cov-append \
|
||||||
|
api/tests/unit_tests/controllers; \
|
||||||
|
echo "Running backend integration tests"; \
|
||||||
|
uv run --project api --dev pytest -p no:benchmark --start-middleware -n auto \
|
||||||
|
--timeout "$${PYTEST_TIMEOUT:-180}" \
|
||||||
|
--cov-append \
|
||||||
|
api/tests/integration_tests/workflow \
|
||||||
|
api/tests/integration_tests/tools \
|
||||||
|
api/tests/test_containers_integration_tests; \
|
||||||
|
echo "Running VDB smoke tests"; \
|
||||||
|
uv run --project api --dev pytest --start-vdb \
|
||||||
|
--timeout "$${PYTEST_TIMEOUT:-180}" \
|
||||||
|
--cov-append \
|
||||||
|
api/providers/vdb/vdb-chroma/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-pgvector/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-qdrant/tests/integration_tests \
|
||||||
|
api/providers/vdb/vdb-weaviate/tests/integration_tests; \
|
||||||
fi
|
fi
|
||||||
@echo "✅ Tests complete"
|
@echo "✅ Tests complete"
|
||||||
|
|
||||||
# Build Docker images
|
# Build Docker images
|
||||||
build-web:
|
build-web:
|
||||||
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
||||||
docker build -t $(WEB_IMAGE):$(VERSION) ./web
|
docker build -f web/Dockerfile -t $(WEB_IMAGE):$(VERSION) .
|
||||||
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
||||||
|
|
||||||
build-api:
|
build-api:
|
||||||
@@ -126,14 +191,17 @@ help:
|
|||||||
@echo " make prepare-docker - Set up Docker middleware"
|
@echo " make prepare-docker - Set up Docker middleware"
|
||||||
@echo " make prepare-web - Set up web environment"
|
@echo " make prepare-web - Set up web environment"
|
||||||
@echo " make prepare-api - Set up API environment"
|
@echo " make prepare-api - Set up API environment"
|
||||||
@echo " make dev-clean - Stop Docker middleware containers"
|
@echo " make dev-clean - Stop Docker middleware containers and remove dev data"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Backend Code Quality:"
|
@echo "Backend Code Quality:"
|
||||||
@echo " make format - Format code with ruff"
|
@echo " make format - Format code with ruff"
|
||||||
@echo " make check - Check code with ruff"
|
@echo " make check - Check code with ruff"
|
||||||
@echo " make lint - Format, fix, and lint code (ruff, imports, dotenv)"
|
@echo " make lint - Format, fix, and lint code (ruff, imports, dotenv)"
|
||||||
@echo " make type-check - Run type checks (basedpyright, pyrefly, mypy)"
|
@echo " make api-contract-lint - Check Flask response docs against returned schemas"
|
||||||
|
@echo " make type-check - Run type checks (pyrefly, mypy)"
|
||||||
|
@echo " make type-check-core - Run core type checks (pyrefly, mypy)"
|
||||||
@echo " make test - Run backend unit tests (or TARGET_TESTS=./api/tests/<target_tests>)"
|
@echo " make test - Run backend unit tests (or TARGET_TESTS=./api/tests/<target_tests>)"
|
||||||
|
@echo " make test-all - Run full backend tests, including Docker-backed suites"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker Build Targets:"
|
@echo "Docker Build Targets:"
|
||||||
@echo " make build-web - Build web Docker image"
|
@echo " make build-web - Build web Docker image"
|
||||||
@@ -143,4 +211,4 @@ help:
|
|||||||
@echo " make build-push-all - Build and push all Docker images"
|
@echo " make build-push-all - Build and push all Docker images"
|
||||||
|
|
||||||
# Phony targets
|
# Phony targets
|
||||||
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help format check lint type-check test
|
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help format check lint api-contract-lint type-check test test-all
|
||||||
|
|||||||
@@ -53,7 +53,11 @@
|
|||||||
<a href="./docs/tr-TR/README.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
<a href="./docs/tr-TR/README.md"><img alt="Türkçe README" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||||
<a href="./docs/vi-VN/README.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
<a href="./docs/vi-VN/README.md"><img alt="README Tiếng Việt" src="https://img.shields.io/badge/Ti%E1%BA%BFng%20Vi%E1%BB%87t-d9d9d9"></a>
|
||||||
<a href="./docs/de-DE/README.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
<a href="./docs/de-DE/README.md"><img alt="README in Deutsch" src="https://img.shields.io/badge/German-d9d9d9"></a>
|
||||||
|
<a href="./docs/it-IT/README.md"><img alt="README in Italiano" src="https://img.shields.io/badge/Italiano-d9d9d9"></a>
|
||||||
|
<a href="./docs/pt-BR/README.md"><img alt="README em Português do Brasil" src="https://img.shields.io/badge/Portugu%C3%AAs%20do%20Brasil-d9d9d9"></a>
|
||||||
|
<a href="./docs/sl-SI/README.md"><img alt="README Slovenščina" src="https://img.shields.io/badge/Sloven%C5%A1%C4%8Dina-d9d9d9"></a>
|
||||||
<a href="./docs/bn-BD/README.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
<a href="./docs/bn-BD/README.md"><img alt="README in বাংলা" src="https://img.shields.io/badge/বাংলা-d9d9d9"></a>
|
||||||
|
<a href="./docs/hi-IN/README.md"><img alt="README in हिन्दी" src="https://img.shields.io/badge/Hindi-d9d9d9"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Dify is an open-source LLM app development platform. Its intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features (including [Opik](https://www.comet.com/docs/opik/integrations/dify), [Langfuse](https://docs.langfuse.com), and [Arize Phoenix](https://docs.arize.com/phoenix)) and more, letting you quickly go from prototype to production. Here's a list of the core features:
|
Dify is an open-source LLM app development platform. Its intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features (including [Opik](https://www.comet.com/docs/opik/integrations/dify), [Langfuse](https://docs.langfuse.com), and [Arize Phoenix](https://docs.arize.com/phoenix)) and more, letting you quickly go from prototype to production. Here's a list of the core features:
|
||||||
@@ -133,20 +137,7 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||
### Custom configurations
|
### Custom configurations
|
||||||
|
|
||||||
If you need to customize the configuration, please refer to the comments in our [.env.example](docker/.env.example) file and update the corresponding values in your `.env` file. Additionally, you might need to make adjustments to the `docker-compose.yaml` file itself, such as changing image versions, port mappings, or volume mounts, based on your specific deployment environment and requirements. After making any changes, please re-run `docker compose up -d`. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
If you need to customize the configuration, edit `docker/.env`. The essential startup defaults live in [`docker/.env.example`](docker/.env.example), and optional advanced variables are split under `docker/envs/` by theme. After making any changes, re-run `docker compose up -d` from the `docker` directory. You can find the full list of available environment variables [here](https://docs.dify.ai/getting-started/install-self-hosted/environments).
|
||||||
|
|
||||||
#### Customizing Suggested Questions
|
|
||||||
|
|
||||||
You can now customize the "Suggested Questions After Answer" feature to better fit your use case. For example, to generate longer, more technical questions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# In your .env file
|
|
||||||
SUGGESTED_QUESTIONS_PROMPT='Please help me predict the five most likely technical follow-up questions a developer would ask. Focus on implementation details, best practices, and architecture considerations. Keep each question between 40-60 characters. Output must be JSON array: ["question1","question2","question3","question4","question5"]'
|
|
||||||
SUGGESTED_QUESTIONS_MAX_TOKENS=512
|
|
||||||
SUGGESTED_QUESTIONS_TEMPERATURE=0.3
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Suggested Questions Configuration Guide](docs/suggested-questions-configuration.md) for detailed examples and usage instructions.
|
|
||||||
|
|
||||||
### Metrics Monitoring with Grafana
|
### Metrics Monitoring with Grafana
|
||||||
|
|
||||||
@@ -156,7 +147,7 @@ Import the dashboard to Grafana, using Dify's PostgreSQL database as data source
|
|||||||
|
|
||||||
### Deployment with Kubernetes
|
### Deployment with Kubernetes
|
||||||
|
|
||||||
If you'd like to configure a highly-available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes.
|
If you'd like to configure a highly available setup, there are community-contributed [Helm Charts](https://helm.sh/) and YAML files which allow Dify to be deployed on Kubernetes.
|
||||||
|
|
||||||
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
- [Helm Chart by @LeoQuote](https://github.com/douban/charts/tree/master/charts/dify)
|
||||||
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
- [Helm Chart by @BorisPolonsky](https://github.com/BorisPolonsky/dify-helm)
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in Dify, please report it privately through GitHub Security Advisories:
|
||||||
|
|
||||||
|
https://github.com/langgenius/dify/security/advisories/new
|
||||||
|
|
||||||
|
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.
|
||||||
|
|
||||||
|
When submitting a report, include as much relevant information as you can safely provide, such as:
|
||||||
|
|
||||||
|
- A description of the vulnerability
|
||||||
|
- Steps to reproduce, if safe to share privately
|
||||||
|
- Affected components, versions, or configurations
|
||||||
|
- Potential impact
|
||||||
|
- Any suggested mitigation or fix, if available
|
||||||
|
|
||||||
|
The maintainers will review reports submitted through GitHub Security Advisories and coordinate follow-up there.
|
||||||
|
|
||||||
|
## Public Disclosure
|
||||||
|
|
||||||
|
Please avoid publicly disclosing details of a vulnerability until it has been reviewed and, where appropriate, a fix or mitigation has been made available.
|
||||||
|
|
||||||
|
## Security Updates
|
||||||
|
|
||||||
|
Security fixes may be released through normal project releases or other appropriate channels. Users are encouraged to keep Dify deployments up to date.
|
||||||
+24
-50
@@ -34,7 +34,7 @@ TRIGGER_URL=http://localhost:5001
|
|||||||
FILES_ACCESS_TIMEOUT=300
|
FILES_ACCESS_TIMEOUT=300
|
||||||
|
|
||||||
# Collaboration mode toggle
|
# Collaboration mode toggle
|
||||||
ENABLE_COLLABORATION_MODE=false
|
ENABLE_COLLABORATION_MODE=true
|
||||||
|
|
||||||
# Access token expiration time in minutes
|
# Access token expiration time in minutes
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
@@ -60,6 +60,9 @@ REDIS_SSL_CERTFILE=
|
|||||||
REDIS_SSL_KEYFILE=
|
REDIS_SSL_KEYFILE=
|
||||||
# Path to client private key file for SSL authentication
|
# Path to client private key file for SSL authentication
|
||||||
REDIS_DB=0
|
REDIS_DB=0
|
||||||
|
# Optional global prefix for Redis keys, topics, streams, and Celery Redis transport artifacts.
|
||||||
|
# Leave empty to preserve current unprefixed behavior.
|
||||||
|
REDIS_KEY_PREFIX=
|
||||||
|
|
||||||
# redis Sentinel configuration.
|
# redis Sentinel configuration.
|
||||||
REDIS_USE_SENTINEL=false
|
REDIS_USE_SENTINEL=false
|
||||||
@@ -74,10 +77,21 @@ REDIS_USE_CLUSTERS=false
|
|||||||
REDIS_CLUSTERS=
|
REDIS_CLUSTERS=
|
||||||
REDIS_CLUSTERS_PASSWORD=
|
REDIS_CLUSTERS_PASSWORD=
|
||||||
|
|
||||||
|
REDIS_RETRY_RETRIES=3
|
||||||
|
REDIS_RETRY_BACKOFF_BASE=1.0
|
||||||
|
REDIS_RETRY_BACKOFF_CAP=10.0
|
||||||
|
REDIS_SOCKET_TIMEOUT=5.0
|
||||||
|
REDIS_SOCKET_CONNECT_TIMEOUT=5.0
|
||||||
|
REDIS_HEALTH_CHECK_INTERVAL=30
|
||||||
|
|
||||||
# celery configuration
|
# celery configuration
|
||||||
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
|
CELERY_BROKER_URL=redis://:difyai123456@localhost:${REDIS_PORT}/1
|
||||||
CELERY_BACKEND=redis
|
CELERY_BACKEND=redis
|
||||||
|
|
||||||
|
# Ops trace retry configuration
|
||||||
|
OPS_TRACE_RETRYABLE_DISPATCH_MAX_RETRIES=60
|
||||||
|
OPS_TRACE_RETRYABLE_DISPATCH_DELAY_SECONDS=5
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
DB_TYPE=postgresql
|
DB_TYPE=postgresql
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
@@ -88,6 +102,8 @@ DB_DATABASE=dify
|
|||||||
|
|
||||||
SQLALCHEMY_POOL_PRE_PING=true
|
SQLALCHEMY_POOL_PRE_PING=true
|
||||||
SQLALCHEMY_POOL_TIMEOUT=30
|
SQLALCHEMY_POOL_TIMEOUT=30
|
||||||
|
# Connection pool reset behavior on return
|
||||||
|
SQLALCHEMY_POOL_RESET_ON_RETURN=rollback
|
||||||
|
|
||||||
# Storage configuration
|
# Storage configuration
|
||||||
# use for store upload files, private keys...
|
# use for store upload files, private keys...
|
||||||
@@ -105,6 +121,7 @@ S3_BUCKET_NAME=your-bucket-name
|
|||||||
S3_ACCESS_KEY=your-access-key
|
S3_ACCESS_KEY=your-access-key
|
||||||
S3_SECRET_KEY=your-secret-key
|
S3_SECRET_KEY=your-secret-key
|
||||||
S3_REGION=your-region
|
S3_REGION=your-region
|
||||||
|
S3_ADDRESS_STYLE=auto
|
||||||
|
|
||||||
# Workflow run and Conversation archive storage (S3-compatible)
|
# Workflow run and Conversation archive storage (S3-compatible)
|
||||||
ARCHIVE_STORAGE_ENABLED=false
|
ARCHIVE_STORAGE_ENABLED=false
|
||||||
@@ -130,7 +147,8 @@ ALIYUN_OSS_AUTH_VERSION=v1
|
|||||||
ALIYUN_OSS_REGION=your-region
|
ALIYUN_OSS_REGION=your-region
|
||||||
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
# Don't start with '/'. OSS doesn't support leading slash in object names.
|
||||||
ALIYUN_OSS_PATH=your-path
|
ALIYUN_OSS_PATH=your-path
|
||||||
ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
# Optional CloudBox ID for Aliyun OSS, DO NOT enable it if you are not using CloudBox.
|
||||||
|
#ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
||||||
|
|
||||||
# Google Storage configuration
|
# Google Storage configuration
|
||||||
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
GOOGLE_STORAGE_BUCKET_NAME=your-bucket-name
|
||||||
@@ -369,7 +387,7 @@ VIKINGDB_ACCESS_KEY=your-ak
|
|||||||
VIKINGDB_SECRET_KEY=your-sk
|
VIKINGDB_SECRET_KEY=your-sk
|
||||||
VIKINGDB_REGION=cn-shanghai
|
VIKINGDB_REGION=cn-shanghai
|
||||||
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
VIKINGDB_HOST=api-vikingdb.xxx.volces.com
|
||||||
VIKINGDB_SCHEMA=http
|
VIKINGDB_SCHEME=http
|
||||||
VIKINGDB_CONNECTION_TIMEOUT=30
|
VIKINGDB_CONNECTION_TIMEOUT=30
|
||||||
VIKINGDB_SOCKET_TIMEOUT=30
|
VIKINGDB_SOCKET_TIMEOUT=30
|
||||||
|
|
||||||
@@ -420,8 +438,6 @@ UPLOAD_FILE_EXTENSION_BLACKLIST=
|
|||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
MULTIMODAL_SEND_FORMAT=base64
|
MULTIMODAL_SEND_FORMAT=base64
|
||||||
PROMPT_GENERATION_MAX_TOKENS=512
|
|
||||||
CODE_GENERATION_MAX_TOKENS=1024
|
|
||||||
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
PLUGIN_BASED_TOKEN_COUNTING_ENABLED=false
|
||||||
|
|
||||||
# Mail configuration, support: resend, smtp, sendgrid
|
# Mail configuration, support: resend, smtp, sendgrid
|
||||||
@@ -541,7 +557,7 @@ MAX_VARIABLE_SIZE=204800
|
|||||||
|
|
||||||
# GraphEngine Worker Pool Configuration
|
# GraphEngine Worker Pool Configuration
|
||||||
# Minimum number of workers per GraphEngine instance (default: 1)
|
# Minimum number of workers per GraphEngine instance (default: 1)
|
||||||
GRAPH_ENGINE_MIN_WORKERS=1
|
GRAPH_ENGINE_MIN_WORKERS=3
|
||||||
# Maximum number of workers per GraphEngine instance (default: 10)
|
# Maximum number of workers per GraphEngine instance (default: 10)
|
||||||
GRAPH_ENGINE_MAX_WORKERS=10
|
GRAPH_ENGINE_MAX_WORKERS=10
|
||||||
# Queue depth threshold that triggers worker scale up (default: 3)
|
# Queue depth threshold that triggers worker scale up (default: 3)
|
||||||
@@ -641,6 +657,7 @@ PLUGIN_REMOTE_INSTALL_PORT=5003
|
|||||||
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
PLUGIN_REMOTE_INSTALL_HOST=localhost
|
||||||
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
PLUGIN_MAX_PACKAGE_SIZE=15728640
|
||||||
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
PLUGIN_MODEL_SCHEMA_CACHE_TTL=3600
|
||||||
|
PLUGIN_MODEL_PROVIDERS_CACHE_TTL=86400
|
||||||
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
INNER_API_KEY_FOR_PLUGIN=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||||
|
|
||||||
# Marketplace configuration
|
# Marketplace configuration
|
||||||
@@ -702,22 +719,6 @@ SWAGGER_UI_PATH=/swagger-ui.html
|
|||||||
# Set to false to export dataset IDs as plain text for easier cross-environment import
|
# Set to false to export dataset IDs as plain text for easier cross-environment import
|
||||||
DSL_EXPORT_ENCRYPT_DATASET_ID=true
|
DSL_EXPORT_ENCRYPT_DATASET_ID=true
|
||||||
|
|
||||||
# Suggested Questions After Answer Configuration
|
|
||||||
# These environment variables allow customization of the suggested questions feature
|
|
||||||
#
|
|
||||||
# Custom prompt for generating suggested questions (optional)
|
|
||||||
# If not set, uses the default prompt that generates 3 questions under 20 characters each
|
|
||||||
# Example: "Please help me predict the five most likely technical follow-up questions a developer would ask. Focus on implementation details, best practices, and architecture considerations. Keep each question between 40-60 characters. Output must be JSON array: [\"question1\",\"question2\",\"question3\",\"question4\",\"question5\"]"
|
|
||||||
# SUGGESTED_QUESTIONS_PROMPT=
|
|
||||||
|
|
||||||
# Maximum number of tokens for suggested questions generation (default: 256)
|
|
||||||
# Adjust this value for longer questions or more questions
|
|
||||||
# SUGGESTED_QUESTIONS_MAX_TOKENS=256
|
|
||||||
|
|
||||||
# Temperature for suggested questions generation (default: 0.0)
|
|
||||||
# Higher values (0.5-1.0) produce more creative questions, lower values (0.0-0.3) produce more focused questions
|
|
||||||
# SUGGESTED_QUESTIONS_TEMPERATURE=0
|
|
||||||
|
|
||||||
# Tenant isolated task queue configuration
|
# Tenant isolated task queue configuration
|
||||||
TENANT_ISOLATED_TASK_CONCURRENCY=1
|
TENANT_ISOLATED_TASK_CONCURRENCY=1
|
||||||
|
|
||||||
@@ -747,34 +748,6 @@ SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_MAX_INTERVAL=200
|
|||||||
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30
|
||||||
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
|
SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000
|
||||||
|
|
||||||
# Sandbox Dify CLI configuration
|
|
||||||
# Directory containing dify CLI binaries (dify-cli-<os>-<arch>). Defaults to api/bin when unset.
|
|
||||||
SANDBOX_DIFY_CLI_ROOT=
|
|
||||||
|
|
||||||
# CLI API URL for sandbox (dify-sandbox or e2b) to call back to Dify API.
|
|
||||||
# This URL must be accessible from the sandbox environment.
|
|
||||||
# For local development: use http://localhost:5001 or http://127.0.0.1:5001
|
|
||||||
# For middleware docker stack (api on host): keep localhost/127.0.0.1 and use agentbox via 127.0.0.1:2222
|
|
||||||
# For Docker deployment: use http://api:5001 (internal Docker network)
|
|
||||||
# For external sandbox (e.g., e2b): use a publicly accessible URL
|
|
||||||
CLI_API_URL=http://localhost:5001
|
|
||||||
|
|
||||||
# Base URL for storage file ticket API endpoints (upload/download).
|
|
||||||
# Used by sandbox containers (internal or external like e2b) that need an absolute,
|
|
||||||
# routable address to reach the Dify API file endpoints.
|
|
||||||
# Required for sandbox runtime file access.
|
|
||||||
# For local development: http://localhost:5001
|
|
||||||
# For all-in-one Docker deployment with nginx: http://localhost
|
|
||||||
# For public/remote sandbox environments (e.g., e2b): use a public domain or IP
|
|
||||||
FILES_API_URL=http://localhost:5001
|
|
||||||
|
|
||||||
# Optional defaults for SSH sandbox provider setup (for manual config/CLI usage).
|
|
||||||
# Middleware/local dev usually uses 127.0.0.1:2222; full docker deployment usually uses agentbox:22.
|
|
||||||
SSH_SANDBOX_HOST=127.0.0.1
|
|
||||||
SSH_SANDBOX_PORT=2222
|
|
||||||
SSH_SANDBOX_USERNAME=agentbox
|
|
||||||
SSH_SANDBOX_PASSWORD=agentbox
|
|
||||||
SSH_SANDBOX_BASE_WORKING_PATH=/workspace/sandboxes
|
|
||||||
|
|
||||||
# Redis URL used for event bus between API and
|
# Redis URL used for event bus between API and
|
||||||
# celery worker
|
# celery worker
|
||||||
@@ -795,6 +768,7 @@ EVENT_BUS_REDIS_CHANNEL_TYPE=pubsub
|
|||||||
# Whether to use Redis cluster mode while use redis as event bus.
|
# Whether to use Redis cluster mode while use redis as event bus.
|
||||||
# It's highly recommended to enable this for large deployments.
|
# It's highly recommended to enable this for large deployments.
|
||||||
EVENT_BUS_REDIS_USE_CLUSTERS=false
|
EVENT_BUS_REDIS_USE_CLUSTERS=false
|
||||||
|
EVENT_BUS_LISTENER_JOIN_TIMEOUT_MS=2000
|
||||||
|
|
||||||
# Whether to Enable human input timeout check task
|
# Whether to Enable human input timeout check task
|
||||||
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=true
|
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=true
|
||||||
|
|||||||
+4
-232
@@ -1,242 +1,14 @@
|
|||||||
[importlinter]
|
[importlinter]
|
||||||
root_packages =
|
root_packages =
|
||||||
core
|
core
|
||||||
dify_graph
|
constants
|
||||||
|
context
|
||||||
configs
|
configs
|
||||||
controllers
|
controllers
|
||||||
extensions
|
extensions
|
||||||
|
factories
|
||||||
|
libs
|
||||||
models
|
models
|
||||||
tasks
|
tasks
|
||||||
services
|
services
|
||||||
include_external_packages = True
|
include_external_packages = True
|
||||||
|
|
||||||
[importlinter:contract:workflow]
|
|
||||||
name = Workflow
|
|
||||||
type=layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
graph_events
|
|
||||||
graph
|
|
||||||
nodes
|
|
||||||
node_events
|
|
||||||
runtime
|
|
||||||
entities
|
|
||||||
containers =
|
|
||||||
dify_graph
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.base.node -> dify_graph.graph_events
|
|
||||||
dify_graph.nodes.iteration.iteration_node -> dify_graph.graph_events
|
|
||||||
dify_graph.nodes.loop.loop_node -> dify_graph.graph_events
|
|
||||||
|
|
||||||
dify_graph.nodes.iteration.iteration_node -> dify_graph.graph_engine
|
|
||||||
dify_graph.nodes.loop.loop_node -> dify_graph.graph_engine
|
|
||||||
# TODO(QuantumGhost): fix the import violation later
|
|
||||||
dify_graph.entities.pause_reason -> dify_graph.nodes.human_input.entities
|
|
||||||
|
|
||||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
|
||||||
dify_graph.file.file_manager -> models.model
|
|
||||||
dify_graph.file.file_manager -> models.tools
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
|
|
||||||
[importlinter:contract:workflow-infrastructure-dependencies]
|
|
||||||
name = Workflow Infrastructure Dependencies
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph
|
|
||||||
forbidden_modules =
|
|
||||||
extensions.ext_database
|
|
||||||
extensions.ext_redis
|
|
||||||
allow_indirect_imports = True
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.llm.node -> extensions.ext_database
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
|
||||||
|
|
||||||
[importlinter:contract:workflow-external-imports]
|
|
||||||
name = Workflow External Imports
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph
|
|
||||||
forbidden_modules =
|
|
||||||
configs
|
|
||||||
controllers
|
|
||||||
extensions
|
|
||||||
models
|
|
||||||
services
|
|
||||||
tasks
|
|
||||||
core.agent
|
|
||||||
core.app
|
|
||||||
core.base
|
|
||||||
core.callback_handler
|
|
||||||
core.datasource
|
|
||||||
core.db
|
|
||||||
core.entities
|
|
||||||
core.errors
|
|
||||||
core.extension
|
|
||||||
core.external_data_tool
|
|
||||||
core.file
|
|
||||||
core.helper
|
|
||||||
core.hosting_configuration
|
|
||||||
core.indexing_runner
|
|
||||||
core.llm_generator
|
|
||||||
core.logging
|
|
||||||
core.mcp
|
|
||||||
core.memory
|
|
||||||
core.moderation
|
|
||||||
core.ops
|
|
||||||
core.plugin
|
|
||||||
core.prompt
|
|
||||||
core.provider_manager
|
|
||||||
core.rag
|
|
||||||
core.repositories
|
|
||||||
core.schemas
|
|
||||||
core.tools
|
|
||||||
core.trigger
|
|
||||||
core.variables
|
|
||||||
ignore_imports =
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.protocols -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.llm_utils -> dify_graph.model_runtime.model_providers.__base.large_language_model
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.signature
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.callback_handler.workflow_tool_callback_handler
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.tool_engine
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.tool_manager
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.advanced_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.simple_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> dify_graph.model_runtime.model_providers.__base.large_language_model
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.prompt.simple_prompt_transform
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.model_manager
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.model_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.utils.message_transformer
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.errors
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.file_ref
|
|
||||||
dify_graph.nodes.llm.node -> core.llm_generator.output_parser.structured_output
|
|
||||||
dify_graph.nodes.llm.node -> core.model_manager
|
|
||||||
dify_graph.nodes.llm.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.parameter_extractor.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.question_classifier.entities -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.question_classifier.question_classifier_node -> core.prompt.utils.prompt_message_util
|
|
||||||
dify_graph.nodes.llm.node -> models.dataset
|
|
||||||
dify_graph.nodes.llm.file_saver -> core.tools.signature
|
|
||||||
dify_graph.nodes.llm.file_saver -> core.tools.tool_file_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.tools.errors
|
|
||||||
dify_graph.nodes.llm.node -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.node -> models.model
|
|
||||||
dify_graph.nodes.llm.node -> configs
|
|
||||||
dify_graph.nodes.llm.node -> core.agent.entities
|
|
||||||
dify_graph.nodes.llm.node -> core.agent.patterns
|
|
||||||
dify_graph.nodes.llm.node -> core.app.entities.app_invoke_entities
|
|
||||||
dify_graph.nodes.llm.node -> core.helper.code_executor
|
|
||||||
dify_graph.nodes.llm.node -> core.memory.base
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox.bash.session
|
|
||||||
dify_graph.nodes.llm.node -> core.sandbox.entities.config
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.assembler
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.constants
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_bundle
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_document
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.skill_metadata
|
|
||||||
dify_graph.nodes.llm.node -> core.skill.entities.tool_dependencies
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.tool_file_manager
|
|
||||||
dify_graph.nodes.llm.node -> core.tools.tool_manager
|
|
||||||
dify_graph.nodes.tool.tool_node -> services
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.__base.large_language_model -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.__base.text_embedding_model -> core.entities.embedding_type
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> configs
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
|
||||||
dify_graph.model_runtime.model_providers.model_provider_factory -> models.provider_ids
|
|
||||||
dify_graph.file.file_manager -> configs
|
|
||||||
dify_graph.file.file_manager -> extensions.ext_database
|
|
||||||
dify_graph.file.file_manager -> models.model
|
|
||||||
dify_graph.file.file_manager -> models.tools
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.model_access
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.app.llm.quota
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.memory
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.memory.base
|
|
||||||
dify_graph.nodes.llm.llm_utils -> extensions.ext_database
|
|
||||||
dify_graph.nodes.llm.llm_utils -> models.model
|
|
||||||
dify_graph.nodes.llm.llm_utils -> core.prompt.entities.advanced_prompt_entities
|
|
||||||
dify_graph.nodes.llm.entities -> core.agent.entities
|
|
||||||
dify_graph.nodes.base.node -> core.workflow.node_factory
|
|
||||||
dify_graph.nodes.tool.tool_node -> core.workflow.node_factory
|
|
||||||
|
|
||||||
[importlinter:contract:rsc]
|
|
||||||
name = RSC
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
response_coordinator
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:worker]
|
|
||||||
name = Worker
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
worker
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:graph-engine-architecture]
|
|
||||||
name = Graph Engine Architecture
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
graph_engine
|
|
||||||
orchestration
|
|
||||||
command_processing
|
|
||||||
event_management
|
|
||||||
error_handler
|
|
||||||
graph_traversal
|
|
||||||
graph_state_manager
|
|
||||||
worker_management
|
|
||||||
domain
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine
|
|
||||||
|
|
||||||
[importlinter:contract:domain-isolation]
|
|
||||||
name = Domain Model Isolation
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph.graph_engine.domain
|
|
||||||
forbidden_modules =
|
|
||||||
dify_graph.graph_engine.worker_management
|
|
||||||
dify_graph.graph_engine.command_channels
|
|
||||||
dify_graph.graph_engine.layers
|
|
||||||
dify_graph.graph_engine.protocols
|
|
||||||
|
|
||||||
[importlinter:contract:worker-management]
|
|
||||||
name = Worker Management
|
|
||||||
type = forbidden
|
|
||||||
source_modules =
|
|
||||||
dify_graph.graph_engine.worker_management
|
|
||||||
forbidden_modules =
|
|
||||||
dify_graph.graph_engine.orchestration
|
|
||||||
dify_graph.graph_engine.command_processing
|
|
||||||
dify_graph.graph_engine.event_management
|
|
||||||
|
|
||||||
|
|
||||||
[importlinter:contract:graph-traversal-components]
|
|
||||||
name = Graph Traversal Components
|
|
||||||
type = layers
|
|
||||||
layers =
|
|
||||||
edge_processor
|
|
||||||
skip_propagator
|
|
||||||
containers =
|
|
||||||
dify_graph.graph_engine.graph_traversal
|
|
||||||
|
|
||||||
[importlinter:contract:command-channels]
|
|
||||||
name = Command Channels Independence
|
|
||||||
type = independence
|
|
||||||
modules =
|
|
||||||
dify_graph.graph_engine.command_channels.in_memory_channel
|
|
||||||
dify_graph.graph_engine.command_channels.redis_channel
|
|
||||||
|
|||||||
+3
-22
@@ -69,8 +69,6 @@ ignore = [
|
|||||||
"FURB152", # math-constant
|
"FURB152", # math-constant
|
||||||
"UP007", # non-pep604-annotation
|
"UP007", # non-pep604-annotation
|
||||||
"UP032", # f-string
|
"UP032", # f-string
|
||||||
"UP045", # non-pep604-annotation-optional
|
|
||||||
"B005", # strip-with-multi-characters
|
|
||||||
"B006", # mutable-argument-default
|
"B006", # mutable-argument-default
|
||||||
"B007", # unused-loop-control-variable
|
"B007", # unused-loop-control-variable
|
||||||
"B026", # star-arg-unpacking-after-keyword-arg
|
"B026", # star-arg-unpacking-after-keyword-arg
|
||||||
@@ -84,7 +82,6 @@ ignore = [
|
|||||||
"SIM102", # collapsible-if
|
"SIM102", # collapsible-if
|
||||||
"SIM103", # needless-bool
|
"SIM103", # needless-bool
|
||||||
"SIM105", # suppressible-exception
|
"SIM105", # suppressible-exception
|
||||||
"SIM107", # return-in-try-except-finally
|
|
||||||
"SIM108", # if-else-block-instead-of-if-exp
|
"SIM108", # if-else-block-instead-of-if-exp
|
||||||
"SIM113", # enumerate-for-loop
|
"SIM113", # enumerate-for-loop
|
||||||
"SIM117", # multiple-with-statements
|
"SIM117", # multiple-with-statements
|
||||||
@@ -93,38 +90,22 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[lint.per-file-ignores]
|
[lint.per-file-ignores]
|
||||||
"__init__.py" = [
|
|
||||||
"F401", # unused-import
|
|
||||||
"F811", # redefined-while-unused
|
|
||||||
]
|
|
||||||
"configs/*" = [
|
"configs/*" = [
|
||||||
"N802", # invalid-function-name
|
"N802", # invalid-function-name
|
||||||
]
|
]
|
||||||
"dify_graph/model_runtime/callbacks/base_callback.py" = ["T201"]
|
|
||||||
"core/workflow/callbacks/workflow_logging_callback.py" = ["T201"]
|
|
||||||
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
"libs/gmpy2_pkcs10aep_cipher.py" = [
|
||||||
"N803", # invalid-argument-name
|
"N803", # invalid-argument-name
|
||||||
]
|
]
|
||||||
"tests/*" = [
|
"tests/*" = [
|
||||||
"F811", # redefined-while-unused
|
|
||||||
"T201", # allow print in tests,
|
"T201", # allow print in tests,
|
||||||
"S110", # allow ignoring exceptions in tests code (currently)
|
"S110", # allow ignoring exceptions in tests code (currently)
|
||||||
|
|
||||||
]
|
]
|
||||||
"controllers/console/explore/trial.py" = ["TID251"]
|
|
||||||
"controllers/console/human_input_form.py" = ["TID251"]
|
|
||||||
"controllers/web/human_input_form.py" = ["TID251"]
|
|
||||||
|
|
||||||
[lint.pyflakes]
|
|
||||||
allowed-unused-imports = [
|
|
||||||
"tests.integration_tests",
|
|
||||||
"tests.unit_tests",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.flake8-tidy-imports]
|
|
||||||
|
|
||||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse"]
|
||||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||||
|
|
||||||
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
[lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"]
|
||||||
msg = "Use Pydantic payload/query models instead of reqparse."
|
msg = "Use Pydantic payload/query models instead of reqparse."
|
||||||
|
|
||||||
|
[lint.isort]
|
||||||
|
known-first-party = ["graphon"]
|
||||||
Vendored
+5
-13
@@ -3,29 +3,21 @@
|
|||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Launch Flask and Celery",
|
"name": "Launch Flask and Celery",
|
||||||
"configurations": ["Python: Flask", "Python: Celery"]
|
"configurations": ["Python: API (gevent)", "Python: Celery"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: Flask",
|
"name": "Python: API (gevent)",
|
||||||
"consoleName": "Flask",
|
"consoleName": "API",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"python": "${workspaceFolder}/.venv/bin/python",
|
"python": "${workspaceFolder}/.venv/bin/python",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"envFile": ".env",
|
"envFile": ".env",
|
||||||
"module": "flask",
|
"program": "${workspaceFolder}/app.py",
|
||||||
"justMyCode": true,
|
"justMyCode": true,
|
||||||
"jinja": true,
|
"jinja": true
|
||||||
"env": {
|
|
||||||
"FLASK_APP": "app.py",
|
|
||||||
"GEVENT_SUPPORT": "True"
|
|
||||||
},
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"--port=5001"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python: Celery",
|
"name": "Python: Celery",
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ Quick checks while iterating:
|
|||||||
- Format: `make format`
|
- Format: `make format`
|
||||||
- Lint (includes auto-fix): `make lint`
|
- Lint (includes auto-fix): `make lint`
|
||||||
- Type check: `make type-check`
|
- Type check: `make type-check`
|
||||||
|
- Unit tests: `make test`
|
||||||
|
- Full backend tests, including Docker-backed suites: `make test-all`
|
||||||
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
|
- Targeted tests: `make test TARGET_TESTS=./api/tests/<target_tests>`
|
||||||
|
|
||||||
Before opening a PR / submitting:
|
Before opening a PR / submitting:
|
||||||
@@ -193,6 +195,11 @@ Before opening a PR / submitting:
|
|||||||
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
- Controllers: parse input via Pydantic, invoke services, return serialised responses; no business logic.
|
||||||
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
- Services: coordinate repositories, providers, background tasks; keep side effects explicit.
|
||||||
- Document non-obvious behaviour with concise docstrings and comments.
|
- Document non-obvious behaviour with concise docstrings and comments.
|
||||||
|
- For `204 No Content` responses, return an empty body only; never return a dict, model, or other payload.
|
||||||
|
- For Flask-RESTX controller request, query, and response schemas, follow `controllers/API_SCHEMA_GUIDE.md`.
|
||||||
|
In short: use Pydantic models, document GET query params with `query_params_from_model(...)`, register response
|
||||||
|
DTOs with `register_response_schema_models(...)`, serialize response DTOs with `dump_response(...)`,
|
||||||
|
and avoid adding new legacy `ns.model(...)`, `@marshal_with(...)`, or GET `@ns.expect(...)` patterns.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
|
|||||||
+10
-6
@@ -17,13 +17,17 @@ FROM base AS packages
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
# basic environment
|
# basic environment
|
||||||
g++ \
|
git g++ \
|
||||||
# for building gmpy2
|
# for building gmpy2
|
||||||
libmpfr-dev libmpc-dev
|
libmpfr-dev libmpc-dev
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies (workspace members under providers/vdb/)
|
||||||
COPY pyproject.toml uv.lock ./
|
COPY api/pyproject.toml api/uv.lock ./
|
||||||
RUN uv sync --locked --no-dev
|
COPY api/providers ./providers
|
||||||
|
COPY dify-agent/pyproject.toml dify-agent/README.md /app/dify-agent/
|
||||||
|
COPY dify-agent/src /app/dify-agent/src
|
||||||
|
# Trust the checked-in lock during image builds; local path sources are copied from the repository context.
|
||||||
|
RUN uv sync --frozen --no-dev --no-editable
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
@@ -106,10 +110,10 @@ RUN python -c "import tiktoken; tiktoken.encoding_for_model('gpt2')" \
|
|||||||
&& chown -R dify:dify ${TIKTOKEN_CACHE_DIR}
|
&& chown -R dify:dify ${TIKTOKEN_CACHE_DIR}
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY --chown=dify:dify . /app/api/
|
COPY --chown=dify:dify api /app/api/
|
||||||
|
|
||||||
# Prepare entrypoint script
|
# Prepare entrypoint script
|
||||||
COPY --chown=dify:dify --chmod=755 docker/entrypoint.sh /entrypoint.sh
|
COPY --chown=dify:dify --chmod=755 api/docker/entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
ARG COMMIT_SHA
|
ARG COMMIT_SHA
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
*
|
||||||
|
|
||||||
|
!api/
|
||||||
|
!api/**
|
||||||
|
!dify-agent/
|
||||||
|
!dify-agent/pyproject.toml
|
||||||
|
!dify-agent/README.md
|
||||||
|
!dify-agent/src/
|
||||||
|
!dify-agent/src/**
|
||||||
|
|
||||||
|
api/.venv
|
||||||
|
api/.venv/**
|
||||||
|
api/.env
|
||||||
|
api/*.env.*
|
||||||
|
api/.idea
|
||||||
|
api/.mypy_cache
|
||||||
|
api/.ruff_cache
|
||||||
|
api/storage/generate_files/*
|
||||||
|
api/storage/privkeys/*
|
||||||
|
api/storage/tools/*
|
||||||
|
api/storage/upload_files/*
|
||||||
|
api/logs
|
||||||
|
api/*.log*
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
+11
-81
@@ -40,6 +40,8 @@ The scripts resolve paths relative to their location, so you can run them from a
|
|||||||
./dev/start-web
|
./dev/start-web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`./dev/setup` and `./dev/start-web` install JavaScript dependencies through the repository root workspace, so you do not need a separate `cd web && pnpm install` step.
|
||||||
|
|
||||||
1. Set up your application by visiting `http://localhost:3000`.
|
1. Set up your application by visiting `http://localhost:3000`.
|
||||||
|
|
||||||
1. Start the worker service (async and scheduler tasks, runs from `api`).
|
1. Start the worker service (async and scheduler tasks, runs from `api`).
|
||||||
@@ -54,86 +56,6 @@ The scripts resolve paths relative to their location, so you can run them from a
|
|||||||
./dev/start-beat
|
./dev/start-beat
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual commands
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Show manual setup and run steps</summary>
|
|
||||||
|
|
||||||
These commands assume you start from the repository root.
|
|
||||||
|
|
||||||
1. Start the docker-compose stack.
|
|
||||||
|
|
||||||
The backend requires middleware, including PostgreSQL, Redis, and Weaviate, which can be started together using `docker-compose`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp docker/middleware.env.example docker/middleware.env
|
|
||||||
# Use mysql or another vector database profile if you are not using postgres/weaviate.
|
|
||||||
docker compose -f docker/docker-compose.middleware.yaml --profile postgresql --profile weaviate -p dify up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Copy env files.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp api/.env.example api/.env
|
|
||||||
cp web/.env.example web/.env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install UV if needed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install uv
|
|
||||||
# Or on macOS
|
|
||||||
brew install uv
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install API dependencies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv sync --group dev
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Install web dependencies.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
pnpm install
|
|
||||||
cd ..
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Start backend (runs migrations first, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run flask db upgrade
|
|
||||||
uv run flask run --host 0.0.0.0 --port=5001 --debug
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Start Dify [web](../web) service (in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
pnpm dev:inspect
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Set up your application by visiting `http://localhost:3000`.
|
|
||||||
|
|
||||||
1. Optional: start the worker service (async tasks, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run celery -A app.celery worker -P threads -c 2 --loglevel INFO -Q api_token,dataset,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Optional: start Celery Beat (scheduled tasks, in a new terminal).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd api
|
|
||||||
uv run celery -A app.celery beat
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Environment notes
|
### Environment notes
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
@@ -177,5 +99,13 @@ These commands assume you start from the repository root.
|
|||||||
./dev/reformat # Run all formatters and linters
|
./dev/reformat # Run all formatters and linters
|
||||||
uv run ruff check --fix ./ # Fix linting issues
|
uv run ruff check --fix ./ # Fix linting issues
|
||||||
uv run ruff format ./ # Format code
|
uv run ruff format ./ # Format code
|
||||||
uv run basedpyright . # Type checking
|
uv run pyrefly check # Type checking
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Generate TS stub
|
||||||
|
|
||||||
|
```
|
||||||
|
uv run dev/generate_swagger_specs.py --output-dir openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
use https://jsontotable.org/openapi-to-typescript to convert to typescript
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Application configuration definitions, including file access settings.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- File access settings drive signed URL expiration and base URLs.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Config parsing tests under tests/unit_tests/configs.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Registers file-related API namespaces and routes for files service.
|
|
||||||
- Includes app-assets and sandbox archive proxy controllers.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- files_ns must include all file controller modules to register routes.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Coverage via controller unit tests and route registration smoke checks.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- App assets download proxy endpoint (signed URL verification, stream from storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates AssetPath fields (UUIDs, asset_type allowlist).
|
|
||||||
- Verifies tenant-scoped signature and expiration before reading storage.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing files return NotFound.
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Verify signature validation and invalid/expired cases.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- App assets upload proxy endpoint (signed URL verification, upload to storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates AssetPath fields (UUIDs, asset_type allowlist).
|
|
||||||
- Verifies tenant-scoped signature and expiration before writing storage.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Verify signature validation and invalid/expired cases.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox archive upload/download proxy endpoints (signed URL verification, stream to storage).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Validates tenant_id and sandbox_id UUIDs.
|
|
||||||
- Verifies tenant-scoped signature and expiration before storage access.
|
|
||||||
- URL uses expires_at/nonce/sign query params.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing archive returns NotFound.
|
|
||||||
- Invalid signature or expired link returns Forbidden.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Add unit tests for signature validation if needed.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Collects file assets and emits FileAsset entries with storage keys.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Storage keys are derived via AppAssetStorage for draft files.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by asset build pipeline tests.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Builds skill artifacts from markdown assets and uploads resolved outputs.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Reads draft asset content via AppAssetStorage refs.
|
|
||||||
- Writes resolved artifacts via AppAssetStorage refs.
|
|
||||||
- FileAsset storage keys are derived via AppAssetStorage.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing or invalid JSON content yields empty skill content/metadata.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Build pipeline unit tests covering compile/upload paths.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Converts AppAssetFileTree to FileAsset items for packaging.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Storage keys for assets are derived via AppAssetStorage.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Used in packaging/service tests for asset bundles.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Zip Packager Notes
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
- Builds a ZIP archive of asset contents stored via the configured storage backend.
|
|
||||||
|
|
||||||
## Key Decisions
|
|
||||||
- Packaging writes assets into an in-memory zip buffer returned as bytes.
|
|
||||||
- Asset fetch + zip writing are executed via a thread pool with a lock guarding `ZipFile` writes.
|
|
||||||
|
|
||||||
## Edge Cases
|
|
||||||
- ZIP writes are serialized by the lock; storage reads still run in parallel.
|
|
||||||
|
|
||||||
## Tests/Verification
|
|
||||||
- None yet.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Builds AssetItem entries for asset trees using AssetPath-derived storage keys.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AssetPath to compute draft storage keys.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by asset parsing and packaging tests.
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Defines AssetPath facade + typed asset path classes for app-asset storage access.
|
|
||||||
- Maps asset paths to storage keys and generates presigned or signed-proxy URLs.
|
|
||||||
- Signs proxy URLs using tenant private keys and enforces expiration.
|
|
||||||
- Exposes app_asset_storage singleton for reuse.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- AssetPathBase fields (tenant_id/app_id/resource_id/node_id) must be UUIDs.
|
|
||||||
- AssetPath.from_components enforces valid types and resolved node_id presence.
|
|
||||||
- Storage keys are derived internally via AssetPathBase.get_storage_key; callers never supply raw paths.
|
|
||||||
- AppAssetStorage.storage returns the cached presign wrapper (not the raw storage).
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Storage backends without presign support must fall back to signed proxy URLs.
|
|
||||||
- Signed proxy verification enforces expiration and tenant-scoped signing keys.
|
|
||||||
- Upload URLs also fall back to signed proxy endpoints when presign is unsupported.
|
|
||||||
- load_or_none treats SilentStorage "File Not Found" bytes as missing.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Unit tests for ref validation, storage key mapping, and signed URL verification.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Extracts asset files from a zip and persists them into app asset storage.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Rejects path traversal/absolute/backslash paths.
|
|
||||||
- Saves extracted files via AppAssetStorage draft refs.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Zip security edge cases and tree construction tests.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Downloads published app asset zip into sandbox and extracts it.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AppAssetStorage to generate download URLs for build zips (internal URL).
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Sandbox initialization integration tests.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
Summary:
|
|
||||||
Summary:
|
|
||||||
- Downloads draft/resolved assets into sandbox for draft execution.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- Uses AppAssetStorage to generate download URLs for draft/resolved refs (internal URL).
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- No nodes -> returns early.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Sandbox draft initialization tests.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox lifecycle wrapper (ready/cancel/fail signals, mount/unmount, release).
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- wait_ready raises with the original initialization error as the cause.
|
|
||||||
- release always attempts unmount and environment release, logging failures.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Covered by sandbox lifecycle/unit tests and workflow execution error handling.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Sandbox security helper modules.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
Summary:
|
|
||||||
- Generates and verifies signed URLs for sandbox archive upload/download.
|
|
||||||
|
|
||||||
Invariants:
|
|
||||||
- tenant_id and sandbox_id must be UUIDs.
|
|
||||||
- Signatures are tenant-scoped and include operation, expiry, and nonce.
|
|
||||||
|
|
||||||
Edge Cases:
|
|
||||||
- Missing tenant private key raises ValueError.
|
|
||||||
- Expired or tampered signatures are rejected.
|
|
||||||
|
|
||||||
Tests:
|
|
||||||
- Add unit tests if sandbox archive signature behavior expands.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user