mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Compare commits
1526 Commits
main
...
1.14.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd8385abf1 | ||
|
|
5cfb3d70e1 | ||
|
|
292bfceae6 | ||
|
|
4b6f1861ef | ||
|
|
5432bba0ca | ||
|
|
6415a0b799 | ||
|
|
e2ce12f7a9 | ||
|
|
65e733dadd | ||
|
|
72aaa82a5d | ||
|
|
e059fe81b9 | ||
|
|
2d8b6dff7d | ||
|
|
cc1085ebd4 | ||
|
|
ada3a6d4a2 | ||
|
|
047f499f95 | ||
|
|
e730124740 | ||
|
|
4ff200b327 | ||
|
|
7ce3070301 | ||
|
|
15dff1d59f | ||
|
|
9e0f906fac | ||
|
|
6fd368da06 | ||
|
|
39804035f8 | ||
|
|
a7ab84106e | ||
|
|
0893c8531d | ||
|
|
086f6a2bb3 | ||
|
|
49b115b1ea | ||
|
|
e21c9cb4a6 | ||
|
|
87d09b2e3d | ||
|
|
eac9cbdfb9 | ||
|
|
c0bba03b13 | ||
|
|
e45d4d8d90 | ||
|
|
5e9f3eab8b | ||
|
|
40078108de | ||
|
|
0def4dc204 | ||
|
|
535436ea1d | ||
|
|
c0b47bd318 | ||
|
|
d953982536 | ||
|
|
50c8b699a8 | ||
|
|
aa92f0dd32 | ||
|
|
c02a96a916 | ||
|
|
a71d13523d | ||
|
|
17ba8af331 | ||
|
|
2bf767d5f7 | ||
|
|
98d2eb6579 | ||
|
|
e16a3ef396 | ||
|
|
26ace0da95 | ||
|
|
ccf27adaa3 | ||
|
|
29d6d030f8 | ||
|
|
f01f7a0d32 | ||
|
|
9185c05050 | ||
|
|
dbc575cac0 | ||
|
|
a5662c947c | ||
|
|
25c4df27ba | ||
|
|
c2fb3604de | ||
|
|
c3163840ec | ||
|
|
a3162c745a | ||
|
|
d3b59abeb2 | ||
|
|
29406dbc46 | ||
|
|
d64d796815 | ||
|
|
439d931e9f | ||
|
|
5157a33aff | ||
|
|
655f61a187 | ||
|
|
aca1ac5714 | ||
|
|
2da770cdbd | ||
|
|
a5271baea0 | ||
|
|
4b1af0d525 | ||
|
|
56fd571acc | ||
|
|
d876fddab0 | ||
|
|
8d0e646912 | ||
|
|
aba1752e6c | ||
|
|
cbcb5a71c3 | ||
|
|
2ab0b334a8 | ||
|
|
3a8bbd10cd | ||
|
|
acbcca0322 | ||
|
|
ce8c484ab8 | ||
|
|
c980f1b2ac | ||
|
|
32fcbcdc62 | ||
|
|
33dda01788 | ||
|
|
102310f5b9 | ||
|
|
a1cd9316bb | ||
|
|
9c0b01ef20 | ||
|
|
ee2df33e73 | ||
|
|
e86802ad27 | ||
|
|
fb679962a3 | ||
|
|
adf450c712 | ||
|
|
573e099f6c | ||
|
|
ae59074c59 | ||
|
|
868423a138 | ||
|
|
b906188cf9 | ||
|
|
4d439028e5 | ||
|
|
34bff10782 | ||
|
|
1af4e5dda2 | ||
|
|
1502121125 | ||
|
|
93734d2c9a | ||
|
|
b820c7d1cb | ||
|
|
3ced895f4c | ||
|
|
78acba7a35 | ||
|
|
d8765c8c16 | ||
|
|
f6b0fda9f7 | ||
|
|
f359bbc5de | ||
|
|
7067b5f3cb | ||
|
|
d47bc3abc4 | ||
|
|
fff2c11d9c | ||
|
|
001950d9f8 | ||
|
|
41b218f427 | ||
|
|
a71f336ee0 | ||
|
|
cce7970f77 | ||
|
|
9614fe8e6e | ||
|
|
5eeb6c56f0 | ||
|
|
fcf2a334d2 | ||
|
|
f80b5a9537 | ||
|
|
defa99e6cd | ||
|
|
29a6dd5648 | ||
|
|
92e58aa624 | ||
|
|
9e10b73b54 | ||
|
|
db0c527ce8 | ||
|
|
ef2fabc158 | ||
|
|
64e338ece5 | ||
|
|
91649c9bfd | ||
|
|
8a057ac242 | ||
|
|
83c260ee4c | ||
|
|
ff7b62f2de | ||
|
|
363802aa66 | ||
|
|
3a1eefa477 | ||
|
|
3c0b50ee77 | ||
|
|
b014e91740 | ||
|
|
8f6a8997f4 | ||
|
|
63d965bc44 | ||
|
|
a303560b98 | ||
|
|
6f50915d2b | ||
|
|
bc9ca4e0dd | ||
|
|
ca243d7efc | ||
|
|
b289e6a2b6 | ||
|
|
2d6b30f3b8 | ||
|
|
f6b036b121 | ||
|
|
0868f941f6 | ||
|
|
9fba10036e | ||
|
|
6e750814b3 | ||
|
|
404ac3aeab | ||
|
|
7d2e630fc8 | ||
|
|
68f7f2f19b | ||
|
|
e528112394 | ||
|
|
e9bff0b7b7 | ||
|
|
d23a94982d | ||
|
|
724700acc4 | ||
|
|
2b848d7e93 | ||
|
|
a761ab5cee | ||
|
|
f5a29b69a8 | ||
|
|
11d5efc13e | ||
|
|
e10996c368 | ||
|
|
865b221ce6 | ||
|
|
bb9ae66f81 | ||
|
|
c5439a3739 | ||
|
|
776fb04bf0 | ||
|
|
dae2e3b6fb | ||
|
|
971bfa4758 | ||
|
|
3bc574234f | ||
|
|
81715426d2 | ||
|
|
c61129590d | ||
|
|
fef42a05ee | ||
|
|
287c1bbc35 | ||
|
|
d3b32645f4 | ||
|
|
3bfa495795 | ||
|
|
dc213ca76c | ||
|
|
f1100b82f9 | ||
|
|
ad3a5ad473 | ||
|
|
c5d1b2a02e | ||
|
|
768bfa8a7e | ||
|
|
871ec3b0ca | ||
|
|
44fd58853c | ||
|
|
4d1d83b509 | ||
|
|
f0ba739e44 | ||
|
|
799d0c0d0b | ||
|
|
92c3656fe5 | ||
|
|
ecbcd5803b | ||
|
|
30981dfa7c | ||
|
|
3eba0c561e | ||
|
|
a0984a779f | ||
|
|
6ac9bbfd5f | ||
|
|
c9c826d0d2 | ||
|
|
cb7c086377 | ||
|
|
306ef79526 | ||
|
|
06c31dfdb2 | ||
|
|
7161c3dd80 | ||
|
|
94c354e36d | ||
|
|
083154e57b | ||
|
|
7446779198 | ||
|
|
8235ad9e62 | ||
|
|
b60f9c7703 | ||
|
|
23f7f188bd | ||
|
|
9893bf267e | ||
|
|
7dcb0897c4 | ||
|
|
6913d5b88c | ||
|
|
9e08f5827b | ||
|
|
befefb04b4 | ||
|
|
733c8a0d76 | ||
|
|
7f018a3e7f | ||
|
|
3c214f762a | ||
|
|
ce3d2b581b | ||
|
|
882ad92c24 | ||
|
|
561d8d301f | ||
|
|
e92b9afd4e | ||
|
|
71f15741b0 | ||
|
|
d690b97568 | ||
|
|
469fda8327 | ||
|
|
a750d87ae4 | ||
|
|
6b0e6b2785 | ||
|
|
eb87512122 | ||
|
|
aad15a0777 | ||
|
|
740fafc926 | ||
|
|
61cfbd1c8d | ||
|
|
b2df9ba9d3 | ||
|
|
6840c7e37f | ||
|
|
21a723fb26 | ||
|
|
a2380c4fd3 | ||
|
|
52b34b1fdb | ||
|
|
5fdcedcbed | ||
|
|
0618b2532f | ||
|
|
47fffedd2e | ||
|
|
15c0011897 | ||
|
|
10fb482351 | ||
|
|
9bd714623e | ||
|
|
e0082dbf18 | ||
|
|
25065a4f2f | ||
|
|
8a9e0e3b31 | ||
|
|
49de78a20b | ||
|
|
23f98652e1 | ||
|
|
2df0d540a9 | ||
|
|
625163705b | ||
|
|
d0200e90d2 | ||
|
|
8850a0c5c7 | ||
|
|
ecf4c06ed7 | ||
|
|
b96459b656 | ||
|
|
4466688e97 | ||
|
|
d84aaff825 | ||
|
|
dee3e026a6 | ||
|
|
75d2148ef6 | ||
|
|
00e9dce3ad | ||
|
|
60a5d5c67c | ||
|
|
8c6139fa21 | ||
|
|
c111079624 | ||
|
|
d056b23168 | ||
|
|
36bfb08f34 | ||
|
|
7535b67423 | ||
|
|
9172997f24 | ||
|
|
f1d099d50d | ||
|
|
b6b2af45a7 | ||
|
|
c9b0d90ca7 | ||
|
|
70c887ed5c | ||
|
|
178421a8ac | ||
|
|
0fcddfe9a5 | ||
|
|
19a5aee38e | ||
|
|
31177462e1 | ||
|
|
10f5d9e7ad | ||
|
|
49befa6d3f | ||
|
|
5441b9c3ad | ||
|
|
beba89cc0a | ||
|
|
808a32c457 | ||
|
|
6ea16837ff | ||
|
|
987ea0f764 | ||
|
|
303e1e3eb0 | ||
|
|
85b9661b82 | ||
|
|
5e808f6f31 | ||
|
|
f1c15e0a17 | ||
|
|
e2913d9ee1 | ||
|
|
806ece9a67 | ||
|
|
2833965815 | ||
|
|
3ca767de47 | ||
|
|
726fc1851f | ||
|
|
b66db183c9 | ||
|
|
b6465327c1 | ||
|
|
03774a7bd0 | ||
|
|
4d82769baa | ||
|
|
d353feb172 | ||
|
|
db56fe546b | ||
|
|
f76f4252e0 | ||
|
|
be96e6032e | ||
|
|
9ad49340bf | ||
|
|
5572de1d3c | ||
|
|
078e2d7150 | ||
|
|
52d0716159 | ||
|
|
473262d70e | ||
|
|
f880ef0052 | ||
|
|
51ffab8a1a | ||
|
|
a87560d667 | ||
|
|
7be3c4c7b4 | ||
|
|
3bdc16ac5f | ||
|
|
03ec2f64cd | ||
|
|
a052c414ac | ||
|
|
d0d553ba38 | ||
|
|
b67d0d8c45 | ||
|
|
bb4dd85ae3 | ||
|
|
bc1d3bdf57 | ||
|
|
618dde1e3d | ||
|
|
9d5db4993d | ||
|
|
ea88bcfbd2 | ||
|
|
ea91f96924 | ||
|
|
73b78c9edc | ||
|
|
617b64bb93 | ||
|
|
0265cc0403 | ||
|
|
c1de8f75ca | ||
|
|
304d8e5fe7 | ||
|
|
dbc32af932 | ||
|
|
40b58dfcde | ||
|
|
3f5f893e6c | ||
|
|
25ee3f7bc4 | ||
|
|
a4b0e4a2a0 | ||
|
|
0f6f46b1f0 | ||
|
|
03c8387830 | ||
|
|
142b72f435 | ||
|
|
4338632a78 | ||
|
|
5c0023b788 | ||
|
|
e9608532bd | ||
|
|
60b4b10622 | ||
|
|
abe2b37e3a | ||
|
|
c33d27938d | ||
|
|
32329cf27b | ||
|
|
038b03fa8e | ||
|
|
acc8671c28 | ||
|
|
66b4fa102b | ||
|
|
f5b84384cf | ||
|
|
bf2e3d5151 | ||
|
|
ed9efba039 | ||
|
|
2a46bf26b5 | ||
|
|
fb97bcfdc7 | ||
|
|
0711af20f2 | ||
|
|
e9c2279b80 | ||
|
|
561f383cbc | ||
|
|
d2a60b3b94 | ||
|
|
27664ec37a | ||
|
|
e8f45a9685 | ||
|
|
2e63e20131 | ||
|
|
c577e2a5ec | ||
|
|
cb4391f705 | ||
|
|
0035587fed | ||
|
|
171551cfb3 | ||
|
|
ac9985321e | ||
|
|
cdb1449a96 | ||
|
|
2008768cb9 | ||
|
|
22b0a08a5f | ||
|
|
ec9ade62f5 | ||
|
|
7926024569 | ||
|
|
869b43a95b | ||
|
|
4a7f6597c4 | ||
|
|
206d56358d | ||
|
|
d3fc457331 | ||
|
|
cb12ada689 | ||
|
|
806016244f | ||
|
|
ff478b6fef | ||
|
|
e9c9f0d7cc | ||
|
|
c8a0a2c00d | ||
|
|
5d8ba8f8cc | ||
|
|
03f1197d77 | ||
|
|
464b92da32 | ||
|
|
1a51f52061 | ||
|
|
edce6d4152 | ||
|
|
749cebe60d | ||
|
|
6be800e14f | ||
|
|
20a4a83129 | ||
|
|
f52fb919d1 | ||
|
|
4aea4071a8 | ||
|
|
f198540357 | ||
|
|
919d7ef5cd | ||
|
|
a886bfef8a | ||
|
|
f1321765c6 | ||
|
|
6ee9078349 | ||
|
|
ff71816373 | ||
|
|
9b62be2eb1 | ||
|
|
8f7b9e2de4 | ||
|
|
e47f690cd2 | ||
|
|
92731bffba | ||
|
|
9d99675a1d | ||
|
|
fb034a1324 | ||
|
|
5c91311077 | ||
|
|
807d0e5fba | ||
|
|
76484406a2 | ||
|
|
079484d21c | ||
|
|
72c712b3bb | ||
|
|
b93e21f99f | ||
|
|
8a5983e071 | ||
|
|
9fa42a65e1 | ||
|
|
459d9b5842 | ||
|
|
2973968cc6 | ||
|
|
255b7511ae | ||
|
|
37f35bced2 | ||
|
|
8185d146b6 | ||
|
|
a5ace48f96 | ||
|
|
d73a36d6bc | ||
|
|
3ad05be9ca | ||
|
|
bacc5c32f5 | ||
|
|
efb3657cfe | ||
|
|
bd4b76db5c | ||
|
|
8a96f9f8df | ||
|
|
90bb7bf2f3 | ||
|
|
26dd6c128c | ||
|
|
0495dc5085 | ||
|
|
23ee9e618b | ||
|
|
8326b9e3e5 | ||
|
|
999587fbdd | ||
|
|
f16516549e | ||
|
|
2df4445aa7 | ||
|
|
d63a012680 | ||
|
|
2aa6dcaa1a | ||
|
|
144089d3ed | ||
|
|
3dad0b38df | ||
|
|
c600fdafcd | ||
|
|
3af927556e | ||
|
|
0c1e812d21 | ||
|
|
e95241b94f | ||
|
|
2513e191fb | ||
|
|
f9f3d33911 | ||
|
|
acec271e88 | ||
|
|
76c4d7f62c | ||
|
|
fb78a4450d | ||
|
|
209e4f8f7b | ||
|
|
636156f5da | ||
|
|
7408405c91 | ||
|
|
135fc45ae9 | ||
|
|
a7890c140e | ||
|
|
bd80cd217d | ||
|
|
ceea593010 | ||
|
|
3c7f641f60 | ||
|
|
000bdf6bc0 | ||
|
|
4c77b5f5c5 | ||
|
|
7cf54238c3 | ||
|
|
f00d9186e4 | ||
|
|
807697c664 | ||
|
|
116e075b94 | ||
|
|
a784121070 | ||
|
|
543802cc65 | ||
|
|
156b779a1d | ||
|
|
bdb923984f | ||
|
|
680b238a36 | ||
|
|
c88acf48bf | ||
|
|
0ae02938e6 | ||
|
|
ca95b6684f | ||
|
|
b7fc738cfb | ||
|
|
d07ba03a2a | ||
|
|
a0526143e2 | ||
|
|
190453d397 | ||
|
|
20d19fead9 | ||
|
|
27639600f9 | ||
|
|
0b6522df42 | ||
|
|
40a8e8febc | ||
|
|
392cec2f54 | ||
|
|
27781d6b7e | ||
|
|
ef6f7f2a6c | ||
|
|
d10d3b7021 | ||
|
|
a38b8987b4 | ||
|
|
0d9de79fae | ||
|
|
cd688a0d8f | ||
|
|
a571b3abb2 | ||
|
|
9d287647c1 | ||
|
|
403114eee9 | ||
|
|
efbd92fc7f | ||
|
|
71d44ec52c | ||
|
|
c46856d5ac | ||
|
|
ffca687f4e | ||
|
|
fd078f8853 | ||
|
|
af543d2a7f | ||
|
|
015befad43 | ||
|
|
ae9c7d4e9f | ||
|
|
daa6413353 | ||
|
|
5b485d7cd0 | ||
|
|
39ec2b3277 | ||
|
|
bf66627204 | ||
|
|
506163ab2d | ||
|
|
951af125af | ||
|
|
a4a85f7168 | ||
|
|
8174b67e24 | ||
|
|
ae23d30da2 | ||
|
|
adf104becd | ||
|
|
17807dbf18 | ||
|
|
5d41f67fe1 | ||
|
|
ab52550abe | ||
|
|
85ecf1a198 | ||
|
|
046aff93f6 | ||
|
|
57a588265a | ||
|
|
2fb391a642 | ||
|
|
c9e428facf | ||
|
|
1beafd8558 | ||
|
|
504280995d | ||
|
|
b889ab8853 | ||
|
|
394a811e5e | ||
|
|
d098e72c13 | ||
|
|
a29f569e08 | ||
|
|
64b6a5dd31 | ||
|
|
9094f9d313 | ||
|
|
89eb7b17db | ||
|
|
a9e1394011 | ||
|
|
61608e0423 | ||
|
|
1ea7d2d9a1 | ||
|
|
53bc060cea | ||
|
|
74f94633d7 | ||
|
|
a6a1ac4fa6 | ||
|
|
c5ccdcc331 | ||
|
|
f0f796fdc0 | ||
|
|
c4e5eba6c3 | ||
|
|
585e11a1fc | ||
|
|
54fce5e903 | ||
|
|
772dbe620d | ||
|
|
8332f0de2b | ||
|
|
b7f1eb9b7b | ||
|
|
a0188bd9b5 | ||
|
|
6b439b1699 | ||
|
|
bf12445960 | ||
|
|
b57b1a6926 | ||
|
|
a9fb73fa31 | ||
|
|
a9c5201485 | ||
|
|
87d033e186 | ||
|
|
55120ea9f7 | ||
|
|
b99311baa0 | ||
|
|
ec5964c419 | ||
|
|
5ac70633a2 | ||
|
|
c9edd71395 | ||
|
|
2e954388f5 | ||
|
|
2c02c8ac18 | ||
|
|
5cdd69e7e0 | ||
|
|
e5e43bc2b9 | ||
|
|
70d88bc522 | ||
|
|
765b548be4 | ||
|
|
830c286062 | ||
|
|
f90f3287d8 | ||
|
|
d396d92059 | ||
|
|
166b4a5a2b | ||
|
|
694ed4f5e3 | ||
|
|
e39711f9ea | ||
|
|
154018fe31 | ||
|
|
010cbd0a73 | ||
|
|
87bcd70f59 | ||
|
|
39799b9db7 | ||
|
|
453844b9e8 | ||
|
|
677775079f | ||
|
|
3f4d6b9452 | ||
|
|
4f75d7f8e2 | ||
|
|
902468e3e0 | ||
|
|
c75afdb321 | ||
|
|
567634f2a8 | ||
|
|
83c3c23c27 | ||
|
|
3bde614bd3 | ||
|
|
9a68243fcc | ||
|
|
b44169de41 | ||
|
|
bd2ee70c63 | ||
|
|
87dba2765b | ||
|
|
af17e20f99 | ||
|
|
a471caf787 | ||
|
|
cdcd9fd1a2 | ||
|
|
84d032c104 | ||
|
|
b305abdc8f | ||
|
|
e1e7b7e88a | ||
|
|
150730d322 | ||
|
|
8754b321df | ||
|
|
47835aaad9 | ||
|
|
c035133353 | ||
|
|
22287e16e2 | ||
|
|
1c943eb89f | ||
|
|
4c596aaac2 | ||
|
|
f4321279b9 | ||
|
|
2a372df33c | ||
|
|
ef536ba909 | ||
|
|
b192c6e658 | ||
|
|
89b2ae01a6 | ||
|
|
edb4457684 | ||
|
|
bb6d6a4f96 | ||
|
|
a36ea5addc | ||
|
|
98a050e664 | ||
|
|
0419dc9632 | ||
|
|
eed9faedaa | ||
|
|
41dd93c6b2 | ||
|
|
00ae975f0b | ||
|
|
c51c40ede7 | ||
|
|
65ffc5b3d9 | ||
|
|
d39708b3f6 | ||
|
|
e1b0ab5c3f | ||
|
|
8a6e1a695b | ||
|
|
e9f4bde18f | ||
|
|
6551814396 | ||
|
|
88887ea58e | ||
|
|
dcd79df9fb | ||
|
|
63ffac6afe | ||
|
|
248fa38c34 | ||
|
|
3165f3adbe | ||
|
|
b5d843b1fd | ||
|
|
c4714d757d | ||
|
|
5ac6dc62e7 | ||
|
|
788deffa2b | ||
|
|
f8438704a6 | ||
|
|
4448737bd8 | ||
|
|
c3decbab32 | ||
|
|
a91d709aa5 | ||
|
|
4d465d6cf9 | ||
|
|
083f45678d | ||
|
|
225c33633a | ||
|
|
a522327662 | ||
|
|
486a30402b | ||
|
|
64c4f7302d | ||
|
|
e105dc6289 | ||
|
|
aa3cc9b9a0 | ||
|
|
98d1aac765 | ||
|
|
693a9c5b95 | ||
|
|
f555492292 | ||
|
|
c52755e747 | ||
|
|
61fa20d6a9 | ||
|
|
85cf995011 | ||
|
|
8f75be52a1 | ||
|
|
4707a319e5 | ||
|
|
ef8d0f497d | ||
|
|
e22996735f | ||
|
|
5e78aaaec3 | ||
|
|
5f0a21d2d4 | ||
|
|
a409e3d32e | ||
|
|
71f811930f | ||
|
|
cbac914649 | ||
|
|
2f01107b09 | ||
|
|
dde2bea2cc | ||
|
|
6ec4a093c2 | ||
|
|
521b66c488 | ||
|
|
a43efef9f0 | ||
|
|
17404e0956 | ||
|
|
b87e303c00 | ||
|
|
d32996b8c9 | ||
|
|
a3cf73b220 | ||
|
|
55c588a03a | ||
|
|
3058415b4e | ||
|
|
c3b4029d0b | ||
|
|
5e16d85ff6 | ||
|
|
87f35efa2f | ||
|
|
73ce9993f2 | ||
|
|
9d80770dfc | ||
|
|
e7c3e4cd21 | ||
|
|
ecd6c44a32 | ||
|
|
43648b1660 | ||
|
|
9733621301 | ||
|
|
bc22739a96 | ||
|
|
d09d8d34c2 | ||
|
|
6325a3458f | ||
|
|
808510746e | ||
|
|
da738dddab | ||
|
|
aa1ee123b3 | ||
|
|
e69163d072 | ||
|
|
b6228c99cd | ||
|
|
6c75893956 | ||
|
|
878e34c582 | ||
|
|
f7f4d066dc | ||
|
|
bc3629370d | ||
|
|
397d6decc0 | ||
|
|
f23cc6be02 | ||
|
|
512c117395 | ||
|
|
03f53f2282 | ||
|
|
ef97352f71 | ||
|
|
b2071a4352 | ||
|
|
463060ce52 | ||
|
|
dc55591a5e | ||
|
|
3de33f7a4e | ||
|
|
deeadb7f8e | ||
|
|
29deee8161 | ||
|
|
c46b12b234 | ||
|
|
ff07ca97df | ||
|
|
ed60a375b5 | ||
|
|
11005ccb63 | ||
|
|
4a88ffdf2a | ||
|
|
84b4fed3df | ||
|
|
3dcb34e462 | ||
|
|
b7da988ee0 | ||
|
|
7ec0a36dc2 | ||
|
|
bddb41cd47 | ||
|
|
ee35f72861 | ||
|
|
62ec464d91 | ||
|
|
2c95622890 | ||
|
|
219f4a2f3b | ||
|
|
e38a4121e7 | ||
|
|
b470cca533 | ||
|
|
5565546295 | ||
|
|
5cb8d4cc11 | ||
|
|
51c8c50b82 | ||
|
|
c7d106cfa4 | ||
|
|
29e1f5d98b | ||
|
|
aac90133d6 | ||
|
|
0ac847fb3c | ||
|
|
70f5365398 | ||
|
|
b1eecb7051 | ||
|
|
d82943f48c | ||
|
|
c4249f94de | ||
|
|
9ed83a808a | ||
|
|
ea37904c75 | ||
|
|
1b70a7e4c7 | ||
|
|
d7ccea8ac5 | ||
|
|
1fcff5f8d1 | ||
|
|
78c7be09f8 | ||
|
|
a37adddacd | ||
|
|
ccbf908d22 | ||
|
|
d444a8eadc | ||
|
|
b5e31c0f25 | ||
|
|
c4943ff4f5 | ||
|
|
699650565e | ||
|
|
1c90c729bc | ||
|
|
45a76fa90b | ||
|
|
911c1852d5 | ||
|
|
e85b0c49d8 | ||
|
|
b0a059250a | ||
|
|
b94b7860d9 | ||
|
|
478833f069 | ||
|
|
5657bf52f0 | ||
|
|
c3333006cf | ||
|
|
c2885077c2 | ||
|
|
8e20ef6cb5 | ||
|
|
468d84faba | ||
|
|
cf6c089e72 | ||
|
|
2f70f778c9 | ||
|
|
9400863949 | ||
|
|
f831d3bbd6 | ||
|
|
7fd9ef3d22 | ||
|
|
705d4cbba9 | ||
|
|
c9e53bf78c | ||
|
|
7cd280557c | ||
|
|
eaf888b02a | ||
|
|
58da9c3c11 | ||
|
|
68d36ff3ed | ||
|
|
0ed5ed20b5 | ||
|
|
18a589003e | ||
|
|
da6fdc963c | ||
|
|
1c76ed2c40 | ||
|
|
ceb410fb5c | ||
|
|
4fa7843050 | ||
|
|
3205f98d05 | ||
|
|
0092254007 | ||
|
|
ee91c9d5f1 | ||
|
|
2151676db1 | ||
|
|
dc9658b003 | ||
|
|
b527921f3f | ||
|
|
0e66b51ca0 | ||
|
|
33e96fd11a | ||
|
|
f99ac24d5c | ||
|
|
2e037014c3 | ||
|
|
8c4aaa8286 | ||
|
|
dc8c018e28 | ||
|
|
57a8c453b9 | ||
|
|
e5dc56c483 | ||
|
|
812df81d92 | ||
|
|
67c29be3c6 | ||
|
|
cf5e8491df | ||
|
|
53f828f00e | ||
|
|
357489d444 | ||
|
|
331c65fd1d | ||
|
|
56b09d9f72 | ||
|
|
d4ed398e4f | ||
|
|
951a580907 | ||
|
|
3b72b45319 | ||
|
|
2650ceb0a6 | ||
|
|
c5fc3cc08e | ||
|
|
fdaf471a03 | ||
|
|
bdac6f91dd | ||
|
|
27de07e93d | ||
|
|
8154d0af53 | ||
|
|
466f76345b | ||
|
|
9be496f953 | ||
|
|
fc83e2b1c4 | ||
|
|
552f9a8989 | ||
|
|
4f5b175e55 | ||
|
|
13d6923c11 | ||
|
|
4acca22ff0 | ||
|
|
1483a51aa1 | ||
|
|
f5a34e9ee8 | ||
|
|
d69e7eb12a | ||
|
|
c44aaf1883 | ||
|
|
4b91969d0f | ||
|
|
92c54d3c9d | ||
|
|
bc9ce23fdc | ||
|
|
cab33d440b | ||
|
|
267de1861d | ||
|
|
b3793b0198 | ||
|
|
8486c675c8 | ||
|
|
5e49b27dba | ||
|
|
b6df7b3afe | ||
|
|
6f74a66c8a | ||
|
|
31a7db2657 | ||
|
|
68fd7c021c | ||
|
|
e1e64ae430 | ||
|
|
9080607028 | ||
|
|
6e9a5139b4 | ||
|
|
f44305af0d | ||
|
|
8f4a4214a1 | ||
|
|
ff210a98db | ||
|
|
018175ec2d | ||
|
|
faa88dc2f3 | ||
|
|
9ad1f30a8c | ||
|
|
5053fae5b4 | ||
|
|
060c7f2b45 | ||
|
|
acb603bff7 | ||
|
|
d297167fef | ||
|
|
e36ee54a16 | ||
|
|
41aec357b0 | ||
|
|
f3fa4f11ba | ||
|
|
cb8fc9cf2d | ||
|
|
aaa3d2d74f | ||
|
|
c17f564718 | ||
|
|
3389071361 | ||
|
|
96da3b9560 | ||
|
|
3bb9625ced | ||
|
|
1bdc47220b | ||
|
|
41473ff450 | ||
|
|
5aa4088051 | ||
|
|
9f444f1f6a | ||
|
|
49effca35d | ||
|
|
fb28f03155 | ||
|
|
2afc4704ad | ||
|
|
5496fc014c | ||
|
|
7756c151ed | ||
|
|
83c458d2fe | ||
|
|
956436b943 | ||
|
|
3bb9c4b280 | ||
|
|
c38463c9a9 | ||
|
|
fc49592769 | ||
|
|
6643569efc | ||
|
|
fe0ea13f70 | ||
|
|
c979b59e1e | ||
|
|
144ca11c03 | ||
|
|
a432fa5fcf | ||
|
|
805bb7c468 | ||
|
|
dbc70f8f05 | ||
|
|
4b67008dba | ||
|
|
f4b683aa2f | ||
|
|
7de6ecdedf | ||
|
|
bd070857ed | ||
|
|
d3d1ba2488 | ||
|
|
eae82b1085 | ||
|
|
f9fd234cf8 | ||
|
|
1dfee05b7e | ||
|
|
dd42e7706a | ||
|
|
066d18df7a | ||
|
|
06f6ded20f | ||
|
|
3a775fc2bf | ||
|
|
0d5e971a0c | ||
|
|
9aed4f830f | ||
|
|
5947e04226 | ||
|
|
611ff05bde | ||
|
|
0e890e5692 | ||
|
|
995d5ccf66 | ||
|
|
6584dc2480 | ||
|
|
a922e844eb | ||
|
|
4bd05ed96e | ||
|
|
0de32f682a | ||
|
|
245567118c | ||
|
|
021f055c36 | ||
|
|
5f707c5585 | ||
|
|
232da66b53 | ||
|
|
ebeee92e51 | ||
|
|
f481947b0d | ||
|
|
94ea7031e8 | ||
|
|
0d08f7db97 | ||
|
|
6443366f50 | ||
|
|
70c41a7dc3 | ||
|
|
8804623121 | ||
|
|
1fb6d1286f | ||
|
|
511df81201 | ||
|
|
2f081fa6fa | ||
|
|
3b27d9e819 | ||
|
|
682c93f262 | ||
|
|
51c96b0b7e | ||
|
|
224f426765 | ||
|
|
e9657cfb48 | ||
|
|
4200ac0da3 | ||
|
|
434f7f3bcb | ||
|
|
03cc196965 | ||
|
|
c0a76220dd | ||
|
|
9d04fb4992 | ||
|
|
02fcf33067 | ||
|
|
25c88b3f5c | ||
|
|
2d94904241 | ||
|
|
bbf1247f80 | ||
|
|
b82b73ef94 | ||
|
|
15d6f60f25 | ||
|
|
a99e70d96e | ||
|
|
9eeceb2455 | ||
|
|
7901e18fa6 | ||
|
|
2befef0b21 | ||
|
|
8869cd7008 | ||
|
|
91e6ae2a7d | ||
|
|
6ab8e05a5e | ||
|
|
717f99a352 | ||
|
|
735cd78dc2 | ||
|
|
c820501cbb | ||
|
|
43ef2395ac | ||
|
|
bb3d94f1c5 | ||
|
|
c45fbb6491 | ||
|
|
fc291e4ca2 | ||
|
|
ad8c5f5452 | ||
|
|
721d82b91a | ||
|
|
0c62c39a1d | ||
|
|
8d643e4b85 | ||
|
|
d542a74733 | ||
|
|
16078a9df6 | ||
|
|
0bd17c6d0f | ||
|
|
77401e6f5c | ||
|
|
8b42435f7a | ||
|
|
3147e850be | ||
|
|
0b33381efb | ||
|
|
ee7a9a34e0 | ||
|
|
148f92f92d | ||
|
|
4ee49552ce | ||
|
|
40caaaab23 | ||
|
|
1bc1c04be5 | ||
|
|
18abc66585 | ||
|
|
f79df6982d | ||
|
|
e85e31773a | ||
|
|
e5336a2d75 | ||
|
|
649283df09 | ||
|
|
7222a896d8 | ||
|
|
b5712bf8b0 | ||
|
|
06b6625c01 | ||
|
|
7bc2e33e83 | ||
|
|
eb4f57fb8b | ||
|
|
0f5d3f38da | ||
|
|
76da178cc1 | ||
|
|
38a2d2fe68 | ||
|
|
9397ba5bd2 | ||
|
|
7093962f30 | ||
|
|
7022e4b9ca | ||
|
|
b8d67a42bd | ||
|
|
106cb8e373 | ||
|
|
9492eda5ef | ||
|
|
a7826d9ea4 | ||
|
|
64ddcc8960 | ||
|
|
c7bca6a3fb | ||
|
|
f1ce933b33 | ||
|
|
17990512ce | ||
|
|
a30fb5909b | ||
|
|
3dea5adf5c | ||
|
|
5aca563a01 | ||
|
|
bf1ebcdf8f | ||
|
|
3252748345 | ||
|
|
72eb29c01b | ||
|
|
0f3156dfbe | ||
|
|
b21875eaaf | ||
|
|
2591615a3c | ||
|
|
691554ad1c | ||
|
|
f43fde5797 | ||
|
|
783cdb1357 | ||
|
|
2de17cb1a4 | ||
|
|
3b6946d3da | ||
|
|
b8adc8f498 | ||
|
|
ca7c4d2c86 | ||
|
|
d8bafb0d1c | ||
|
|
cd0724b827 | ||
|
|
6e66e2591b | ||
|
|
fd0556909f | ||
|
|
ac2120da1e | ||
|
|
f3904a7e39 | ||
|
|
b3923ec3ca | ||
|
|
9ffdad6465 | ||
|
|
f247ebfbe1 | ||
|
|
713e040481 | ||
|
|
f58f36fc8f | ||
|
|
195cd2c898 | ||
|
|
6bb09dc58c | ||
|
|
33f3374ea6 | ||
|
|
41baaca21d | ||
|
|
d650cde323 | ||
|
|
d641c845dd | ||
|
|
e651c6cacf | ||
|
|
2e10d67610 | ||
|
|
eab395f58a | ||
|
|
2f92957e15 | ||
|
|
e89d4e14ea | ||
|
|
5525f63032 | ||
|
|
7bc1390366 | ||
|
|
e91fb94d0e | ||
|
|
5c03a2e251 | ||
|
|
1741fcf84d | ||
|
|
52215e9166 | ||
|
|
4cfc135652 | ||
|
|
8ee643e88d | ||
|
|
ff632bf9b8 | ||
|
|
ce9ed88b03 | ||
|
|
e6a4a08120 | ||
|
|
388ee087c0 | ||
|
|
2fb8883918 | ||
|
|
28ccd42a1c | ||
|
|
fcd814a2c3 | ||
|
|
fe17cbc1a8 | ||
|
|
63b3e71909 | ||
|
|
b549d669d6 | ||
|
|
802b38eede | ||
|
|
c1c8b6af44 | ||
|
|
4b57e7bd53 | ||
|
|
3bd434ddf2 | ||
|
|
834a5df580 | ||
|
|
e40c2354d5 | ||
|
|
b0eca12d88 | ||
|
|
3a86983207 | ||
|
|
f461ddeb7e | ||
|
|
7b534baf15 | ||
|
|
74d8bdd3a7 | ||
|
|
657739d48b | ||
|
|
f8b27dd662 | ||
|
|
18c7f4698a | ||
|
|
ccb337e8eb | ||
|
|
1ff677c300 | ||
|
|
04145b19a1 | ||
|
|
6cb8d03bf6 | ||
|
|
94ff904a04 | ||
|
|
a0c388f283 | ||
|
|
56e537786f | ||
|
|
810f9eaaad | ||
|
|
31427e9c42 | ||
|
|
384b99435b | ||
|
|
425d182f21 | ||
|
|
4394ba1fe1 | ||
|
|
4828348532 | ||
|
|
be5a4cf5e3 | ||
|
|
d17a92f713 | ||
|
|
bfedee0532 | ||
|
|
5ac2230c5d | ||
|
|
ab531d946e | ||
|
|
1a8fd08563 | ||
|
|
c6ddf89980 | ||
|
|
71c39ae583 | ||
|
|
7209ef4aa7 | ||
|
|
6b55e6781f | ||
|
|
c8c048c3a3 | ||
|
|
4887c9ea6f | ||
|
|
495d575ebc | ||
|
|
18170a1de5 | ||
|
|
7ce144f493 | ||
|
|
2279b605c6 | ||
|
|
3b78f9c2a5 | ||
|
|
7c029ce808 | ||
|
|
b9052bc244 | ||
|
|
b7025ad9d6 | ||
|
|
c5482c2503 | ||
|
|
d394adfaf7 | ||
|
|
bc771d9c50 | ||
|
|
96ec176b83 | ||
|
|
1845938e70 | ||
|
|
fad81ab85e | ||
|
|
d1c64f5c74 | ||
|
|
7f6c93bdce | ||
|
|
f57d2ef31f | ||
|
|
f28ded8455 | ||
|
|
7730c88c74 | ||
|
|
e80bc78780 | ||
|
|
ac6b540fd8 | ||
|
|
8c9276370c | ||
|
|
b91370aff7 | ||
|
|
30424df7ce | ||
|
|
14f7f4758a | ||
|
|
c6ba51127f | ||
|
|
ddbbddbd14 | ||
|
|
79c19983e0 | ||
|
|
9b961fb41e | ||
|
|
1db995be0d | ||
|
|
5675a44ffd | ||
|
|
aeb3fc6729 | ||
|
|
48295e5161 | ||
|
|
4f79d09d7b | ||
|
|
0c18d4e058 | ||
|
|
dbed937fc6 | ||
|
|
ffc39b0235 | ||
|
|
f72f58dbc4 | ||
|
|
9d0f4a2152 | ||
|
|
1ed4ab4299 | ||
|
|
969c96b070 | ||
|
|
3f69d348a1 | ||
|
|
63fff151c7 | ||
|
|
9920e0b89a | ||
|
|
3042f29c15 | ||
|
|
99273e1118 | ||
|
|
041dbd482d | ||
|
|
b4aa1de10a | ||
|
|
c5a9b98cbe | ||
|
|
21f47fbe58 | ||
|
|
49f115dce3 | ||
|
|
a81d0327d2 | ||
|
|
9eafe982ee | ||
|
|
a46bfdd0fc | ||
|
|
16f26c4f99 | ||
|
|
03e0c4c617 | ||
|
|
47790b49d4 | ||
|
|
b25b069917 | ||
|
|
bb190f9610 | ||
|
|
d65ae68668 | ||
|
|
f625350439 | ||
|
|
f4e8f64bf7 | ||
|
|
42fd0a0a62 | ||
|
|
b78439b334 | ||
|
|
1082d73355 | ||
|
|
d91087492d | ||
|
|
cab7cd37b8 | ||
|
|
201a18d6ba | ||
|
|
f990f4a8d4 | ||
|
|
aa5e37f2db | ||
|
|
e7c89b6153 | ||
|
|
3e49d6b900 | ||
|
|
8aaff7fec1 | ||
|
|
51ac23c9f1 | ||
|
|
9dd0361d0e | ||
|
|
3d2840edb6 | ||
|
|
ce0a59b60d | ||
|
|
2d8acf92f0 | ||
|
|
bc2ffa39fc | ||
|
|
390c805ef4 | ||
|
|
5b753dfd6e | ||
|
|
5c8b80b01a | ||
|
|
95d62039b1 | ||
|
|
78acfb0040 | ||
|
|
eb821efda7 | ||
|
|
925825a41b | ||
|
|
f925266c1b | ||
|
|
07ff8df58d | ||
|
|
0a0f02c0c6 | ||
|
|
d2f41ae9ef | ||
|
|
5a4f5f54a7 | ||
|
|
eabfa8f3af | ||
|
|
1557f48740 | ||
|
|
00d787a75b | ||
|
|
3b454fa95a | ||
|
|
0da4d64d38 | ||
|
|
6e2cf23a73 | ||
|
|
8b0bc6937d | ||
|
|
872fd98eda | ||
|
|
5bcd3b6fe6 | ||
|
|
1aed585a19 | ||
|
|
831eba8b1c | ||
|
|
b09a831d15 | ||
|
|
4d3d8b35d9 | ||
|
|
c323028179 | ||
|
|
94dbda503f | ||
|
|
beefff3d48 | ||
|
|
c2e5081437 | ||
|
|
786c3e4137 | ||
|
|
0d33714f28 | ||
|
|
1fbba38436 | ||
|
|
15c3d712d3 | ||
|
|
5b01f544d1 | ||
|
|
8b8e521c4e | ||
|
|
fe4c591cfd | ||
|
|
0cd613ae52 | ||
|
|
0082f468b4 | ||
|
|
eec57e84e4 | ||
|
|
70149ea05e | ||
|
|
1d93f41fcf | ||
|
|
cd0f41a3e0 | ||
|
|
094c9fd802 | ||
|
|
1584a78fc9 | ||
|
|
88248ad2d3 | ||
|
|
1a203031e0 | ||
|
|
05c3344554 | ||
|
|
888be71639 | ||
|
|
3902929d9f | ||
|
|
760a739e91 | ||
|
|
1c7c475c43 | ||
|
|
cef7fd484b | ||
|
|
caabca3f02 | ||
|
|
d92c476388 | ||
|
|
36b7075cf4 | ||
|
|
f3761c26e9 | ||
|
|
43daf4f82c | ||
|
|
932be0ad64 | ||
|
|
9012dced6a | ||
|
|
50bed78d7a | ||
|
|
60250355cb | ||
|
|
75afc2dc0e | ||
|
|
225b13da93 | ||
|
|
37c748192d | ||
|
|
b7a2957340 | ||
|
|
a6ce6a249b | ||
|
|
8834e6e531 | ||
|
|
04f40303fd | ||
|
|
ececc5ec2c | ||
|
|
81547c5981 | ||
|
|
a911b268aa | ||
|
|
39010fd153 | ||
|
|
dc8a618b6a | ||
|
|
f3e7fea628 | ||
|
|
926349b1f8 | ||
|
|
ec29c24916 | ||
|
|
3842eade67 | ||
|
|
bd338a9043 | ||
|
|
cf7e2d5d75 | ||
|
|
2673fe05a5 | ||
|
|
180fdffab1 | ||
|
|
62e422f75a | ||
|
|
41565e91ed | ||
|
|
c9610e9949 | ||
|
|
29dc083d8d | ||
|
|
39d6383474 | ||
|
|
f679065d2c | ||
|
|
0a97e87a8e | ||
|
|
4d81455a83 | ||
|
|
39091fe4df | ||
|
|
bac5245cd0 | ||
|
|
274f9a3f32 | ||
|
|
a513ab9a59 | ||
|
|
e83635ee5a | ||
|
|
d79372a46d | ||
|
|
bbd11c9e89 | ||
|
|
152fd52cd7 | ||
|
|
ccabdbc83b | ||
|
|
56c8221b3f | ||
|
|
add8980790 | ||
|
|
5157e1a96c | ||
|
|
d132abcdb4 | ||
|
|
d60348572e | ||
|
|
f55faae31b | ||
|
|
0cff94d90e | ||
|
|
7fc25cafb2 | ||
|
|
a7859de625 | ||
|
|
4bb76acc37 | ||
|
|
b513933040 | ||
|
|
18ea9d3f18 | ||
|
|
7b660a9ebc | ||
|
|
783a49bd97 | ||
|
|
d3c6b09354 | ||
|
|
3d61496d25 | ||
|
|
16bff9e82f | ||
|
|
22f25731e8 | ||
|
|
035f51ad58 | ||
|
|
e9795bd772 | ||
|
|
93b516a4ec | ||
|
|
fc9d5b2a62 | ||
|
|
e3bfb95c52 | ||
|
|
047ea8c143 | ||
|
|
752cb9e4f4 | ||
|
|
f54b9b12b0 | ||
|
|
cb99b8f04d | ||
|
|
7c03bcba2b | ||
|
|
92fa7271ed | ||
|
|
d3486cab31 | ||
|
|
dd0a870969 | ||
|
|
0c4c268003 | ||
|
|
ff57848268 | ||
|
|
d223fee9b9 | ||
|
|
ad18d084f3 | ||
|
|
9941d1f160 | ||
|
|
13fa56b5b1 | ||
|
|
9ce48b4dc4 | ||
|
|
abb2b860f2 | ||
|
|
930c36e757 | ||
|
|
2d2ce5df85 | ||
|
|
2b23c43434 | ||
|
|
bd597497e7 | ||
|
|
be1f841b37 | ||
|
|
d98a428100 | ||
|
|
26d330e744 | ||
|
|
61bed38afb | ||
|
|
16fbc6b270 | ||
|
|
fe132de3c8 | ||
|
|
f85bf0867c | ||
|
|
b441a7fbc4 | ||
|
|
8497d296b1 | ||
|
|
3ee2508ec8 | ||
|
|
ff8d5ac4b5 | ||
|
|
7fc98b2183 | ||
|
|
a4adafd8ad | ||
|
|
c1bc3aeab9 | ||
|
|
edf962cdb5 | ||
|
|
2fa13cdf86 | ||
|
|
39de7673eb | ||
|
|
d930d8cc4a | ||
|
|
97626a3ba5 | ||
|
|
b7f7d04639 | ||
|
|
13674bd859 | ||
|
|
fb9cbc0471 | ||
|
|
2f60288d86 | ||
|
|
ee3ded0fc2 | ||
|
|
351bad9ec4 | ||
|
|
9bf7473bbf | ||
|
|
fa09c88f5c | ||
|
|
83df78d0c8 | ||
|
|
79266f7302 | ||
|
|
7fecc7236c | ||
|
|
9c7f6b7b71 | ||
|
|
b46da93e99 | ||
|
|
e299a1fb20 | ||
|
|
122033cadb | ||
|
|
df9bd1b3b5 | ||
|
|
f74492eb59 | ||
|
|
eaf1ae37dd | ||
|
|
8e3b412ff6 | ||
|
|
ba17f576e9 | ||
|
|
9415ce4512 | ||
|
|
239536933b | ||
|
|
80b34598e9 | ||
|
|
9c66b92c34 | ||
|
|
79872ea5e2 | ||
|
|
cbf181bd76 | ||
|
|
1393d21858 | ||
|
|
3a46b7bd18 | ||
|
|
0bbfd81d26 | ||
|
|
86db517142 | ||
|
|
50151f4007 | ||
|
|
0395d1f91f | ||
|
|
5f4c1e4057 | ||
|
|
d14413f3b0 | ||
|
|
4fd968270c | ||
|
|
708a7dd362 | ||
|
|
cd85b75312 | ||
|
|
d685da377e | ||
|
|
8583992d23 | ||
|
|
23fec75c90 | ||
|
|
ebe7303894 | ||
|
|
79fb977f10 | ||
|
|
c0af3414a3 | ||
|
|
1857d37fae | ||
|
|
60fdbb56a9 | ||
|
|
4c7853164d | ||
|
|
6c7a3ce4bb | ||
|
|
a9e74b21f1 | ||
|
|
e6730f7164 | ||
|
|
3344723393 | ||
|
|
c571185a91 | ||
|
|
325c1cfa41 | ||
|
|
1069421753 | ||
|
|
b33a97ea5b | ||
|
|
d2c1d4c337 | ||
|
|
67762cf1d8 | ||
|
|
eadce0287c | ||
|
|
ecaff5b63f | ||
|
|
a300c9ef96 | ||
|
|
44fe71e4db | ||
|
|
0ac32188c5 | ||
|
|
9aaace706b | ||
|
|
b22de5a824 | ||
|
|
97463661c1 | ||
|
|
239a11855a | ||
|
|
0632557d91 | ||
|
|
44be7d4c51 | ||
|
|
efb4a9d327 | ||
|
|
a077a3f609 | ||
|
|
3ccec0aab0 | ||
|
|
3006133f0e | ||
|
|
79beb25530 | ||
|
|
b47b228164 | ||
|
|
be91db14d9 | ||
|
|
120893209e | ||
|
|
f19630bcf5 | ||
|
|
9d93fda471 | ||
|
|
d986659add | ||
|
|
00dab7ca5f | ||
|
|
a4add403fb | ||
|
|
e9cdc96c74 | ||
|
|
6af1fea232 | ||
|
|
45d5d9e44f | ||
|
|
376a084aca | ||
|
|
d1f42d47fe | ||
|
|
64b8fd87ad | ||
|
|
364be48248 | ||
|
|
2bce046278 | ||
|
|
1120d552b6 | ||
|
|
69cab0817f | ||
|
|
c4d03bf378 | ||
|
|
6c039be2ca | ||
|
|
832dabc8a4 | ||
|
|
1da2028d9d | ||
|
|
7c3f6dcc8d | ||
|
|
1472884eb5 | ||
|
|
ec22b1c706 | ||
|
|
a1712df7c2 | ||
|
|
a40e11cb3e | ||
|
|
61c46bea40 | ||
|
|
1c5c28a82c | ||
|
|
2310145937 | ||
|
|
6a9c9cadd0 | ||
|
|
7774ff9944 | ||
|
|
33d4c95470 | ||
|
|
659cbc05a9 | ||
|
|
6ce65de2cd | ||
|
|
93b2eb3ff6 | ||
|
|
bf71300635 | ||
|
|
37ecd4a0bc | ||
|
|
827a1b181b | ||
|
|
c4e7cb75cd | ||
|
|
98e4bfcda8 | ||
|
|
ee48ca7671 | ||
|
|
4ba6de1116 | ||
|
|
bfbe636555 | ||
|
|
54ae43ef47 | ||
|
|
7a74b5ee3e | ||
|
|
0e9d43d605 | ||
|
|
cc54363c27 | ||
|
|
89affe3139 | ||
|
|
2c4977dbb1 | ||
|
|
e240175116 | ||
|
|
2398ed6fe8 | ||
|
|
a8420ac33c | ||
|
|
8470be6411 | ||
|
|
3d6295c622 | ||
|
|
ff2f7206f3 | ||
|
|
b937fc8978 | ||
|
|
86a9a51952 | ||
|
|
4188c9a1dd | ||
|
|
8c00f89e36 | ||
|
|
9e8ac5c96b | ||
|
|
05a67f4716 | ||
|
|
f49476a206 | ||
|
|
c1e9c56e25 | ||
|
|
d5dd73cacf | ||
|
|
21f7a49b4e | ||
|
|
716ac04e13 | ||
|
|
c28a32fc47 | ||
|
|
31cba28e8a | ||
|
|
48cd7e6481 | ||
|
|
47aba1c9f9 | ||
|
|
0f3f8bc0d9 | ||
|
|
e0df12c212 | ||
|
|
eb448d9bb8 | ||
|
|
0ba77f13db | ||
|
|
f0a2eb843c | ||
|
|
5cf3d9e4d9 | ||
|
|
41958f55cd | ||
|
|
600ad232e1 | ||
|
|
7a3825cfce | ||
|
|
9519653422 | ||
|
|
efa2307c73 | ||
|
|
068fa3d0e3 | ||
|
|
13d8dbd542 | ||
|
|
b442ba8b2b | ||
|
|
10e36d2355 | ||
|
|
13c53fedad | ||
|
|
4bda1bd884 | ||
|
|
3abe7850d6 | ||
|
|
b50284d864 | ||
|
|
81c6e52401 | ||
|
|
847d257366 | ||
|
|
687662cf1f | ||
|
|
6432d98469 | ||
|
|
088ccf8b8d | ||
|
|
e8683bf957 | ||
|
|
4653981b6b | ||
|
|
e2547413d3 | ||
|
|
ea17f41b5b | ||
|
|
29178d8adf | ||
|
|
7e86ead574 | ||
|
|
72debcb228 | ||
|
|
72737dabc7 | ||
|
|
f6e5cb4381 | ||
|
|
ffad3b5fb1 | ||
|
|
cba9fc3020 | ||
|
|
e776accaf3 | ||
|
|
3eac26929a | ||
|
|
4d3adec738 | ||
|
|
89bed479e4 | ||
|
|
fdd673a3a9 | ||
|
|
22f6d285c7 | ||
|
|
10aa16b471 | ||
|
|
b3838581fd | ||
|
|
affbe7ccdb | ||
|
|
dd8577f832 | ||
|
|
d7f5da5df4 | ||
|
|
9fda130b3a | ||
|
|
72cdbdba0f | ||
|
|
b92a153902 | ||
|
|
9f2927979b | ||
|
|
75257232c3 | ||
|
|
1721314c62 | ||
|
|
fc230bcc59 | ||
|
|
b4636ddf44 | ||
|
|
b1140301a4 | ||
|
|
58cd785da6 | ||
|
|
2035186cd2 | ||
|
|
53ba6aadff | ||
|
|
f091868b7c | ||
|
|
89bedae0d3 | ||
|
|
c8acc48976 | ||
|
|
21fee59b22 | ||
|
|
957a8253f8 | ||
|
|
d5fc3e7bed | ||
|
|
ab438b42da | ||
|
|
3867fece4a | ||
|
|
2b908d4fbe | ||
|
|
8ff062ec8b | ||
|
|
294fc41aec | ||
|
|
684f7df158 | ||
|
|
c3287755e3 | ||
|
|
9f97f4d79e | ||
|
|
34eb421649 | ||
|
|
850b05573e | ||
|
|
6ec8bfdfee | ||
|
|
81638c248e | ||
|
|
2e11b1298e | ||
|
|
20320f3a27 | ||
|
|
4019c12d26 | ||
|
|
cf72184ce4 | ||
|
|
ca8d15bc64 | ||
|
|
a91c897fd3 | ||
|
|
816bdf0320 | ||
|
|
d4a6acbd99 | ||
|
|
e421db4005 | ||
|
|
9067c2a9c1 | ||
|
|
9f7321ca1a | ||
|
|
5fa01132b9 | ||
|
|
e082b6d599 | ||
|
|
d44be2d835 | ||
|
|
7dc8557033 | ||
|
|
72037a1865 | ||
|
|
2d1621c43d | ||
|
|
d1a5db3310 | ||
|
|
ad8fd8fecc | ||
|
|
be74b76079 | ||
|
|
dd64af728f | ||
|
|
e43b46786d | ||
|
|
3f3b37b843 | ||
|
|
2ecf9f6ddf | ||
|
|
48c069fe68 | ||
|
|
9c5c597c85 | ||
|
|
c2eec8545d | ||
|
|
2395d4be26 | ||
|
|
9455476705 | ||
|
|
494e223706 | ||
|
|
348fd18230 | ||
|
|
7233b4de55 | ||
|
|
af6df05685 | ||
|
|
965b65db6e | ||
|
|
4cc01c8aa8 | ||
|
|
41372168b6 | ||
|
|
f4438b0a08 | ||
|
|
897c842637 | ||
|
|
ee86ceb906 | ||
|
|
e298732499 | ||
|
|
4081937e22 | ||
|
|
f9aedb2118 | ||
|
|
74b4719af8 | ||
|
|
2f35cc9188 | ||
|
|
2f966d8c38 | ||
|
|
b0868d9136 | ||
|
|
37440e9416 | ||
|
|
0d7d27ec0b |
@ -1,168 +0,0 @@
|
|||||||
---
|
|
||||||
name: backend-code-review
|
|
||||||
description: Review backend code for quality, security, maintainability, and best practices based on established checklist rules. Use when the user requests a review, analysis, or improvement of backend files (e.g., `.py`) under the `api/` directory. Do NOT use for frontend files (e.g., `.tsx`, `.ts`, `.js`). Supports pending-change review, code snippets review, and file-focused review.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Backend Code Review
|
|
||||||
|
|
||||||
## When to use this skill
|
|
||||||
|
|
||||||
Use this skill whenever the user asks to **review, analyze, or improve** backend code (e.g., `.py`) under the `api/` directory. Supports the following review modes:
|
|
||||||
|
|
||||||
- **Pending-change review**: when the user asks to review current changes (inspect staged/working-tree files slated for commit to get the changes).
|
|
||||||
- **Code snippets review**: when the user pastes code snippets (e.g., a function/class/module excerpt) into the chat and asks for a review.
|
|
||||||
- **File-focused review**: when the user points to specific files and asks for a review of those files (one file or a small, explicit set of files, e.g., `api/...`, `api/app.py`).
|
|
||||||
|
|
||||||
Do NOT use this skill when:
|
|
||||||
|
|
||||||
- The request is about frontend code or UI (e.g., `.tsx`, `.ts`, `.js`, `web/`).
|
|
||||||
- The user is not asking for a review/analysis/improvement of backend code.
|
|
||||||
- The scope is not under `api/` (unless the user explicitly asks to review backend-related changes outside `api/`).
|
|
||||||
|
|
||||||
## How to use this skill
|
|
||||||
|
|
||||||
Follow these steps when using this skill:
|
|
||||||
|
|
||||||
1. **Identify the review mode** (pending-change vs snippet vs file-focused) based on the user’s input. Keep the scope tight: review only what the user provided or explicitly referenced.
|
|
||||||
2. Follow the rules defined in **Checklist** to perform the review. If no Checklist rule matches, apply **General Review Rules** as a fallback to perform the best-effort review.
|
|
||||||
3. Compose the final output strictly follow the **Required Output Format**.
|
|
||||||
|
|
||||||
Notes when using this skill:
|
|
||||||
- Always include actionable fixes or suggestions (including possible code snippets).
|
|
||||||
- Use best-effort `File:Line` references when a file path and line numbers are available; otherwise, use the most specific identifier you can.
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
- db schema design: if the review scope includes code/files under `api/models/` or `api/migrations/`, follow [references/db-schema-rule.md](references/db-schema-rule.md) to perform the review
|
|
||||||
- architecture: if the review scope involves controller/service/core-domain/libs/model layering, dependency direction, or moving responsibilities across modules, follow [references/architecture-rule.md](references/architecture-rule.md) to perform the review
|
|
||||||
- repositories abstraction: if the review scope contains table/model operations (e.g., `select(...)`, `session.execute(...)`, joins, CRUD) and is not under `api/repositories`, `api/core/repositories`, or `api/extensions/*/repositories/`, follow [references/repositories-rule.md](references/repositories-rule.md) to perform the review
|
|
||||||
- sqlalchemy patterns: if the review scope involves SQLAlchemy session/query usage, db transaction/crud usage, or raw SQL usage, follow [references/sqlalchemy-rule.md](references/sqlalchemy-rule.md) to perform the review
|
|
||||||
|
|
||||||
## General Review Rules
|
|
||||||
|
|
||||||
### 1. Security Review
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- SQL injection vulnerabilities
|
|
||||||
- Server-Side Request Forgery (SSRF)
|
|
||||||
- Command injection
|
|
||||||
- Insecure deserialization
|
|
||||||
- Hardcoded secrets/credentials
|
|
||||||
- Improper authentication/authorization
|
|
||||||
- Insecure direct object references
|
|
||||||
|
|
||||||
### 2. Performance Review
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- N+1 queries
|
|
||||||
- Missing database indexes
|
|
||||||
- Memory leaks
|
|
||||||
- Blocking operations in async code
|
|
||||||
- Missing caching opportunities
|
|
||||||
|
|
||||||
### 3. Code Quality Review
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- Code forward compatibility
|
|
||||||
- Code duplication (DRY violations)
|
|
||||||
- Functions doing too much (SRP violations)
|
|
||||||
- Deep nesting / complex conditionals
|
|
||||||
- Magic numbers/strings
|
|
||||||
- Poor naming
|
|
||||||
- Missing error handling
|
|
||||||
- Incomplete type coverage
|
|
||||||
|
|
||||||
### 4. Testing Review
|
|
||||||
|
|
||||||
Check for:
|
|
||||||
- Missing test coverage for new code
|
|
||||||
- Tests that don't test behavior
|
|
||||||
- Flaky test patterns
|
|
||||||
- Missing edge cases
|
|
||||||
|
|
||||||
## Required Output Format
|
|
||||||
|
|
||||||
When this skill invoked, the response must exactly follow one of the two templates:
|
|
||||||
|
|
||||||
### Template A (any findings)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Code Review Summary
|
|
||||||
|
|
||||||
Found <X> critical issues need to be fixed:
|
|
||||||
|
|
||||||
## 🔴 Critical (Must Fix)
|
|
||||||
|
|
||||||
### 1. <brief description of the issue>
|
|
||||||
|
|
||||||
FilePath: <path> line <line>
|
|
||||||
<relevant code snippet or pointer>
|
|
||||||
|
|
||||||
#### Explanation
|
|
||||||
|
|
||||||
<detailed explanation and references of the issue>
|
|
||||||
|
|
||||||
#### Suggested Fix
|
|
||||||
|
|
||||||
1. <brief description of suggested fix>
|
|
||||||
2. <code example> (optional, omit if not applicable)
|
|
||||||
|
|
||||||
---
|
|
||||||
... (repeat for each critical issue) ...
|
|
||||||
|
|
||||||
Found <Y> suggestions for improvement:
|
|
||||||
|
|
||||||
## 🟡 Suggestions (Should Consider)
|
|
||||||
|
|
||||||
### 1. <brief description of the suggestion>
|
|
||||||
|
|
||||||
FilePath: <path> line <line>
|
|
||||||
<relevant code snippet or pointer>
|
|
||||||
|
|
||||||
#### Explanation
|
|
||||||
|
|
||||||
<detailed explanation and references of the suggestion>
|
|
||||||
|
|
||||||
#### Suggested Fix
|
|
||||||
|
|
||||||
1. <brief description of suggested fix>
|
|
||||||
2. <code example> (optional, omit if not applicable)
|
|
||||||
|
|
||||||
---
|
|
||||||
... (repeat for each suggestion) ...
|
|
||||||
|
|
||||||
Found <Z> optional nits:
|
|
||||||
|
|
||||||
## 🟢 Nits (Optional)
|
|
||||||
### 1. <brief description of the nit>
|
|
||||||
|
|
||||||
FilePath: <path> line <line>
|
|
||||||
<relevant code snippet or pointer>
|
|
||||||
|
|
||||||
#### Explanation
|
|
||||||
|
|
||||||
<explanation and references of the optional nit>
|
|
||||||
|
|
||||||
#### Suggested Fix
|
|
||||||
|
|
||||||
- <minor suggestions>
|
|
||||||
|
|
||||||
---
|
|
||||||
... (repeat for each nits) ...
|
|
||||||
|
|
||||||
## ✅ What's Good
|
|
||||||
|
|
||||||
- <Positive feedback on good patterns>
|
|
||||||
```
|
|
||||||
|
|
||||||
- If there are no critical issues or suggestions or option nits or good points, just omit that section.
|
|
||||||
- If the issue number is more than 10, summarize as "Found 10+ critical issues/suggestions/optional nits" and only output the first 10 items.
|
|
||||||
- Don't compress the blank lines between sections; keep them as-is for readability.
|
|
||||||
- If there is any issue requires code changes, append a brief follow-up question to ask whether the user wants to apply the fix(es) after the structured output. For example: "Would you like me to use the Suggested fix(es) to address these issues?"
|
|
||||||
|
|
||||||
### Template B (no issues)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Code Review Summary
|
|
||||||
✅ No issues found.
|
|
||||||
```
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
# Rule Catalog — Architecture
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
- Covers: controller/service/core-domain/libs/model layering, dependency direction, responsibility placement, observability-friendly flow.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### Keep business logic out of controllers
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Controllers should parse input, call services, and return serialized responses. Business decisions inside controllers make behavior hard to reuse and test.
|
|
||||||
- Suggested fix: Move domain/business logic into the service or core/domain layer. Keep controller handlers thin and orchestration-focused.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
@bp.post("/apps/<app_id>/publish")
|
|
||||||
def publish_app(app_id: str):
|
|
||||||
payload = request.get_json() or {}
|
|
||||||
if payload.get("force") and current_user.role != "admin":
|
|
||||||
raise ValueError("only admin can force publish")
|
|
||||||
app = App.query.get(app_id)
|
|
||||||
app.status = "published"
|
|
||||||
db.session.commit()
|
|
||||||
return {"result": "ok"}
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
@bp.post("/apps/<app_id>/publish")
|
|
||||||
def publish_app(app_id: str):
|
|
||||||
payload = PublishRequest.model_validate(request.get_json() or {})
|
|
||||||
app_service.publish_app(app_id=app_id, force=payload.force, actor_id=current_user.id)
|
|
||||||
return {"result": "ok"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Preserve layer dependency direction
|
|
||||||
- Category: best practices
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Controllers may depend on services, and services may depend on core/domain abstractions. Reversing this direction (for example, core importing controller/web modules) creates cycles and leaks transport concerns into domain code.
|
|
||||||
- Suggested fix: Extract shared contracts into core/domain or service-level modules and make upper layers depend on lower, not the reverse.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# core/policy/publish_policy.py
|
|
||||||
from controllers.console.app import request_context
|
|
||||||
|
|
||||||
def can_publish() -> bool:
|
|
||||||
return request_context.current_user.is_admin
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# core/policy/publish_policy.py
|
|
||||||
def can_publish(role: str) -> bool:
|
|
||||||
return role == "admin"
|
|
||||||
|
|
||||||
# service layer adapts web/user context to domain input
|
|
||||||
allowed = can_publish(role=current_user.role)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Keep libs business-agnostic
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Modules under `api/libs/` should remain reusable, business-agnostic building blocks. They must not encode product/domain-specific rules, workflow orchestration, or business decisions.
|
|
||||||
- Suggested fix:
|
|
||||||
- If business logic appears in `api/libs/`, extract it into the appropriate `services/` or `core/` module and keep `libs` focused on generic, cross-cutting helpers.
|
|
||||||
- Keep `libs` dependencies clean: avoid importing service/controller/domain-specific modules into `api/libs/`.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# api/libs/conversation_filter.py
|
|
||||||
from services.conversation_service import ConversationService
|
|
||||||
|
|
||||||
def should_archive_conversation(conversation, tenant_id: str) -> bool:
|
|
||||||
# Domain policy and service dependency are leaking into libs.
|
|
||||||
service = ConversationService()
|
|
||||||
if service.has_paid_plan(tenant_id):
|
|
||||||
return conversation.idle_days > 90
|
|
||||||
return conversation.idle_days > 30
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# api/libs/datetime_utils.py (business-agnostic helper)
|
|
||||||
def older_than_days(idle_days: int, threshold_days: int) -> bool:
|
|
||||||
return idle_days > threshold_days
|
|
||||||
|
|
||||||
# services/conversation_service.py (business logic stays in service/core)
|
|
||||||
from libs.datetime_utils import older_than_days
|
|
||||||
|
|
||||||
def should_archive_conversation(conversation, tenant_id: str) -> bool:
|
|
||||||
threshold_days = 90 if has_paid_plan(tenant_id) else 30
|
|
||||||
return older_than_days(conversation.idle_days, threshold_days)
|
|
||||||
```
|
|
||||||
@ -1,157 +0,0 @@
|
|||||||
# Rule Catalog — DB Schema Design
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
- Covers: model/base inheritance, schema boundaries in model properties, tenant-aware schema design, index redundancy checks, dialect portability in models, and cross-database compatibility in migrations.
|
|
||||||
- Does NOT cover: session lifecycle, transaction boundaries, and query execution patterns (handled by `sqlalchemy-rule.md`).
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### Do not query other tables inside `@property`
|
|
||||||
- Category: [maintainability, performance]
|
|
||||||
- Severity: critical
|
|
||||||
- Description: A model `@property` must not open sessions or query other tables. This hides dependencies across models, tightly couples schema objects to data access, and can cause N+1 query explosions when iterating collections.
|
|
||||||
- Suggested fix:
|
|
||||||
- Keep model properties pure and local to already-loaded fields.
|
|
||||||
- Move cross-table data fetching to service/repository methods.
|
|
||||||
- For list/batch reads, fetch required related data explicitly (join/preload/bulk query) before rendering derived values.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
class Conversation(TypeBase):
|
|
||||||
__tablename__ = "conversations"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app_name(self) -> str:
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session:
|
|
||||||
app = session.execute(select(App).where(App.id == self.app_id)).scalar_one()
|
|
||||||
return app.name
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
class Conversation(TypeBase):
|
|
||||||
__tablename__ = "conversations"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_title(self) -> str:
|
|
||||||
return self.name or "Untitled"
|
|
||||||
|
|
||||||
|
|
||||||
# Service/repository layer performs explicit batch fetch for related App rows.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prefer including `tenant_id` in model definitions
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: suggestion
|
|
||||||
- Description: In multi-tenant domains, include `tenant_id` in schema definitions whenever the entity belongs to tenant-owned data. This improves data isolation safety and keeps future partitioning/sharding strategies practical as data volume grows.
|
|
||||||
- Suggested fix:
|
|
||||||
- Add a `tenant_id` column and ensure related unique/index constraints include tenant dimension when applicable.
|
|
||||||
- Propagate `tenant_id` through service/repository contracts to keep access paths tenant-aware.
|
|
||||||
- Exception: if a table is explicitly designed as non-tenant-scoped global metadata, document that design decision clearly.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
from sqlalchemy.orm import Mapped
|
|
||||||
|
|
||||||
class Dataset(TypeBase):
|
|
||||||
__tablename__ = "datasets"
|
|
||||||
id: Mapped[str] = mapped_column(StringUUID, primary_key=True)
|
|
||||||
name: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
from sqlalchemy.orm import Mapped
|
|
||||||
|
|
||||||
class Dataset(TypeBase):
|
|
||||||
__tablename__ = "datasets"
|
|
||||||
id: Mapped[str] = mapped_column(StringUUID, primary_key=True)
|
|
||||||
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False, index=True)
|
|
||||||
name: Mapped[str] = mapped_column(sa.String(255), nullable=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Detect and avoid duplicate/redundant indexes
|
|
||||||
- Category: performance
|
|
||||||
- Severity: suggestion
|
|
||||||
- Description: Review index definitions for leftmost-prefix redundancy. For example, index `(a, b, c)` can safely cover most lookups for `(a, b)`. Keeping both may increase write overhead and can mislead the optimizer into suboptimal execution plans.
|
|
||||||
- Suggested fix:
|
|
||||||
- Before adding an index, compare against existing composite indexes by leftmost-prefix rules.
|
|
||||||
- Drop or avoid creating redundant prefixes unless there is a proven query-pattern need.
|
|
||||||
- Apply the same review standard in both model `__table_args__` and migration index DDL.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
__table_args__ = (
|
|
||||||
sa.Index("idx_msg_tenant_app", "tenant_id", "app_id"),
|
|
||||||
sa.Index("idx_msg_tenant_app_created", "tenant_id", "app_id", "created_at"),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
__table_args__ = (
|
|
||||||
# Keep the wider index unless profiling proves a dedicated short index is needed.
|
|
||||||
sa.Index("idx_msg_tenant_app_created", "tenant_id", "app_id", "created_at"),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Avoid PostgreSQL-only dialect usage in models; wrap in `models.types`
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Model/schema definitions should avoid PostgreSQL-only constructs directly in business models. When database-specific behavior is required, encapsulate it in `api/models/types.py` using both PostgreSQL and MySQL dialect implementations, then consume that abstraction from model code.
|
|
||||||
- Suggested fix:
|
|
||||||
- Do not directly place dialect-only types/operators in model columns when a portable wrapper can be used.
|
|
||||||
- Add or extend wrappers in `models.types` (for example, `AdjustedJSON`, `LongText`, `BinaryData`) to normalize behavior across PostgreSQL and MySQL.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
|
||||||
from sqlalchemy.orm import Mapped
|
|
||||||
|
|
||||||
class ToolConfig(TypeBase):
|
|
||||||
__tablename__ = "tool_configs"
|
|
||||||
config: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
from sqlalchemy.orm import Mapped
|
|
||||||
|
|
||||||
from models.types import AdjustedJSON
|
|
||||||
|
|
||||||
class ToolConfig(TypeBase):
|
|
||||||
__tablename__ = "tool_configs"
|
|
||||||
config: Mapped[dict] = mapped_column(AdjustedJSON(), nullable=False)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guard migration incompatibilities with dialect checks and shared types
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Migration scripts under `api/migrations/versions/` must account for PostgreSQL/MySQL incompatibilities explicitly. For dialect-sensitive DDL or defaults, branch on the active dialect (for example, `conn.dialect.name == "postgresql"`), and prefer reusable compatibility abstractions from `models.types` where applicable.
|
|
||||||
- Suggested fix:
|
|
||||||
- In migration upgrades/downgrades, bind connection and branch by dialect for incompatible SQL fragments.
|
|
||||||
- Reuse `models.types` wrappers in column definitions when that keeps behavior aligned with runtime models.
|
|
||||||
- Avoid one-dialect-only migration logic unless there is a documented, deliberate compatibility exception.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
with op.batch_alter_table("dataset_keyword_tables") as batch_op:
|
|
||||||
batch_op.add_column(
|
|
||||||
sa.Column(
|
|
||||||
"data_source_type",
|
|
||||||
sa.String(255),
|
|
||||||
server_default=sa.text("'database'::character varying"),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
def _is_pg(conn) -> bool:
|
|
||||||
return conn.dialect.name == "postgresql"
|
|
||||||
|
|
||||||
|
|
||||||
conn = op.get_bind()
|
|
||||||
default_expr = sa.text("'database'::character varying") if _is_pg(conn) else sa.text("'database'")
|
|
||||||
|
|
||||||
with op.batch_alter_table("dataset_keyword_tables") as batch_op:
|
|
||||||
batch_op.add_column(
|
|
||||||
sa.Column("data_source_type", sa.String(255), server_default=default_expr, nullable=False)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
# Rule Catalog - Repositories Abstraction
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
- Covers: when to reuse existing repository abstractions, when to introduce new repositories, and how to preserve dependency direction between service/core and infrastructure implementations.
|
|
||||||
- Does NOT cover: SQLAlchemy session lifecycle and query-shape specifics (handled by `sqlalchemy-rule.md`), and table schema/migration design (handled by `db-schema-rule.md`).
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### Introduce repositories abstraction
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: suggestion
|
|
||||||
- Description: If a table/model already has a repository abstraction, all reads/writes/queries for that table should use the existing repository. If no repository exists, introduce one only when complexity justifies it, such as large/high-volume tables, repeated complex query logic, or likely storage-strategy variation.
|
|
||||||
- Suggested fix:
|
|
||||||
- First check `api/repositories`, `api/core/repositories`, and `api/extensions/*/repositories/` to verify whether the table/model already has a repository abstraction. If it exists, route all operations through it and add missing repository methods instead of bypassing it with ad-hoc SQLAlchemy access.
|
|
||||||
- If no repository exists, add one only when complexity warrants it (for example, repeated complex queries, large data domains, or multiple storage strategies), while preserving dependency direction (service/core depends on abstraction; infra provides implementation).
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# Existing repository is ignored and service uses ad-hoc table queries.
|
|
||||||
class AppService:
|
|
||||||
def archive_app(self, app_id: str, tenant_id: str) -> None:
|
|
||||||
app = self.session.execute(
|
|
||||||
select(App).where(App.id == app_id, App.tenant_id == tenant_id)
|
|
||||||
).scalar_one()
|
|
||||||
app.archived = True
|
|
||||||
self.session.commit()
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# Case A: Existing repository must be reused for all table operations.
|
|
||||||
class AppService:
|
|
||||||
def archive_app(self, app_id: str, tenant_id: str) -> None:
|
|
||||||
app = self.app_repo.get_by_id(app_id=app_id, tenant_id=tenant_id)
|
|
||||||
app.archived = True
|
|
||||||
self.app_repo.save(app)
|
|
||||||
|
|
||||||
# If the query is missing, extend the existing abstraction.
|
|
||||||
active_apps = self.app_repo.list_active_for_tenant(tenant_id=tenant_id)
|
|
||||||
```
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# No repository exists, but large-domain query logic is scattered in service code.
|
|
||||||
class ConversationService:
|
|
||||||
def list_recent_for_app(self, app_id: str, tenant_id: str, limit: int) -> list[Conversation]:
|
|
||||||
...
|
|
||||||
# many filters/joins/pagination variants duplicated across services
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# Case B: Introduce repository for large/complex domains or storage variation.
|
|
||||||
class ConversationRepository(Protocol):
|
|
||||||
def list_recent_for_app(self, app_id: str, tenant_id: str, limit: int) -> list[Conversation]: ...
|
|
||||||
|
|
||||||
class SqlAlchemyConversationRepository:
|
|
||||||
def list_recent_for_app(self, app_id: str, tenant_id: str, limit: int) -> list[Conversation]:
|
|
||||||
...
|
|
||||||
|
|
||||||
class ConversationService:
|
|
||||||
def __init__(self, conversation_repo: ConversationRepository):
|
|
||||||
self.conversation_repo = conversation_repo
|
|
||||||
```
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
# Rule Catalog — SQLAlchemy Patterns
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
- Covers: SQLAlchemy session and transaction lifecycle, query construction, tenant scoping, raw SQL boundaries, and write-path concurrency safeguards.
|
|
||||||
- Does NOT cover: table/model schema and migration design details (handled by `db-schema-rule.md`).
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
### Use Session context manager with explicit transaction control behavior
|
|
||||||
- Category: best practices
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Session and transaction lifecycle must be explicit and bounded on write paths. Missing commits can silently drop intended updates, while ad-hoc or long-lived transactions increase contention, lock duration, and deadlock risk.
|
|
||||||
- Suggested fix:
|
|
||||||
- Use **explicit `session.commit()`** after completing a related write unit.
|
|
||||||
- Or use **`session.begin()` context manager** for automatic commit/rollback on a scoped block.
|
|
||||||
- Keep transaction windows short: avoid network I/O, heavy computation, or unrelated work inside the transaction.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# Missing commit: write may never be persisted.
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session:
|
|
||||||
run = session.get(WorkflowRun, run_id)
|
|
||||||
run.status = "cancelled"
|
|
||||||
|
|
||||||
# Long transaction: external I/O inside a DB transaction.
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session, session.begin():
|
|
||||||
run = session.get(WorkflowRun, run_id)
|
|
||||||
run.status = "cancelled"
|
|
||||||
call_external_api()
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# Option 1: explicit commit.
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session:
|
|
||||||
run = session.get(WorkflowRun, run_id)
|
|
||||||
run.status = "cancelled"
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# Option 2: scoped transaction with automatic commit/rollback.
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session, session.begin():
|
|
||||||
run = session.get(WorkflowRun, run_id)
|
|
||||||
run.status = "cancelled"
|
|
||||||
|
|
||||||
# Keep non-DB work outside transaction scope.
|
|
||||||
call_external_api()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enforce tenant_id scoping on shared-resource queries
|
|
||||||
- Category: security
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Reads and writes against shared tables must be scoped by `tenant_id` to prevent cross-tenant data leakage or corruption.
|
|
||||||
- Suggested fix: Add `tenant_id` predicate to all tenant-owned entity queries and propagate tenant context through service/repository interfaces.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
stmt = select(Workflow).where(Workflow.id == workflow_id)
|
|
||||||
workflow = session.execute(stmt).scalar_one_or_none()
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
stmt = select(Workflow).where(
|
|
||||||
Workflow.id == workflow_id,
|
|
||||||
Workflow.tenant_id == tenant_id,
|
|
||||||
)
|
|
||||||
workflow = session.execute(stmt).scalar_one_or_none()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prefer SQLAlchemy expressions over raw SQL by default
|
|
||||||
- Category: maintainability
|
|
||||||
- Severity: suggestion
|
|
||||||
- Description: Raw SQL should be exceptional. ORM/Core expressions are easier to evolve, safer to compose, and more consistent with the codebase.
|
|
||||||
- Suggested fix: Rewrite straightforward raw SQL into SQLAlchemy `select/update/delete` expressions; keep raw SQL only when required by clear technical constraints.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
row = session.execute(
|
|
||||||
text("SELECT * FROM workflows WHERE id = :id AND tenant_id = :tenant_id"),
|
|
||||||
{"id": workflow_id, "tenant_id": tenant_id},
|
|
||||||
).first()
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
stmt = select(Workflow).where(
|
|
||||||
Workflow.id == workflow_id,
|
|
||||||
Workflow.tenant_id == tenant_id,
|
|
||||||
)
|
|
||||||
row = session.execute(stmt).scalar_one_or_none()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Protect write paths with concurrency safeguards
|
|
||||||
- Category: quality
|
|
||||||
- Severity: critical
|
|
||||||
- Description: Multi-writer paths without explicit concurrency control can silently overwrite data. Choose the safeguard based on contention level, lock scope, and throughput cost instead of defaulting to one strategy.
|
|
||||||
- Suggested fix:
|
|
||||||
- **Optimistic locking**: Use when contention is usually low and retries are acceptable. Add a version (or updated_at) guard in `WHERE` and treat `rowcount == 0` as a conflict.
|
|
||||||
- **Redis distributed lock**: Use when the critical section spans multiple steps/processes (or includes non-DB side effects) and you need cross-worker mutual exclusion.
|
|
||||||
- **SELECT ... FOR UPDATE**: Use when contention is high on the same rows and strict in-transaction serialization is required. Keep transactions short to reduce lock wait/deadlock risk.
|
|
||||||
- In all cases, scope by `tenant_id` and verify affected row counts for conditional writes.
|
|
||||||
- Example:
|
|
||||||
- Bad:
|
|
||||||
```python
|
|
||||||
# No tenant scope, no conflict detection, and no lock on a contested write path.
|
|
||||||
session.execute(update(WorkflowRun).where(WorkflowRun.id == run_id).values(status="cancelled"))
|
|
||||||
session.commit() # silently overwrites concurrent updates
|
|
||||||
```
|
|
||||||
- Good:
|
|
||||||
```python
|
|
||||||
# 1) Optimistic lock (low contention, retry on conflict)
|
|
||||||
result = session.execute(
|
|
||||||
update(WorkflowRun)
|
|
||||||
.where(
|
|
||||||
WorkflowRun.id == run_id,
|
|
||||||
WorkflowRun.tenant_id == tenant_id,
|
|
||||||
WorkflowRun.version == expected_version,
|
|
||||||
)
|
|
||||||
.values(status="cancelled", version=WorkflowRun.version + 1)
|
|
||||||
)
|
|
||||||
if result.rowcount == 0:
|
|
||||||
raise WorkflowStateConflictError("stale version, retry")
|
|
||||||
|
|
||||||
# 2) Redis distributed lock (cross-worker critical section)
|
|
||||||
lock_name = f"workflow_run_lock:{tenant_id}:{run_id}"
|
|
||||||
with redis_client.lock(lock_name, timeout=20):
|
|
||||||
session.execute(
|
|
||||||
update(WorkflowRun)
|
|
||||||
.where(WorkflowRun.id == run_id, WorkflowRun.tenant_id == tenant_id)
|
|
||||||
.values(status="cancelled")
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
# 3) Pessimistic lock with SELECT ... FOR UPDATE (high contention)
|
|
||||||
run = session.execute(
|
|
||||||
select(WorkflowRun)
|
|
||||||
.where(WorkflowRun.id == run_id, WorkflowRun.tenant_id == tenant_id)
|
|
||||||
.with_for_update()
|
|
||||||
).scalar_one()
|
|
||||||
run.status = "cancelled"
|
|
||||||
session.commit()
|
|
||||||
```
|
|
||||||
@ -63,7 +63,7 @@ pnpm analyze-component <path> --json
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// ❌ Before: Complex state logic in component
|
// ❌ Before: Complex state logic in component
|
||||||
function Configuration() {
|
const Configuration: FC = () => {
|
||||||
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
|
||||||
function Configuration() {
|
const Configuration: FC = () => {
|
||||||
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
const { modelConfig, setModelConfig } = useModelConfig(appId)
|
||||||
return <div>...</div>
|
return <div>...</div>
|
||||||
}
|
}
|
||||||
@ -187,10 +187,53 @@ const Template = useMemo(() => {
|
|||||||
|
|
||||||
**When**: Component directly handles API calls, data transformation, or complex async operations.
|
**When**: Component directly handles API calls, data transformation, or complex async operations.
|
||||||
|
|
||||||
**Dify Convention**:
|
**Dify Convention**: Use `@tanstack/react-query` hooks from `web/service/use-*.ts` or create custom data hooks.
|
||||||
- This skill is for component decomposition, not query/mutation design.
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
```typescript
|
||||||
- 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.
|
// ❌ Before: API logic in component
|
||||||
|
const MCPServiceCard = () => {
|
||||||
|
const [basicAppConfig, setBasicAppConfig] = useState({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isBasicApp && appId) {
|
||||||
|
(async () => {
|
||||||
|
const res = await fetchAppDetail({ url: '/apps', id: appId })
|
||||||
|
setBasicAppConfig(res?.model_config || {})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [appId, isBasicApp])
|
||||||
|
|
||||||
|
// More API-related logic...
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After: Extract to data hook using React Query
|
||||||
|
// use-app-config.ts
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { get } from '@/service/base'
|
||||||
|
|
||||||
|
const NAME_SPACE = 'appConfig'
|
||||||
|
|
||||||
|
export const useAppConfig = (appId: string, isBasicApp: boolean) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: isBasicApp && !!appId,
|
||||||
|
queryKey: [NAME_SPACE, 'detail', appId],
|
||||||
|
queryFn: () => get<AppDetailResponse>(`/apps/${appId}`),
|
||||||
|
select: data => data?.model_config || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component becomes cleaner
|
||||||
|
const MCPServiceCard = () => {
|
||||||
|
const { data: config, isLoading } = useAppConfig(appId, isBasicApp)
|
||||||
|
// UI only
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**React Query Best Practices in Dify**:
|
||||||
|
- Define `NAME_SPACE` for query key organization
|
||||||
|
- Use `enabled` option for conditional fetching
|
||||||
|
- Use `select` for data transformation
|
||||||
|
- Export invalidation hooks: `useInvalidXxx`
|
||||||
|
|
||||||
**Dify Examples**:
|
**Dify Examples**:
|
||||||
- `web/service/use-workflow.ts`
|
- `web/service/use-workflow.ts`
|
||||||
@ -365,7 +408,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 │
|
│ 3. Run: pnpm type-check:tsgo │
|
||||||
│ 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,10 +60,8 @@ 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, ComponentType<TemplateProps>>> = {
|
const TEMPLATE_MAP: Record<AppModeEnum, Record<string, FC<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
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConfigurationHeader({
|
const ConfigurationHeader: FC<ConfigurationHeaderProps> = ({
|
||||||
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
|
||||||
function AppInfoExpanded({ appDetail, onAction }: AppInfoViewProps) {
|
const AppInfoExpanded: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
||||||
return (
|
return (
|
||||||
<div className="expanded">
|
<div className="expanded">
|
||||||
{/* Clean, focused expanded view */}
|
{/* Clean, focused expanded view */}
|
||||||
@ -144,7 +144,7 @@ function AppInfoExpanded({ appDetail, onAction }: AppInfoViewProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppInfoCollapsed({ appDetail, onAction }: AppInfoViewProps) {
|
const AppInfoCollapsed: FC<AppInfoViewProps> = ({ appDetail, onAction }) => {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppInfoModals({
|
const AppInfoModals: FC<AppInfoModalsProps> = ({
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
function OperationItem({ operation, onAction }: OperationItemProps) {
|
const OperationItem: FC<OperationItemProps> = ({ operation, onAction }) => {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
function Child({ value, onChange, onSubmit }: ChildProps) {
|
const Child: FC<ChildProps> = ({ value, onChange, onSubmit }) => {
|
||||||
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
|
||||||
function Configuration() {
|
const Configuration: FC = () => {
|
||||||
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
|
||||||
function Configuration() {
|
const Configuration: FC = () => {
|
||||||
const {
|
const {
|
||||||
modelConfig,
|
modelConfig,
|
||||||
setModelConfig,
|
setModelConfig,
|
||||||
@ -155,12 +155,48 @@ function Configuration() {
|
|||||||
|
|
||||||
## Common Hook Patterns in Dify
|
## Common Hook Patterns in Dify
|
||||||
|
|
||||||
### 1. Data Fetching / Mutation Hooks
|
### 1. Data Fetching Hook (React Query)
|
||||||
|
|
||||||
When hook extraction touches query or mutation code, do not use this reference as the source of truth for data-layer patterns.
|
```typescript
|
||||||
|
// Pattern: Use @tanstack/react-query for data fetching
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { get } from '@/service/base'
|
||||||
|
import { useInvalid } from '@/service/use-base'
|
||||||
|
|
||||||
- Do not introduce deprecated `useInvalid` / `useReset`.
|
const NAME_SPACE = 'appConfig'
|
||||||
- Do not extract thin passthrough `useQuery` hooks; only extract orchestration hooks.
|
|
||||||
|
// Query keys for cache management
|
||||||
|
export const appConfigQueryKeys = {
|
||||||
|
detail: (appId: string) => [NAME_SPACE, 'detail', appId] as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main data hook
|
||||||
|
export const useAppConfig = (appId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!appId,
|
||||||
|
queryKey: appConfigQueryKeys.detail(appId),
|
||||||
|
queryFn: () => get<AppDetailResponse>(`/apps/${appId}`),
|
||||||
|
select: data => data?.model_config || null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidation hook for refreshing data
|
||||||
|
export const useInvalidAppConfig = () => {
|
||||||
|
return useInvalid([NAME_SPACE])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in component
|
||||||
|
const Component = () => {
|
||||||
|
const { data: config, isLoading, error, refetch } = useAppConfig(appId)
|
||||||
|
const invalidAppConfig = useInvalidAppConfig()
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
invalidAppConfig() // Invalidates cache and triggers refetch
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>...</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 2. Form State Hook
|
### 2. Form State Hook
|
||||||
|
|
||||||
|
|||||||
@ -1,79 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
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/."
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
# 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?
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
# 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 stability
|
## Complex prop memoization
|
||||||
|
|
||||||
IsUrgent: False
|
IsUrgent: True
|
||||||
Category: Performance
|
Category: Performance
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
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.
|
Wrap complex prop values (objects, arrays, maps) in `useMemo` prior to passing them into child components to guarantee stable references and prevent unnecessary renders.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Risky:
|
Wrong:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<HeavyComp
|
<HeavyComp
|
||||||
@ -31,7 +31,7 @@ Risky:
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
Better when stable identity matters:
|
Right:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const config = useMemo(() => ({
|
const config = useMemo(() => ({
|
||||||
|
|||||||
@ -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 Codex to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
This skill enables Claude 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,27 +24,35 @@ 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 (Cucumber + Playwright under `e2e/`)
|
- User is asking about E2E tests (Playwright/Cypress)
|
||||||
- User is only asking conceptual questions without code context
|
- User is only asking conceptual questions without code context
|
||||||
|
|
||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### Key Commands
|
### Tech Stack
|
||||||
|
|
||||||
Run these commands from `web/`. From the repository root, prefix them with `pnpm -C web`.
|
| 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
|
||||||
|
|
||||||
```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>
|
||||||
@ -55,8 +63,7 @@ pnpm analyze-component <path> --review
|
|||||||
|
|
||||||
### File Naming
|
### File Naming
|
||||||
|
|
||||||
- Test files: `ComponentName.spec.tsx` inside a same-level `__tests__/` directory
|
- Test files: `ComponentName.spec.tsx` (same directory as component)
|
||||||
- Placement rule: Component, hook, and utility tests must live in a sibling `__tests__/` folder at the same level as the source under test. For example, `foo/index.tsx` maps to `foo/__tests__/index.spec.tsx`, and `foo/bar.ts` maps to `foo/__tests__/bar.spec.ts`.
|
|
||||||
- Integration tests: `web/__tests__/` directory
|
- Integration tests: `web/__tests__/` directory
|
||||||
|
|
||||||
## Test Structure Template
|
## Test Structure Template
|
||||||
@ -192,21 +199,11 @@ 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/*`) or dify-ui primitives (`@langgenius/dify-ui/*`)
|
- ❌ **DO NOT mock** base components (`@/app/components/base/*`)
|
||||||
- ❌ **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.
|
||||||
|
|
||||||
### `nuqs` Query State Testing (Required for URL State Hooks)
|
|
||||||
|
|
||||||
When a component or hook uses `useQueryState` / `useQueryStates`:
|
|
||||||
|
|
||||||
- ✅ Use `NuqsTestingAdapter` (prefer shared helpers in `web/test/nuqs-testing.tsx`)
|
|
||||||
- ✅ Assert URL synchronization via `onUrlUpdate` (`searchParams`, `options.history`)
|
|
||||||
- ✅ For custom parsers (`createParser`), keep `parse` and `serialize` bijective and add round-trip edge cases (`%2F`, `%25`, spaces, legacy encoded values)
|
|
||||||
- ✅ Verify default-clearing behavior (default values should be removed from URL when applicable)
|
|
||||||
- ⚠️ Only mock `nuqs` directly when URL behavior is explicitly out of scope for the test
|
|
||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
### 1. AAA Pattern (Arrange-Act-Assert)
|
### 1. AAA Pattern (Arrange-Act-Assert)
|
||||||
@ -220,10 +217,7 @@ 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` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`)
|
- Use semantic queries (getByRole, getByLabelText)
|
||||||
- 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:
|
||||||
|
|
||||||
@ -320,12 +314,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/radio/__tests__/index.spec.tsx` - Component tests
|
- `web/app/components/base/button/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/vite.config.ts` - Vite/Vitest configuration
|
- `web/vitest.config.ts` - 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.
|
||||||
|
|||||||
@ -41,7 +41,7 @@ import userEvent from '@testing-library/user-event'
|
|||||||
// Router (if component uses useRouter, usePathname, useSearchParams)
|
// Router (if component uses useRouter, usePathname, useSearchParams)
|
||||||
// WHY: Isolates tests from Next.js routing, enables testing navigation behavior
|
// WHY: Isolates tests from Next.js routing, enables testing navigation behavior
|
||||||
// const mockPush = vi.fn()
|
// const mockPush = vi.fn()
|
||||||
// vi.mock('@/next/navigation', () => ({
|
// vi.mock('next/navigation', () => ({
|
||||||
// useRouter: () => ({ push: mockPush }),
|
// useRouter: () => ({ push: mockPush }),
|
||||||
// usePathname: () => '/test-path',
|
// usePathname: () => '/test-path',
|
||||||
// }))
|
// }))
|
||||||
|
|||||||
@ -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 or dify-ui primitives** (base `Loading`, `Input`, `Badge`; dify-ui `Button`, `Tooltip`, `Dialog`, etc.)
|
- [ ] **DO NOT mock base components** (`Loading`, `Button`, `Tooltip`, 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,16 +73,13 @@ Use this checklist when generating or reviewing tests for Dify frontend componen
|
|||||||
|
|
||||||
### Mocks
|
### Mocks
|
||||||
|
|
||||||
- [ ] **DO NOT mock base components or dify-ui primitives** (`@/app/components/base/*` or `@langgenius/dify-ui/*`)
|
- [ ] **DO NOT mock base components** (`@/app/components/base/*`)
|
||||||
- [ ] `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
|
||||||
- [ ] Router mocks match actual Next.js API
|
- [ ] Router mocks match actual Next.js API
|
||||||
- [ ] Mocks reflect actual component conditional behavior
|
- [ ] Mocks reflect actual component conditional behavior
|
||||||
- [ ] Only mock: API services, complex context providers, third-party libs
|
- [ ] Only mock: API services, complex context providers, third-party libs
|
||||||
- [ ] For `nuqs` URL-state tests, wrap with `NuqsTestingAdapter` (prefer `web/test/nuqs-testing.tsx`)
|
|
||||||
- [ ] For `nuqs` URL-state tests, assert `onUrlUpdate` payload (`searchParams`, `options.history`)
|
|
||||||
- [ ] If custom `nuqs` parser exists, add round-trip tests for encoded edge cases (`%2F`, `%25`, spaces, legacy encoded values)
|
|
||||||
|
|
||||||
### Queries
|
### Queries
|
||||||
|
|
||||||
@ -127,7 +124,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`
|
- [ ] Run `pnpm type-check:tsgo`
|
||||||
|
|
||||||
## Common Issues to Watch
|
## Common Issues to Watch
|
||||||
|
|
||||||
|
|||||||
@ -2,27 +2,29 @@
|
|||||||
|
|
||||||
## ⚠️ Important: What NOT to Mock
|
## ⚠️ Important: What NOT to Mock
|
||||||
|
|
||||||
### DO NOT Mock Base Components or dify-ui Primitives
|
### DO NOT Mock Base Components
|
||||||
|
|
||||||
**Never mock components from `@/app/components/base/` or from `@langgenius/dify-ui/*`** such as:
|
**Never mock components from `@/app/components/base/`** such as:
|
||||||
|
|
||||||
- Legacy base (`@/app/components/base/*`): `Loading`, `Spinner`, `Input`, `Badge`, `Tag`
|
- `Loading`, `Spinner`
|
||||||
- dify-ui primitives (`@langgenius/dify-ui/*`): `Button`, `Tooltip`, `Dialog`, `Popover`, `DropdownMenu`, `ContextMenu`, `Select`, `AlertDialog`, `Toast`
|
- `Button`, `Input`, `Select`
|
||||||
|
- `Tooltip`, `Modal`, `Dropdown`
|
||||||
|
- `Icon`, `Badge`, `Tag`
|
||||||
|
|
||||||
**Why?**
|
**Why?**
|
||||||
|
|
||||||
- These components have their own dedicated tests
|
- Base components will 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 or dify-ui primitives
|
// ❌ WRONG: Don't mock base components
|
||||||
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
vi.mock('@/app/components/base/loading', () => () => <div>Loading</div>)
|
||||||
vi.mock('@langgenius/dify-ui/button', () => ({ Button: ({ children }: any) => <button>{children}</button> }))
|
vi.mock('@/app/components/base/button', () => ({ children }: any) => <button>{children}</button>)
|
||||||
|
|
||||||
// ✅ CORRECT: Import and use the real components
|
// ✅ CORRECT: Import and use real base components
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import Button from '@/app/components/base/button'
|
||||||
// They will render normally in tests
|
// They will render normally in tests
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -56,7 +58,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`, `zustand`, clipboard, FloatingPortal, Monaco, localStorage`) |
|
| `web/vitest.setup.ts` | Global mocks shared by all tests (`react-i18next`, `next/image`, `zustand`) |
|
||||||
| `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()` |
|
||||||
@ -123,31 +125,6 @@ describe('Component', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.1 `nuqs` Query State (Preferred: Testing Adapter)
|
|
||||||
|
|
||||||
For tests that validate URL query behavior, use `NuqsTestingAdapter` instead of mocking `nuqs` directly.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { renderHookWithNuqs } from '@/test/nuqs-testing'
|
|
||||||
|
|
||||||
it('should sync query to URL with push history', async () => {
|
|
||||||
const { result, onUrlUpdate } = renderHookWithNuqs(() => useMyQueryState(), {
|
|
||||||
searchParams: '?page=1',
|
|
||||||
})
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setQuery({ page: 2 })
|
|
||||||
})
|
|
||||||
|
|
||||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
|
||||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0]
|
|
||||||
expect(update.options.history).toBe('push')
|
|
||||||
expect(update.searchParams.get('page')).toBe('2')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
Use direct `vi.mock('nuqs')` only when URL synchronization is intentionally out of scope.
|
|
||||||
|
|
||||||
### 3. Portal Components (with Shared State)
|
### 3. Portal Components (with Shared State)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -216,21 +193,28 @@ describe('Component', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. HTTP and `fetch` Mocking
|
### 5. HTTP Mocking with Nock
|
||||||
|
|
||||||
```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', () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks()
|
nock.cleanAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display repo info', async () => {
|
it('should display repo info', async () => {
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
mockGithubApi(200, { name: 'dify', stars: 1000 })
|
||||||
new Response(JSON.stringify({ name: 'dify', stars: 1000 }), {
|
|
||||||
status: 200,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@ -240,12 +224,7 @@ describe('GithubComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle API error', async () => {
|
it('should handle API error', async () => {
|
||||||
vi.mocked(globalThis.fetch).mockResolvedValueOnce(
|
mockGithubApi(500, { message: 'Server error' })
|
||||||
new Response(JSON.stringify({ message: 'Server error' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
render(<GithubComponent />)
|
render(<GithubComponent />)
|
||||||
|
|
||||||
@ -256,8 +235,6 @@ 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
|
||||||
@ -317,7 +294,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ✅ DO
|
### ✅ DO
|
||||||
|
|
||||||
1. **Use real base components and dify-ui primitives** - Import from `@/app/components/base/` or `@langgenius/dify-ui/*` directly
|
1. **Use real base components** - Import from `@/app/components/base/` 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`
|
||||||
@ -328,11 +305,11 @@ const renderWithQueryClient = (ui: React.ReactElement) => {
|
|||||||
|
|
||||||
### ❌ DON'T
|
### ❌ DON'T
|
||||||
|
|
||||||
1. **Don't mock base components or dify-ui primitives** (`Loading`, `Input`, `Button`, `Tooltip`, `Dialog`, etc.)
|
1. **Don't mock base components** (`Loading`, `Button`, `Tooltip`, 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 leave HTTP mocks or service mock state leaking between tests
|
1. Don't forget to clean up nock after each test
|
||||||
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
|
||||||
@ -340,7 +317,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/* or @langgenius/dify-ui/*?
|
├─ Is it from @/app/components/base/*?
|
||||||
│ └─ 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 Codex's Todo Feature
|
## Integration with Claude's Todo Feature
|
||||||
|
|
||||||
When using Codex for multi-file testing:
|
When using Claude for multi-file testing:
|
||||||
|
|
||||||
1. **Create a todo list** before starting
|
1. **Ask Claude to create a todo list** before starting
|
||||||
1. **Process one file at a time**
|
1. **Request one file at a time** or ensure Claude processes incrementally
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
- Use Tailwind CSS v4.1+ rules via the `tailwind-css-rules` skill. Prefer v4 utilities, `gap`, `text-size/line-height`, `min-h-dvh`, and avoid deprecated utilities and `@apply`.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
46
.agents/skills/orpc-contract-first/SKILL.md
Normal file
46
.agents/skills/orpc-contract-first/SKILL.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: orpc-contract-first
|
||||||
|
description: Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.
|
||||||
|
---
|
||||||
|
|
||||||
|
# oRPC Contract-First Development
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
web/contract/
|
||||||
|
├── base.ts # Base contract (inputStructure: 'detailed')
|
||||||
|
├── router.ts # Router composition & type exports
|
||||||
|
├── marketplace.ts # Marketplace contracts
|
||||||
|
└── console/ # Console contracts by domain
|
||||||
|
├── system.ts
|
||||||
|
└── billing.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Create contract** in `web/contract/console/{domain}.ts`
|
||||||
|
- Import `base` from `../base` and `type` from `@orpc/contract`
|
||||||
|
- Define route with `path`, `method`, `input`, `output`
|
||||||
|
|
||||||
|
2. **Register in router** at `web/contract/router.ts`
|
||||||
|
- Import directly from domain file (no barrel files)
|
||||||
|
- Nest by API prefix: `billing: { invoices, bindPartnerStack }`
|
||||||
|
|
||||||
|
3. **Create hooks** in `web/service/use-{domain}.ts`
|
||||||
|
- Use `consoleQuery.{group}.{contract}.queryKey()` for query keys
|
||||||
|
- Use `consoleClient.{group}.{contract}()` for API calls
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
- **Input structure**: Always use `{ params, query?, body? }` format
|
||||||
|
- **Path params**: Use `{paramName}` in path, match in `params` object
|
||||||
|
- **Router nesting**: Group by API prefix (e.g., `/billing/*` → `billing: {}`)
|
||||||
|
- **No barrel files**: Import directly from specific files
|
||||||
|
- **Types**: Import from `@/types/`, use `type<T>()` helper
|
||||||
|
|
||||||
|
## Type Export
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
|
||||||
|
```
|
||||||
@ -1,367 +0,0 @@
|
|||||||
---
|
|
||||||
name: tailwind-css-rules
|
|
||||||
description: Tailwind CSS v4.1+ rules and best practices. Use when writing, reviewing, refactoring, or upgrading Tailwind CSS classes and styles, especially v4 utility migrations, layout spacing, typography, responsive variants, dark mode, gradients, CSS variables, and component styling.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Tailwind CSS Rules and Best Practices
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
- **Always use Tailwind CSS v4.1+** - Ensure the codebase is using the latest version
|
|
||||||
- **Do not use deprecated or removed utilities** - ALWAYS use the replacement
|
|
||||||
- **Never use `@apply`** - Use CSS variables, the `--spacing()` function, or framework components instead
|
|
||||||
- **Check for redundant classes** - Remove any classes that aren't necessary
|
|
||||||
- **Group elements logically** to simplify responsive tweaks later
|
|
||||||
|
|
||||||
## Upgrading to Tailwind CSS v4
|
|
||||||
|
|
||||||
### Before Upgrading
|
|
||||||
|
|
||||||
- **Always read the upgrade documentation first** - Read https://tailwindcss.com/docs/upgrade-guide and https://tailwindcss.com/blog/tailwindcss-v4 before starting an upgrade.
|
|
||||||
- Ensure the git repository is in a clean state before starting
|
|
||||||
|
|
||||||
### Upgrade Process
|
|
||||||
|
|
||||||
1. Run the upgrade command: `npx @tailwindcss/upgrade@latest` for both major and minor updates
|
|
||||||
2. The tool will convert JavaScript config files to the new CSS format
|
|
||||||
3. Review all changes extensively to clean up any false positives
|
|
||||||
4. Test thoroughly across your application
|
|
||||||
|
|
||||||
## Breaking Changes Reference
|
|
||||||
|
|
||||||
### Removed Utilities (NEVER use these in v4)
|
|
||||||
|
|
||||||
| ❌ Deprecated | ✅ Replacement |
|
|
||||||
| ----------------------- | ------------------------------------------------- |
|
|
||||||
| `bg-opacity-*` | Use opacity modifiers like `bg-black/50` |
|
|
||||||
| `text-opacity-*` | Use opacity modifiers like `text-black/50` |
|
|
||||||
| `border-opacity-*` | Use opacity modifiers like `border-black/50` |
|
|
||||||
| `divide-opacity-*` | Use opacity modifiers like `divide-black/50` |
|
|
||||||
| `ring-opacity-*` | Use opacity modifiers like `ring-black/50` |
|
|
||||||
| `placeholder-opacity-*` | Use opacity modifiers like `placeholder-black/50` |
|
|
||||||
| `flex-shrink-*` | `shrink-*` |
|
|
||||||
| `flex-grow-*` | `grow-*` |
|
|
||||||
| `overflow-ellipsis` | `text-ellipsis` |
|
|
||||||
| `decoration-slice` | `box-decoration-slice` |
|
|
||||||
| `decoration-clone` | `box-decoration-clone` |
|
|
||||||
|
|
||||||
### Renamed Utilities
|
|
||||||
|
|
||||||
Use the v4 name when migrating code that still carries Tailwind v3 semantics. Do not blanket-replace existing v4 classes: classes such as `rounded-sm`, `shadow-sm`, `ring-1`, and `ring-2` are valid in this codebase when they intentionally represent the current design scale.
|
|
||||||
|
|
||||||
| ❌ v3 pattern | ✅ v4 pattern |
|
|
||||||
| ------------------- | -------------------------------------------------- |
|
|
||||||
| `bg-gradient-*` | `bg-linear-*` |
|
|
||||||
| old shadow scale | verify against the current Tailwind/design scale |
|
|
||||||
| old blur scale | verify against the current Tailwind/design scale |
|
|
||||||
| old radius scale | use the Dify radius token mapping when applicable |
|
|
||||||
| `outline-none` | `outline-hidden` |
|
|
||||||
| bare `ring` utility | use an explicit ring width such as `ring-1`/`ring-2`/`ring-3` |
|
|
||||||
|
|
||||||
For Figma radius tokens, follow `packages/dify-ui/AGENTS.md`. For example, `--radius/xs` maps to `rounded-sm`; do not rewrite it to `rounded-xs`.
|
|
||||||
|
|
||||||
## Layout and Spacing Rules
|
|
||||||
|
|
||||||
### Flexbox and Grid Spacing
|
|
||||||
|
|
||||||
#### Always use gap utilities for internal spacing
|
|
||||||
|
|
||||||
Gap provides consistent spacing without edge cases (no extra space on last items). It's cleaner and more maintainable than margins on children.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Don't do this -->
|
|
||||||
<div class="flex">
|
|
||||||
<div class="mr-4">Item 1</div>
|
|
||||||
<div class="mr-4">Item 2</div>
|
|
||||||
<div>Item 3</div>
|
|
||||||
<!-- No margin on last -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Do this instead -->
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<div>Item 1</div>
|
|
||||||
<div>Item 2</div>
|
|
||||||
<div>Item 3</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Gap vs Space utilities
|
|
||||||
|
|
||||||
- **Never use `space-x-*` or `space-y-*` in flex/grid layouts** - always use gap
|
|
||||||
- Space utilities add margins to children and have issues with wrapped items
|
|
||||||
- Gap works correctly with flex-wrap and all flex directions
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Avoid space utilities in flex containers -->
|
|
||||||
<div class="flex flex-wrap space-x-4">
|
|
||||||
<!-- Space utilities break with wrapped items -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Use gap for consistent spacing -->
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<!-- Gap works perfectly with wrapping -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### General Spacing Guidelines
|
|
||||||
|
|
||||||
- **Prefer top and left margins** over bottom and right margins (unless conditionally rendered)
|
|
||||||
- **Use padding on parent containers** instead of bottom margins on the last child
|
|
||||||
- **Always use `min-h-dvh` instead of `min-h-screen`** - `min-h-screen` is buggy on mobile Safari
|
|
||||||
- **Prefer `size-*` utilities** over separate `w-*` and `h-*` when setting equal dimensions
|
|
||||||
- For max-widths, prefer the container scale (e.g., `max-w-2xs` over `max-w-72`)
|
|
||||||
|
|
||||||
## Typography Rules
|
|
||||||
|
|
||||||
### Line Heights
|
|
||||||
|
|
||||||
- **Never use `leading-*` classes** - Always use line height modifiers with text size
|
|
||||||
- **Always use fixed line heights from the spacing scale** - Don't use named values
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Don't do this -->
|
|
||||||
<p class="text-base leading-7">Text with separate line height</p>
|
|
||||||
<p class="text-lg leading-relaxed">Text with named line height</p>
|
|
||||||
|
|
||||||
<!-- ✅ Do this instead -->
|
|
||||||
<p class="text-base/7">Text with line height modifier</p>
|
|
||||||
<p class="text-lg/8">Text with specific line height</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Font Size Reference
|
|
||||||
|
|
||||||
Be precise with font sizes - know the actual pixel values:
|
|
||||||
|
|
||||||
- `text-xs` = 12px
|
|
||||||
- `text-sm` = 14px
|
|
||||||
- `text-base` = 16px
|
|
||||||
- `text-lg` = 18px
|
|
||||||
- `text-xl` = 20px
|
|
||||||
|
|
||||||
## Color and Opacity
|
|
||||||
|
|
||||||
### Opacity Modifiers
|
|
||||||
|
|
||||||
**Never use `bg-opacity-*`, `text-opacity-*`, etc.** - use the opacity modifier syntax:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Don't do this -->
|
|
||||||
<div class="bg-red-500 bg-opacity-60">Old opacity syntax</div>
|
|
||||||
|
|
||||||
<!-- ✅ Do this instead -->
|
|
||||||
<div class="bg-red-500/60">Modern opacity syntax</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Responsive Design
|
|
||||||
|
|
||||||
### Breakpoint Optimization
|
|
||||||
|
|
||||||
- **Check for redundant classes across breakpoints**
|
|
||||||
- **Only add breakpoint variants when values change**
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Redundant breakpoint classes -->
|
|
||||||
<div class="px-4 md:px-4 lg:px-4">
|
|
||||||
<!-- md:px-4 and lg:px-4 are redundant -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Efficient breakpoint usage -->
|
|
||||||
<div class="px-4 lg:px-8">
|
|
||||||
<!-- Only specify when value changes -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dark Mode
|
|
||||||
|
|
||||||
### Dark Mode Best Practices
|
|
||||||
|
|
||||||
- Use the plain `dark:` variant pattern
|
|
||||||
- Put light mode styles first, then dark mode styles
|
|
||||||
- Ensure `dark:` variant comes before other variants
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ✅ Correct dark mode pattern -->
|
|
||||||
<div class="bg-white text-black dark:bg-black dark:text-white">
|
|
||||||
<button class="hover:bg-gray-100 dark:hover:bg-gray-800">Click me</button>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gradient Utilities
|
|
||||||
|
|
||||||
- **ALWAYS Use `bg-linear-*` instead of `bg-gradient-*` utilities** - The gradient utilities were renamed in v4
|
|
||||||
- Use the new `bg-radial` or `bg-radial-[<position>]` to create radial gradients
|
|
||||||
- Use the new `bg-conic` or `bg-conic-*` to create conic gradients
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ✅ Use the new gradient utilities -->
|
|
||||||
<div class="h-14 bg-linear-to-br from-violet-500 to-fuchsia-500"></div>
|
|
||||||
<div
|
|
||||||
class="size-18 bg-radial-[at_50%_75%] from-sky-200 via-blue-400 to-indigo-900 to-90%"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="size-24 bg-conic-180 from-indigo-600 via-indigo-50 to-indigo-600"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- ❌ Do not use bg-gradient-* utilities -->
|
|
||||||
<div class="h-14 bg-gradient-to-br from-violet-500 to-fuchsia-500"></div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Working with CSS Variables
|
|
||||||
|
|
||||||
### Accessing Theme Values
|
|
||||||
|
|
||||||
Tailwind CSS v4 exposes all theme values as CSS variables:
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* Access colors, and other theme values */
|
|
||||||
.custom-element {
|
|
||||||
background: var(--color-red-500);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### The `--spacing()` Function
|
|
||||||
|
|
||||||
Use the dedicated `--spacing()` function for spacing calculations:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.custom-class {
|
|
||||||
margin-top: calc(100vh - --spacing(16));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extending theme values
|
|
||||||
|
|
||||||
Use CSS to extend theme values:
|
|
||||||
|
|
||||||
```css
|
|
||||||
@import "tailwindcss";
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--color-mint-500: oklch(0.72 0.11 178);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="bg-mint-500">
|
|
||||||
<!-- ... -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## New v4 Features
|
|
||||||
|
|
||||||
### Container Queries
|
|
||||||
|
|
||||||
Use the `@container` class and size variants:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<article class="@container">
|
|
||||||
<div class="flex flex-col @md:flex-row @lg:gap-8">
|
|
||||||
<img class="w-full @md:w-48" />
|
|
||||||
<div class="mt-4 @md:mt-0">
|
|
||||||
<!-- Content adapts to container size -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Query Units
|
|
||||||
|
|
||||||
Use container-based units like `cqw` for responsive sizing:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="@container">
|
|
||||||
<h1 class="text-[50cqw]">Responsive to container width</h1>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Text Shadows (v4.1)
|
|
||||||
|
|
||||||
Use text-shadow-\* utilities from text-shadow-2xs to text-shadow-lg:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ✅ Text shadow examples -->
|
|
||||||
<h1 class="text-shadow-lg">Large shadow</h1>
|
|
||||||
<p class="text-shadow-sm/50">Small shadow with opacity</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Masking (v4.1)
|
|
||||||
|
|
||||||
Use the new composable mask utilities for image and gradient masks:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ✅ Linear gradient masks on specific sides -->
|
|
||||||
<div class="mask-t-from-50%">Top fade</div>
|
|
||||||
<div class="mask-b-from-20% mask-b-to-80%">Bottom gradient</div>
|
|
||||||
<div class="mask-linear-from-white mask-linear-to-black/60">
|
|
||||||
Fade from white to black
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Radial gradient masks -->
|
|
||||||
<div class="mask-radial-[100%_100%] mask-radial-from-75% mask-radial-at-left">
|
|
||||||
Radial mask
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Patterns
|
|
||||||
|
|
||||||
### Avoiding Utility Inheritance
|
|
||||||
|
|
||||||
Don't add utilities to parents that you override in children:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!-- ❌ Avoid this pattern -->
|
|
||||||
<div class="text-center">
|
|
||||||
<h1>Centered Heading</h1>
|
|
||||||
<div class="text-left">Left-aligned content</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Better approach -->
|
|
||||||
<div>
|
|
||||||
<h1 class="text-center">Centered Heading</h1>
|
|
||||||
<div>Left-aligned content</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Extraction
|
|
||||||
|
|
||||||
- Extract repeated patterns into framework components, not CSS classes
|
|
||||||
- Keep utility classes in templates/JSX
|
|
||||||
- Use data attributes for complex state-based styling
|
|
||||||
|
|
||||||
## CSS Best Practices
|
|
||||||
|
|
||||||
### Nesting Guidelines
|
|
||||||
|
|
||||||
- Use nesting when styling both parent and children
|
|
||||||
- Avoid empty parent selectors
|
|
||||||
|
|
||||||
```css
|
|
||||||
/* ✅ Good nesting - parent has styles */
|
|
||||||
.card {
|
|
||||||
padding: --spacing(4);
|
|
||||||
|
|
||||||
> .card-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ❌ Avoid empty parents */
|
|
||||||
ul {
|
|
||||||
> li {
|
|
||||||
/* Parent has no styles */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Pitfalls to Avoid
|
|
||||||
|
|
||||||
1. **Using old opacity utilities** - Always use `/opacity` syntax like `bg-red-500/60`
|
|
||||||
2. **Redundant breakpoint classes** - Only specify changes
|
|
||||||
3. **Space utilities in flex/grid** - Always use gap
|
|
||||||
4. **Leading utilities** - Use line-height modifiers like `text-sm/6`
|
|
||||||
5. **Arbitrary values** - Use the design scale
|
|
||||||
6. **@apply directive** - Use components or CSS variables
|
|
||||||
7. **min-h-screen on mobile** - Use min-h-dvh
|
|
||||||
8. **Separate width/height** - Use size utilities when equal
|
|
||||||
9. **Arbitrary values** - Always use Tailwind's predefined scale whenever possible (e.g., use `ml-4` over `ml-[16px]`)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/backend-code-review
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/e2e-cucumber-playwright
|
|
||||||
@ -1 +0,0 @@
|
|||||||
../../.agents/skills/frontend-query-mutation
|
|
||||||
1
.claude/skills/orpc-contract-first
Symbolic link
1
.claude/skills/orpc-contract-first
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/orpc-contract-first
|
||||||
@ -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_publisher,trigger_refresh_executor,retention\"" >> ~/.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,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-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
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
have_fun: false
|
|
||||||
memory_config:
|
|
||||||
disabled: false
|
|
||||||
code_review:
|
|
||||||
disable: true
|
|
||||||
comment_severity_threshold: MEDIUM
|
|
||||||
max_review_comments: -1
|
|
||||||
pull_request_opened:
|
|
||||||
help: false
|
|
||||||
summary: false
|
|
||||||
code_review: false
|
|
||||||
include_drafts: false
|
|
||||||
ignore_patterns: []
|
|
||||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -6,9 +6,6 @@
|
|||||||
|
|
||||||
* @crazywoola @laipz8200 @Yeuoly
|
* @crazywoola @laipz8200 @Yeuoly
|
||||||
|
|
||||||
# ESLint suppression file is maintained by autofix.ci pruning.
|
|
||||||
/eslint-suppressions.json
|
|
||||||
|
|
||||||
# CODEOWNERS file
|
# CODEOWNERS file
|
||||||
/.github/CODEOWNERS @laipz8200 @crazywoola
|
/.github/CODEOWNERS @laipz8200 @crazywoola
|
||||||
|
|
||||||
@ -39,6 +36,7 @@
|
|||||||
/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/core/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
|
||||||
|
|||||||
11
.github/actions/setup-web/action.yml
vendored
11
.github/actions/setup-web/action.yml
vendored
@ -1,11 +0,0 @@
|
|||||||
name: Setup Web Environment
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Setup Vite+
|
|
||||||
uses: voidzero-dev/setup-vp@4f5aa3e38c781f1b01e78fb9255527cee8a6efa6 # v1.8.0
|
|
||||||
with:
|
|
||||||
node-version-file: .nvmrc
|
|
||||||
cache: true
|
|
||||||
run-install: true
|
|
||||||
112
.github/dependabot.yml
vendored
112
.github/dependabot.yml
vendored
@ -1,112 +1,12 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/web"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
- package-ecosystem: "uv"
|
- package-ecosystem: "uv"
|
||||||
directory: "/api"
|
directory: "/api"
|
||||||
open-pull-requests-limit: 10
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
groups:
|
open-pull-requests-limit: 2
|
||||||
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: "/"
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
github-actions-dependencies:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
|
|||||||
8
.github/labeler.yml
vendored
8
.github/labeler.yml
vendored
@ -1,9 +1,3 @@
|
|||||||
web:
|
web:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file:
|
- any-glob-to-any-file: 'web/**'
|
||||||
- 'web/**'
|
|
||||||
- 'packages/**'
|
|
||||||
- 'package.json'
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'pnpm-workspace.yaml'
|
|
||||||
- '.nvmrc'
|
|
||||||
|
|||||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@ -7,7 +7,6 @@
|
|||||||
## 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
|
||||||
|
|
||||||
@ -18,7 +17,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)
|
||||||
- [ ] 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 understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
|
||||||
- [ ] 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 added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
|
||||||
- [ ] I've updated the documentation accordingly.
|
- [x] I've updated the documentation accordingly.
|
||||||
- [ ] I ran `make lint && make type-check` (backend) and `cd web && pnpm exec vp staged` (frontend) to appease the lint gods
|
- [x] I ran `make lint` and `make type-check` (backend) and `cd web && npx lint-staged` (frontend) to appease the lint gods
|
||||||
|
|||||||
82
.github/scripts/generate-i18n-changes.mjs
vendored
82
.github/scripts/generate-i18n-changes.mjs
vendored
@ -1,82 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
163
.github/workflows/api-tests.yml
vendored
163
.github/workflows/api-tests.yml
vendored
@ -2,40 +2,32 @@ name: Run Pytest
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
|
||||||
CODECOV_TOKEN:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: api-tests-${{ github.head_ref || github.run_id }}
|
group: api-tests-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
api-unit:
|
test:
|
||||||
name: API Unit Tests
|
name: API Tests
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
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:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -50,62 +42,16 @@ jobs:
|
|||||||
- 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 dev/pytest/pytest_config_tests.py
|
||||||
|
|
||||||
- name: Run Unit Tests
|
|
||||||
run: uv run --project api bash dev/pytest/pytest_unit_tests.sh
|
|
||||||
|
|
||||||
- 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: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
cp docker/envs/middleware.env.example docker/middleware.env
|
cp docker/middleware.env.example docker/middleware.env
|
||||||
|
|
||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
|
|
||||||
- name: Set up Sandbox
|
- name: Set up Sandbox
|
||||||
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
uses: hoverkraft-tech/compose-action@v2
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -119,94 +65,35 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
||||||
|
|
||||||
- name: Run Integration Tests
|
- 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 \
|
||||||
-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
|
- name: Coverage Summary
|
||||||
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: |
|
run: |
|
||||||
set -euo pipefail
|
set -x
|
||||||
|
# Extract coverage percentage and create a summary
|
||||||
|
TOTAL_COVERAGE=$(python -c 'import json; print(json.load(open("coverage.json"))["totals"]["percent_covered_display"])')
|
||||||
|
|
||||||
echo "### API Coverage" >> "$GITHUB_STEP_SUMMARY"
|
# Create a detailed coverage summary
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "### Test Coverage Summary :test_tube:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Merged backend coverage report generated for Codecov project status." >> "$GITHUB_STEP_SUMMARY"
|
echo "Total Coverage: ${TOTAL_COVERAGE}%" >> $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 ""
|
||||||
echo "<details><summary>Coverage report</summary>"
|
echo "<details><summary>File-level coverage (click to expand)</summary>"
|
||||||
echo ""
|
echo ""
|
||||||
echo '```'
|
echo '```'
|
||||||
cat "$report_file"
|
uv run --project api coverage report -m
|
||||||
echo '```'
|
echo '```'
|
||||||
echo "</details>"
|
echo "</details>"
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
uv run --project api coverage xml -o coverage.xml
|
|
||||||
|
|
||||||
- name: Report coverage
|
|
||||||
if: ${{ env.CODECOV_TOKEN != '' }}
|
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
|
||||||
with:
|
|
||||||
files: ./coverage.xml
|
|
||||||
disable_search: true
|
|
||||||
flags: api
|
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
|
||||||
|
|||||||
66
.github/workflows/autofix.yml
vendored
66
.github/workflows/autofix.yml
vendored
@ -2,9 +2,6 @@ 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:
|
||||||
@ -13,60 +10,32 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
autofix:
|
autofix:
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Complete merge group check
|
- uses: actions/checkout@v6
|
||||||
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@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
uses: tj-actions/changed-files@v47
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
docker/generate_docker_compose
|
docker/generate_docker_compose
|
||||||
docker/.env.example
|
docker/.env.example
|
||||||
docker/docker-compose-template.yaml
|
docker/docker-compose-template.yaml
|
||||||
docker/docker-compose.yaml
|
docker/docker-compose.yaml
|
||||||
- name: Check web inputs
|
- uses: actions/setup-python@v6
|
||||||
if: github.event_name != 'merge_group'
|
|
||||||
id: web-changes
|
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
web/**
|
|
||||||
packages/**
|
|
||||||
package.json
|
|
||||||
pnpm-lock.yaml
|
|
||||||
pnpm-workspace.yaml
|
|
||||||
.nvmrc
|
|
||||||
- name: Check api inputs
|
|
||||||
if: github.event_name != 'merge_group'
|
|
||||||
id: api-changes
|
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
api/**
|
|
||||||
- if: github.event_name != 'merge_group'
|
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- if: github.event_name != 'merge_group'
|
- uses: astral-sh/setup-uv@v7
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
||||||
|
|
||||||
- name: Generate Docker Compose
|
- name: Generate Docker Compose
|
||||||
if: github.event_name != 'merge_group' && steps.docker-compose-changes.outputs.any_changed == 'true'
|
if: steps.docker-compose-changes.outputs.any_changed == 'true'
|
||||||
run: |
|
run: |
|
||||||
cd docker
|
cd docker
|
||||||
./generate_docker_compose
|
./generate_docker_compose
|
||||||
|
|
||||||
- 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
|
||||||
# fmt first to avoid line too long
|
# fmt first to avoid line too long
|
||||||
@ -77,13 +46,11 @@ jobs:
|
|||||||
uv run ruff format ..
|
uv run ruff format ..
|
||||||
|
|
||||||
- name: count migration progress
|
- name: count migration progress
|
||||||
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: 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
|
||||||
@ -112,20 +79,9 @@ jobs:
|
|||||||
find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \;
|
find . -name "*.py" -type f -exec sed -i.bak -E 's/"([^"]+)" \| None/Optional["\1"]/g; s/'"'"'([^'"'"']+)'"'"' \| None/Optional['"'"'\1'"'"']/g' {} \;
|
||||||
find . -name "*.py.bak" -type f -delete
|
find . -name "*.py.bak" -type f -delete
|
||||||
|
|
||||||
- name: Setup web environment
|
# mdformat breaks YAML front matter in markdown files. Add --exclude for directories containing YAML front matter.
|
||||||
if: github.event_name != 'merge_group'
|
- name: mdformat
|
||||||
uses: ./.github/actions/setup-web
|
|
||||||
|
|
||||||
- name: Generate API docs
|
|
||||||
if: github.event_name != 'merge_group' && steps.api-changes.outputs.any_changed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
cd api
|
uvx --python 3.13 mdformat . --exclude ".agents/skills/**"
|
||||||
uv run dev/generate_swagger_markdown_docs.py --swagger-dir openapi --markdown-dir openapi/markdown
|
|
||||||
|
|
||||||
- name: ESLint autofix
|
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||||
if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true'
|
|
||||||
run: |
|
|
||||||
vp exec eslint --concurrency=2 --prune-suppressions --quiet || true
|
|
||||||
|
|
||||||
- if: github.event_name != 'merge_group'
|
|
||||||
uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a # v1.3.4
|
|
||||||
|
|||||||
84
.github/workflows/build-push.yml
vendored
84
.github/workflows/build-push.yml
vendored
@ -24,42 +24,27 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.runs_on }}
|
runs-on: ${{ matrix.platform == 'linux/arm64' && 'arm64_runner' || 'ubuntu-latest' }}
|
||||||
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"
|
||||||
artifact_context: "api"
|
context: "api"
|
||||||
build_context: "{{defaultContext}}:api"
|
|
||||||
file: "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"
|
||||||
artifact_context: "api"
|
context: "api"
|
||||||
build_context: "{{defaultContext}}:api"
|
|
||||||
file: "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"
|
||||||
artifact_context: "web"
|
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"
|
||||||
artifact_context: "web"
|
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
|
||||||
@ -68,31 +53,36 @@ 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@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
- name: Set up QEMU
|
||||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env[matrix.image_name_env] }}
|
images: ${{ env[matrix.image_name_env] }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
project: ${{ vars.DEPOT_PROJECT_ID }}
|
context: "{{defaultContext}}:${{ matrix.context }}"
|
||||||
context: ${{ matrix.build_context }}
|
|
||||||
file: ${{ matrix.file }}
|
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
build-args: COMMIT_SHA=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
build-args: |
|
||||||
|
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:
|
||||||
@ -103,40 +93,16 @@ jobs:
|
|||||||
touch "/tmp/digests/${sanitized_digest}"
|
touch "/tmp/digests/${sanitized_digest}"
|
||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.artifact_context }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ matrix.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}}:api"
|
|
||||||
file: "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: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'langgenius/dify'
|
if: github.repository == 'langgenius/dify'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -149,21 +115,21 @@ jobs:
|
|||||||
context: "web"
|
context: "web"
|
||||||
steps:
|
steps:
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
pattern: digests-${{ matrix.context }}-*
|
pattern: digests-${{ matrix.context }}-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKERHUB_USER }}
|
username: ${{ env.DOCKERHUB_USER }}
|
||||||
password: ${{ env.DOCKERHUB_TOKEN }}
|
password: ${{ env.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata for Docker
|
- name: Extract metadata for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env[matrix.image_name_env] }}
|
images: ${{ env[matrix.image_name_env] }}
|
||||||
tags: |
|
tags: |
|
||||||
|
|||||||
42
.github/workflows/db-migration-test.yml
vendored
42
.github/workflows/db-migration-test.yml
vendored
@ -9,17 +9,17 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
db-migration-test-postgres:
|
db-migration-test-postgres:
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@v7
|
||||||
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 envs/middleware.env.example middleware.env
|
cp middleware.env.example middleware.env
|
||||||
|
|
||||||
- name: Set up Middlewares
|
- name: Set up Middlewares
|
||||||
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -59,17 +59,17 @@ 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: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@v7
|
||||||
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 envs/middleware.env.example middleware.env
|
cp 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@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.middleware.yaml
|
docker/docker-compose.middleware.yaml
|
||||||
@ -110,28 +110,6 @@ 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/compose-action@v2.6.0 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
|
||||||
|
|||||||
4
.github/workflows/deploy-agent-dev.yml
vendored
4
.github/workflows/deploy-agent-dev.yml
vendored
@ -13,13 +13,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
github.event.workflow_run.head_branch == 'deploy/agent-dev'
|
github.event.workflow_run.head_branch == 'deploy/agent-dev'
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.AGENT_DEV_SSH_HOST }}
|
host: ${{ secrets.AGENT_DEV_SSH_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
|||||||
4
.github/workflows/deploy-dev.yml
vendored
4
.github/workflows/deploy-dev.yml
vendored
@ -10,13 +10,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
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'
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.SSH_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
|||||||
2
.github/workflows/deploy-enterprise.yml
vendored
2
.github/workflows/deploy-enterprise.yml
vendored
@ -13,7 +13,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
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'
|
||||||
|
|||||||
4
.github/workflows/deploy-hitl.yml
vendored
4
.github/workflows/deploy-hitl.yml
vendored
@ -10,13 +10,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
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'
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.HITL_SSH_HOST }}
|
host: ${{ secrets.HITL_SSH_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
|||||||
62
.github/workflows/docker-build.yml
vendored
62
.github/workflows/docker-build.yml
vendored
@ -14,69 +14,35 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker:
|
build-docker:
|
||||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
runs-on: ubuntu-latest
|
||||||
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
|
||||||
runs_on: depot-ubuntu-24.04-4
|
context: "api"
|
||||||
context: "{{defaultContext}}:api"
|
|
||||||
file: "Dockerfile"
|
|
||||||
- service_name: "api-arm64"
|
- service_name: "api-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
runs_on: depot-ubuntu-24.04-4
|
context: "api"
|
||||||
context: "{{defaultContext}}:api"
|
|
||||||
file: "Dockerfile"
|
|
||||||
- service_name: "web-amd64"
|
- service_name: "web-amd64"
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
runs_on: depot-ubuntu-24.04-4
|
context: "web"
|
||||||
context: "{{defaultContext}}"
|
|
||||||
file: "web/Dockerfile"
|
|
||||||
- service_name: "web-arm64"
|
- service_name: "web-arm64"
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
runs_on: depot-ubuntu-24.04-4
|
context: "web"
|
||||||
context: "{{defaultContext}}"
|
|
||||||
file: "web/Dockerfile"
|
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Depot CLI
|
- name: Set up QEMU
|
||||||
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1.7.1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- 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}}:api"
|
|
||||||
file: "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@v3
|
||||||
|
|
||||||
- name: Build Docker Image
|
- name: Build Docker Image
|
||||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: false
|
push: false
|
||||||
context: ${{ matrix.context }}
|
context: "{{defaultContext}}:${{ matrix.context }}"
|
||||||
file: ${{ matrix.file }}
|
file: "${{ matrix.file }}"
|
||||||
platforms: linux/amd64
|
platforms: ${{ matrix.platform }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
4
.github/workflows/labeler.yml
vendored
4
.github/workflows/labeler.yml
vendored
@ -7,8 +7,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
- uses: actions/labeler@v6
|
||||||
with:
|
with:
|
||||||
sync-labels: true
|
sync-labels: true
|
||||||
|
|||||||
385
.github/workflows/main-ci.yml
vendored
385
.github/workflows/main-ci.yml
vendored
@ -3,14 +3,10 @@ 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
|
||||||
@ -21,408 +17,63 @@ 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
|
||||||
needs: pre_job
|
runs-on: ubuntu-latest
|
||||||
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 }}
|
||||||
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 }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@v6
|
||||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
- uses: dorny/paths-filter@v3
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
api:
|
api:
|
||||||
- 'api/**'
|
- 'api/**'
|
||||||
|
- 'docker/**'
|
||||||
- '.github/workflows/api-tests.yml'
|
- '.github/workflows/api-tests.yml'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
|
||||||
- '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/**'
|
|
||||||
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/**'
|
|
||||||
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/**'
|
||||||
- 'api/tests/integration_tests/vdb/**'
|
- 'docker/**'
|
||||||
- 'api/providers/vdb/*/tests/**'
|
|
||||||
- '.github/workflows/vdb-tests.yml'
|
- '.github/workflows/vdb-tests.yml'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
|
||||||
- 'docker/.env.example'
|
|
||||||
- 'docker/envs/middleware.env.example'
|
|
||||||
- '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'
|
||||||
- '.github/workflows/expose_service_ports.sh'
|
|
||||||
- '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 while always emitting stable required checks.
|
|
||||||
api-tests-run:
|
|
||||||
name: Run API Tests
|
|
||||||
needs:
|
|
||||||
- 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
|
|
||||||
secrets: inherit
|
|
||||||
|
|
||||||
api-tests-skip:
|
|
||||||
name: Skip API Tests
|
|
||||||
needs:
|
|
||||||
- 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."
|
|
||||||
|
|
||||||
|
# Run tests in parallel
|
||||||
api-tests:
|
api-tests:
|
||||||
name: API Tests
|
name: API Tests
|
||||||
if: ${{ always() }}
|
needs: check-changes
|
||||||
needs:
|
if: needs.check-changes.outputs.api-changed == 'true'
|
||||||
- pre_job
|
uses: ./.github/workflows/api-tests.yml
|
||||||
- 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
|
|
||||||
|
|
||||||
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
|
|
||||||
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:
|
web-tests:
|
||||||
name: Web Tests
|
name: Web Tests
|
||||||
if: ${{ always() }}
|
needs: check-changes
|
||||||
needs:
|
if: needs.check-changes.outputs.web-changed == 'true'
|
||||||
- pre_job
|
uses: ./.github/workflows/web-tests.yml
|
||||||
- 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
|
||||||
if: ${{ always() }}
|
needs: check-changes
|
||||||
needs:
|
if: needs.check-changes.outputs.vdb-changed == 'true'
|
||||||
- pre_job
|
uses: ./.github/workflows/vdb-tests.yml
|
||||||
- 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
|
||||||
if: ${{ always() }}
|
needs: check-changes
|
||||||
needs:
|
if: needs.check-changes.outputs.migration-changed == 'true'
|
||||||
- pre_job
|
uses: ./.github/workflows/db-migration-test.yml
|
||||||
- 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
|
|
||||||
|
|||||||
104
.github/workflows/pyrefly-diff-comment.yml
vendored
104
.github/workflows/pyrefly-diff-comment.yml
vendored
@ -1,104 +0,0 @@
|
|||||||
name: Comment with Pyrefly Diff
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- Pyrefly Diff Check
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
name: Comment PR with pyrefly diff
|
|
||||||
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: Download pyrefly diff 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_diff'
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error('pyrefly_diff 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_diff.zip', Buffer.from(download.data));
|
|
||||||
|
|
||||||
- name: Unzip artifact
|
|
||||||
run: unzip -o pyrefly_diff.zip
|
|
||||||
|
|
||||||
- name: Post comment
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' });
|
|
||||||
let prNumber = null;
|
|
||||||
try {
|
|
||||||
prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10);
|
|
||||||
} catch (err) {
|
|
||||||
// Fallback to workflow_run payload if artifact is missing or incomplete.
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_CHARS = 65000;
|
|
||||||
if (diff.length > MAX_CHARS) {
|
|
||||||
diff = diff.slice(0, MAX_CHARS);
|
|
||||||
diff = diff.slice(0, diff.lastIndexOf('\\n'));
|
|
||||||
diff += '\\n\\n... (truncated) ...';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff.trim()) {
|
|
||||||
const body = '### Pyrefly Diff\n<details>\n<summary>base → PR</summary>\n\n```diff\n' + diff + '\n```\n</details>';
|
|
||||||
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));
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
128
.github/workflows/pyrefly-diff.yml
vendored
128
.github/workflows/pyrefly-diff.yml
vendored
@ -1,128 +0,0 @@
|
|||||||
name: Pyrefly Diff Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'api/**/*.py'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pyrefly-diff:
|
|
||||||
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: Prepare diagnostics extractor
|
|
||||||
run: |
|
|
||||||
git show ${{ github.event.pull_request.head.sha }}:api/libs/pyrefly_diagnostics.py > /tmp/pyrefly_diagnostics.py
|
|
||||||
|
|
||||||
- name: Run pyrefly on PR branch
|
|
||||||
run: |
|
|
||||||
uv run --directory api --dev pyrefly check 2>&1 \
|
|
||||||
| uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_pr.txt || true
|
|
||||||
|
|
||||||
- name: Checkout base branch
|
|
||||||
run: git checkout ${{ github.base_ref }}
|
|
||||||
|
|
||||||
- name: Run pyrefly on base branch
|
|
||||||
run: |
|
|
||||||
uv run --directory api --dev pyrefly check 2>&1 \
|
|
||||||
| uv run --directory api python /tmp/pyrefly_diagnostics.py > /tmp/pyrefly_base.txt || true
|
|
||||||
|
|
||||||
- name: Compute diff
|
|
||||||
run: |
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
echo ${{ github.event.pull_request.number }} > pr_number.txt
|
|
||||||
|
|
||||||
- name: Upload pyrefly diff
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: pyrefly_diff
|
|
||||||
path: |
|
|
||||||
pyrefly_diff.txt
|
|
||||||
pr_number.txt
|
|
||||||
|
|
||||||
- name: Comment PR with pyrefly diff
|
|
||||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && steps.line_count_check.outputs.same == 'false' }}
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const fs = require('fs');
|
|
||||||
let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' });
|
|
||||||
const prNumber = context.payload.pull_request.number;
|
|
||||||
|
|
||||||
const MAX_CHARS = 65000;
|
|
||||||
if (diff.length > MAX_CHARS) {
|
|
||||||
diff = diff.slice(0, MAX_CHARS);
|
|
||||||
diff = diff.slice(0, diff.lastIndexOf('\n'));
|
|
||||||
diff += '\n\n... (truncated) ...';
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = diff.trim()
|
|
||||||
? [
|
|
||||||
'### Pyrefly Diff',
|
|
||||||
'<details>',
|
|
||||||
'<summary>base → PR</summary>',
|
|
||||||
'',
|
|
||||||
'```diff',
|
|
||||||
diff,
|
|
||||||
'```',
|
|
||||||
'</details>',
|
|
||||||
].join('\n')
|
|
||||||
: '### 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));
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
118
.github/workflows/pyrefly-type-coverage-comment.yml
vendored
118
.github/workflows/pyrefly-type-coverage-comment.yml
vendored
@ -1,118 +0,0 @@
|
|||||||
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 base_report.json \
|
|
||||||
< 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
120
.github/workflows/pyrefly-type-coverage.yml
vendored
120
.github/workflows/pyrefly-type-coverage.yml
vendored
@ -1,120 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
- 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
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
11
.github/workflows/semantic-pull-request.yml
vendored
11
.github/workflows/semantic-pull-request.yml
vendored
@ -7,22 +7,15 @@ 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: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
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@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 }}
|
||||||
|
|||||||
10
.github/workflows/stale.yml
vendored
10
.github/workflows/stale.yml
vendored
@ -12,19 +12,19 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
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: "Closed due to inactivity. If you have any questions, you can reopen it."
|
stale-issue-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-pr-message: "Close due to it's no longer active, 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: '🌚 invalid,🙋♂️ question,wont-fix,no-issue-activity,no-pr-activity,💪 enhancement,🤔 cant-reproduce,🙏 help wanted'
|
any-of-labels: 'duplicate,question,invalid,wontfix,no-issue-activity,no-pr-activity,enhancement,cant-reproduce,help-wanted'
|
||||||
|
|||||||
92
.github/workflows/style.yml
vendored
92
.github/workflows/style.yml
vendored
@ -15,17 +15,17 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
python-style:
|
python-style:
|
||||||
name: Python Style
|
name: Python Style
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
uses: tj-actions/changed-files@v47
|
||||||
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@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
enable-cache: false
|
enable-cache: false
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- 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-core
|
run: make type-check
|
||||||
|
|
||||||
- name: Dotenv check
|
- name: Dotenv check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
web-style:
|
web-style:
|
||||||
name: Web Style
|
name: Web Style
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
@ -67,83 +67,81 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
uses: tj-actions/changed-files@v47
|
||||||
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/workflows/style.yml
|
||||||
.github/actions/setup-web/**
|
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Install pnpm
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
uses: pnpm/action-setup@v4
|
||||||
uses: ./.github/actions/setup-web
|
|
||||||
|
|
||||||
- name: Restore ESLint cache
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
|
||||||
id: eslint-cache-restore
|
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
with:
|
||||||
path: .eslintcache
|
package_json_file: web/package.json
|
||||||
key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }}
|
run_install: false
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Web dependencies
|
||||||
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Web style check
|
- name: Web style check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: .
|
working-directory: ./web
|
||||||
run: vp run lint:ci
|
run: |
|
||||||
|
pnpm run lint:ci
|
||||||
|
# pnpm run lint:report
|
||||||
|
# continue-on-error: true
|
||||||
|
|
||||||
|
# - name: Annotate Code
|
||||||
|
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
|
||||||
|
# uses: DerLev/eslint-annotations@51347b3a0abfb503fc8734d5ae31c4b151297fae
|
||||||
|
# with:
|
||||||
|
# eslint-report: web/eslint_report.json
|
||||||
|
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Web tsslint
|
- name: Web tsslint
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
env:
|
run: pnpm run lint:tss
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
|
||||||
run: vp run lint:tss
|
|
||||||
|
|
||||||
- name: Web type check
|
- name: Web type check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: .
|
working-directory: ./web
|
||||||
run: vp run type-check
|
run: pnpm run type-check
|
||||||
|
|
||||||
- name: Web dead code check
|
- name: Web dead code check
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: vp run knip
|
run: pnpm run knip
|
||||||
|
|
||||||
- name: Save ESLint cache
|
|
||||||
if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: .eslintcache
|
|
||||||
key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }}
|
|
||||||
|
|
||||||
superlinter:
|
superlinter:
|
||||||
name: SuperLinter
|
name: SuperLinter
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check changed files
|
- name: Check changed files
|
||||||
id: changed-files
|
id: changed-files
|
||||||
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
|
uses: tj-actions/changed-files@v47
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
**.sh
|
**.sh
|
||||||
@ -154,7 +152,7 @@ jobs:
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
|
|
||||||
- name: Super-linter
|
- name: Super-linter
|
||||||
uses: super-linter/super-linter/slim@9e863354e3ff62e0727d37183162c4a88873df41 # v8.6.0
|
uses: super-linter/super-linter/slim@v8
|
||||||
if: steps.changed-files.outputs.any_changed == 'true'
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
env:
|
env:
|
||||||
BASH_SEVERITY: warning
|
BASH_SEVERITY: warning
|
||||||
|
|||||||
11
.github/workflows/tool-test-sdks.yaml
vendored
11
.github/workflows/tool-test-sdks.yaml
vendored
@ -6,9 +6,6 @@ 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 }}
|
||||||
@ -17,21 +14,21 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: unit test for Node.js SDK
|
name: unit test for Node.js SDK
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: sdks/nodejs-client
|
working-directory: sdks/nodejs-client
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 24
|
||||||
cache: ''
|
cache: ''
|
||||||
cache-dependency-path: 'pnpm-lock.yaml'
|
cache-dependency-path: 'pnpm-lock.yaml'
|
||||||
|
|
||||||
|
|||||||
653
.github/workflows/translate-i18n-claude.yml
vendored
653
.github/workflows/translate-i18n-claude.yml
vendored
@ -1,24 +1,26 @@
|
|||||||
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 bridged by trigger-i18n-sync.yml via repository_dispatch.
|
# Push events are handled by trigger-i18n-sync.yml which sends 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"). Required for full mode; leave empty in incremental mode to use en-US files changed since HEAD~1.'
|
description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
|
||||||
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 target languages except en-US.'
|
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
mode:
|
mode:
|
||||||
description: 'Sync mode: incremental (compare with previous en-US revision) or full (sync all keys in scope)'
|
description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
|
||||||
required: false
|
required: false
|
||||||
default: incremental
|
default: 'incremental'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- incremental
|
- incremental
|
||||||
@ -28,19 +30,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: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
timeout-minutes: 60
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -50,296 +48,393 @@ jobs:
|
|||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Install pnpm
|
||||||
uses: ./.github/actions/setup-web
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: web/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
- name: Prepare sync context
|
- name: Set up Node.js
|
||||||
id: context
|
uses: actions/setup-node@v6
|
||||||
shell: bash
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Detect changed files and generate diff
|
||||||
|
id: detect_changes
|
||||||
run: |
|
run: |
|
||||||
DEFAULT_TARGET_LANGS=$(awk "
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
/value: '/ {
|
# Manual trigger
|
||||||
value=\$2
|
|
||||||
gsub(/[',]/, \"\", value)
|
|
||||||
}
|
|
||||||
/supported: true/ && value != \"en-US\" {
|
|
||||||
printf \"%s \", value
|
|
||||||
}
|
|
||||||
" web/i18n-config/languages.ts | sed 's/[[:space:]]*$//')
|
|
||||||
|
|
||||||
generate_changes_json() {
|
|
||||||
node .github/scripts/generate-i18n-changes.mjs
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
|
||||||
BASE_SHA="${{ github.event.client_payload.base_sha }}"
|
|
||||||
HEAD_SHA="${{ github.event.client_payload.head_sha }}"
|
|
||||||
CHANGED_FILES="${{ github.event.client_payload.changed_files }}"
|
|
||||||
TARGET_LANGS="$DEFAULT_TARGET_LANGS"
|
|
||||||
SYNC_MODE="${{ github.event.client_payload.sync_mode || 'incremental' }}"
|
|
||||||
|
|
||||||
if [ -n "${{ github.event.client_payload.changes_base64 }}" ]; then
|
|
||||||
printf '%s' '${{ github.event.client_payload.changes_base64 }}' | base64 -d > /tmp/i18n-changes.json
|
|
||||||
CHANGES_AVAILABLE="true"
|
|
||||||
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
|
|
||||||
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
|
||||||
CHANGES_AVAILABLE="false"
|
|
||||||
CHANGES_SOURCE="unavailable"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
BASE_SHA=""
|
|
||||||
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
|
if [ -n "${{ github.event.inputs.files }}" ]; then
|
||||||
CHANGED_FILES="${{ github.event.inputs.files }}"
|
echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
|
||||||
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
|
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:]]*$//')
|
# Get all JSON files in en-US directory
|
||||||
|
files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
|
||||||
|
echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
elif [ "$SYNC_MODE" = "full" ]; then
|
echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
|
||||||
echo "workflow_dispatch full mode requires the files input to stay within CI limits." >&2
|
echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# For manual trigger with incremental mode, get diff from last commit
|
||||||
|
# For full mode, we'll do a complete check anyway
|
||||||
|
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
|
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 [ -n "${{ github.event.client_payload.diff_base64 }}" ]; then
|
||||||
|
if ! echo "${{ github.event.client_payload.diff_base64 }}" | base64 -d > /tmp/i18n-diff.txt 2>&1; then
|
||||||
|
echo "Warning: Failed to decode base64 diff payload" >&2
|
||||||
|
echo "" > /tmp/i18n-diff.txt
|
||||||
|
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||||
|
elif [ -s /tmp/i18n-diff.txt ]; then
|
||||||
|
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
CHANGED_FILES=""
|
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
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
|
else
|
||||||
printf '%s' '{"baseSha":"","headSha":"","files":[],"changes":{}}' > /tmp/i18n-changes.json
|
echo "" > /tmp/i18n-diff.txt
|
||||||
CHANGES_AVAILABLE="false"
|
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||||
CHANGES_SOURCE="unavailable"
|
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
echo "Unsupported event type: ${{ github.event_name }}"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FILE_ARGS=""
|
# Truncate diff if too large (keep first 50KB)
|
||||||
if [ -n "$CHANGED_FILES" ]; then
|
if [ -f /tmp/i18n-diff.txt ]; then
|
||||||
FILE_ARGS="--file $CHANGED_FILES"
|
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
||||||
|
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
LANG_ARGS=""
|
echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
|
||||||
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.context.outputs.CHANGED_FILES != ''
|
if: steps.detect_changes.outputs.CHANGED_FILES != ''
|
||||||
uses: anthropics/claude-code-action@476e359e6203e73dad705c8b322e333fabbd7416 # v1.0.119
|
uses: anthropics/claude-code-action@v1
|
||||||
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 the i18n sync agent for the Dify repository.
|
You are a professional i18n synchronization engineer for the Dify project.
|
||||||
Your job is to keep translations synchronized with the English source files under `${{ github.workspace }}/web/i18n/en-US/`.
|
Your task is to keep all language translations in sync with the English source (en-US).
|
||||||
|
|
||||||
Use absolute paths at all times:
|
## CRITICAL TOOL RESTRICTIONS
|
||||||
- Repo root: `${{ github.workspace }}`
|
- Use **Read** tool to read files (NOT cat or bash)
|
||||||
- Web directory: `${{ github.workspace }}/web`
|
- Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
|
||||||
- Language config: `${{ github.workspace }}/web/i18n-config/languages.ts`
|
- Use **Bash** ONLY for: git commands, gh commands, pnpm commands
|
||||||
|
- Run bash commands ONE BY ONE, never combine with && or ||
|
||||||
|
- NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
|
||||||
|
|
||||||
Inputs:
|
## WORKING DIRECTORY & ABSOLUTE PATHS
|
||||||
- Files in scope: `${{ steps.context.outputs.CHANGED_FILES }}`
|
Claude Code sandbox working directory may vary. Always use absolute paths:
|
||||||
- Target languages: `${{ steps.context.outputs.TARGET_LANGS }}`
|
- For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
|
||||||
- Sync mode: `${{ steps.context.outputs.SYNC_MODE }}`
|
- For git: `git -C ${{ github.workspace }} <command>`
|
||||||
- Base SHA: `${{ steps.context.outputs.BASE_SHA }}`
|
- For gh: `gh --repo ${{ github.repository }} <command>`
|
||||||
- Head SHA: `${{ steps.context.outputs.HEAD_SHA }}`
|
- For file paths: `${{ github.workspace }}/web/i18n/`
|
||||||
- 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`
|
|
||||||
|
|
||||||
Tool rules:
|
## EFFICIENCY RULES
|
||||||
- Use Read for repository files.
|
- **ONE Edit per language file** - batch all key additions into a single Edit
|
||||||
- Use Edit for JSON updates.
|
- Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
|
||||||
- Use Bash only for `vp`.
|
- Translate ALL keys for a language mentally first, then do ONE Edit
|
||||||
- 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 120
|
--max-turns 150
|
||||||
--allowedTools "Read,Write,Edit,Bash(vp *),Bash(vp:*),Glob,Grep"
|
--allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),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
|
|
||||||
|
|||||||
94
.github/workflows/trigger-i18n-sync.yml
vendored
94
.github/workflows/trigger-i18n-sync.yml
vendored
@ -1,5 +1,9 @@
|
|||||||
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]
|
||||||
@ -9,82 +13,54 @@ 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: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Detect changed files and build structured change set
|
- name: Detect changed files and generate diff
|
||||||
id: detect
|
id: detect
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
BASE_SHA="${{ github.event.before }}"
|
BEFORE_SHA="${{ github.event.before }}"
|
||||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
# Handle edge case: force push may have null/zero SHA
|
||||||
BASE_SHA=$(git rev-parse HEAD~1 2>/dev/null || true)
|
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||||
|
BEFORE_SHA="HEAD~1"
|
||||||
fi
|
fi
|
||||||
HEAD_SHA="${{ github.sha }}"
|
|
||||||
|
|
||||||
if [ -n "$BASE_SHA" ]; then
|
# Detect changed i18n files
|
||||||
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:]]*$//')
|
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 "")
|
||||||
|
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
|
||||||
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 "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
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: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: peter-evans/repository-dispatch@v3
|
||||||
env:
|
|
||||||
BASE_SHA: ${{ steps.detect.outputs.base_sha }}
|
|
||||||
HEAD_SHA: ${{ steps.detect.outputs.head_sha }}
|
|
||||||
CHANGED_FILES: ${{ steps.detect.outputs.changed_files }}
|
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
event-type: i18n-sync
|
||||||
const fs = require('fs')
|
client-payload: '{"changed_files": "${{ steps.detect.outputs.changed_files }}", "diff_base64": "${{ steps.detect.outputs.diff_base64 }}", "sync_mode": "incremental", "trigger_sha": "${{ github.sha }}"}'
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
95
.github/workflows/vdb-tests-full.yml
vendored
95
.github/workflows/vdb-tests-full.yml
vendored
@ -1,95 +0,0 @@
|
|||||||
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 dotenvs
|
|
||||||
run: |
|
|
||||||
cp docker/.env.example docker/.env
|
|
||||||
cp docker/envs/middleware.env.example docker/middleware.env
|
|
||||||
|
|
||||||
- name: Expose Service Ports
|
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
|
||||||
|
|
||||||
# - name: Set up Vector Store (TiDB)
|
|
||||||
# uses: hoverkraft-tech/compose-action@v2.0.2
|
|
||||||
# with:
|
|
||||||
# compose-file: docker/tidb/docker-compose.yaml
|
|
||||||
# services: |
|
|
||||||
# tidb
|
|
||||||
# tiflash
|
|
||||||
|
|
||||||
- name: Set up Full Vector Store Matrix
|
|
||||||
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.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)
|
|
||||||
# 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 bash dev/pytest/pytest_vdb.sh
|
|
||||||
40
.github/workflows/vdb-tests.yml
vendored
40
.github/workflows/vdb-tests.yml
vendored
@ -1,39 +1,37 @@
|
|||||||
name: Run VDB Smoke Tests
|
name: Run VDB 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 Smoke Tests
|
name: VDB Tests
|
||||||
runs-on: depot-ubuntu-24.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Free Disk Space
|
- name: Free Disk Space
|
||||||
uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3.2.2
|
uses: endersonmenezes/free-disk-space@v3
|
||||||
with:
|
with:
|
||||||
remove_dotnet: true
|
remove_dotnet: true
|
||||||
remove_haskell: true
|
remove_haskell: true
|
||||||
remove_tool_cache: true
|
remove_tool_cache: true
|
||||||
|
|
||||||
- name: Setup UV and Python
|
- name: Setup UV and Python
|
||||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
uses: astral-sh/setup-uv@v7
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
@ -48,7 +46,7 @@ jobs:
|
|||||||
- name: Set up dotenvs
|
- name: Set up dotenvs
|
||||||
run: |
|
run: |
|
||||||
cp docker/.env.example docker/.env
|
cp docker/.env.example docker/.env
|
||||||
cp docker/envs/middleware.env.example docker/middleware.env
|
cp docker/middleware.env.example docker/middleware.env
|
||||||
|
|
||||||
- name: Expose Service Ports
|
- name: Expose Service Ports
|
||||||
run: sh .github/workflows/expose_service_ports.sh
|
run: sh .github/workflows/expose_service_ports.sh
|
||||||
@ -61,18 +59,23 @@ jobs:
|
|||||||
# tidb
|
# tidb
|
||||||
# tiflash
|
# tiflash
|
||||||
|
|
||||||
- name: Set up Vector Stores for Smoke Coverage
|
- name: Set up Vector Stores (Weaviate, Qdrant, PGVector, Milvus, PgVecto-RS, Chroma, MyScale, ElasticSearch, Couchbase, OceanBase)
|
||||||
uses: hoverkraft-tech/compose-action@d2bee4f07e8ca410d6b196d00f90c12e7d48c33a # v2.6.0
|
uses: hoverkraft-tech/compose-action@v2.0.2
|
||||||
with:
|
with:
|
||||||
compose-file: |
|
compose-file: |
|
||||||
docker/docker-compose.yaml
|
docker/docker-compose.yaml
|
||||||
services: |
|
services: |
|
||||||
db_postgres
|
|
||||||
redis
|
|
||||||
weaviate
|
weaviate
|
||||||
qdrant
|
qdrant
|
||||||
|
couchbase-server
|
||||||
|
etcd
|
||||||
|
minio
|
||||||
|
milvus-standalone
|
||||||
|
pgvecto-rs
|
||||||
pgvector
|
pgvector
|
||||||
chroma
|
chroma
|
||||||
|
elasticsearch
|
||||||
|
oceanbase
|
||||||
|
|
||||||
- name: setup test config
|
- name: setup test config
|
||||||
run: |
|
run: |
|
||||||
@ -81,12 +84,7 @@ jobs:
|
|||||||
cp api/tests/integration_tests/.env.example api/tests/integration_tests/.env
|
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/providers/vdb/tidb-vector/tests/integration_tests/check_tiflash_ready.py
|
# run: uv run --project api python api/tests/integration_tests/vdb/tidb_vector/check_tiflash_ready.py
|
||||||
|
|
||||||
- name: Test Vector Stores
|
- name: Test Vector Stores
|
||||||
run: |
|
run: uv run --project api bash dev/pytest/pytest_vdb.sh
|
||||||
uv run --project api pytest --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
|
|
||||||
|
|||||||
68
.github/workflows/web-e2e.yml
vendored
68
.github/workflows/web-e2e.yml
vendored
@ -1,68 +0,0 @@
|
|||||||
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: e2e-admin@example.com
|
|
||||||
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
|
|
||||||
454
.github/workflows/web-tests.yml
vendored
454
.github/workflows/web-tests.yml
vendored
@ -2,12 +2,6 @@ name: Web Tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
|
||||||
CODECOV_TOKEN:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: web-tests-${{ github.head_ref || github.run_id }}
|
group: web-tests-${{ github.head_ref || github.run_id }}
|
||||||
@ -15,15 +9,8 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
name: Web Tests
|
||||||
runs-on: depot-ubuntu-24.04-4
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
VITEST_COVERAGE_SCOPE: app-components
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
shardIndex: [1, 2, 3, 4]
|
|
||||||
shardTotal: [4]
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -31,95 +18,396 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Install pnpm
|
||||||
uses: ./.github/actions/setup-web
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: web/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: vp test run --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --coverage
|
run: pnpm test:ci
|
||||||
|
|
||||||
- name: Upload blob report
|
- name: Coverage Summary
|
||||||
if: ${{ !cancelled() }}
|
if: always()
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
id: coverage-summary
|
||||||
|
run: |
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
COVERAGE_FILE="coverage/coverage-final.json"
|
||||||
|
COVERAGE_SUMMARY_FILE="coverage/coverage-summary.json"
|
||||||
|
|
||||||
|
if [ ! -f "$COVERAGE_FILE" ] && [ ! -f "$COVERAGE_SUMMARY_FILE" ]; then
|
||||||
|
echo "has_coverage=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "### 🚨 Test Coverage Report :test_tube:" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
echo "Coverage data not found. Ensure Vitest runs with coverage enabled." >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "has_coverage=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
node <<'NODE' >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let libCoverage = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
libCoverage = require('istanbul-lib-coverage');
|
||||||
|
} catch (error) {
|
||||||
|
libCoverage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryPath = path.join('coverage', 'coverage-summary.json');
|
||||||
|
const finalPath = path.join('coverage', 'coverage-final.json');
|
||||||
|
|
||||||
|
const hasSummary = fs.existsSync(summaryPath);
|
||||||
|
const hasFinal = fs.existsSync(finalPath);
|
||||||
|
|
||||||
|
if (!hasSummary && !hasFinal) {
|
||||||
|
console.log('### Test Coverage Summary :test_tube:');
|
||||||
|
console.log('');
|
||||||
|
console.log('No coverage data found.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = hasSummary
|
||||||
|
? JSON.parse(fs.readFileSync(summaryPath, 'utf8'))
|
||||||
|
: null;
|
||||||
|
const coverage = hasFinal
|
||||||
|
? JSON.parse(fs.readFileSync(finalPath, 'utf8'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const getLineCoverageFromStatements = (statementMap, statementHits) => {
|
||||||
|
const lineHits = {};
|
||||||
|
|
||||||
|
if (!statementMap || !statementHits) {
|
||||||
|
return lineHits;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(statementMap).forEach(([key, statement]) => {
|
||||||
|
const line = statement?.start?.line;
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hits = statementHits[key] ?? 0;
|
||||||
|
const previous = lineHits[line];
|
||||||
|
lineHits[line] = previous === undefined ? hits : Math.max(previous, hits);
|
||||||
|
});
|
||||||
|
|
||||||
|
return lineHits;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileCoverage = (entry) => (
|
||||||
|
libCoverage ? libCoverage.createFileCoverage(entry) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLineHits = (entry, fileCoverage) => {
|
||||||
|
const lineHits = entry.l ?? {};
|
||||||
|
if (Object.keys(lineHits).length > 0) {
|
||||||
|
return lineHits;
|
||||||
|
}
|
||||||
|
if (fileCoverage) {
|
||||||
|
return fileCoverage.getLineCoverage();
|
||||||
|
}
|
||||||
|
return getLineCoverageFromStatements(entry.statementMap ?? {}, entry.s ?? {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUncoveredLines = (entry, fileCoverage, lineHits) => {
|
||||||
|
if (lineHits && Object.keys(lineHits).length > 0) {
|
||||||
|
return Object.entries(lineHits)
|
||||||
|
.filter(([, count]) => count === 0)
|
||||||
|
.map(([line]) => Number(line))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
if (fileCoverage) {
|
||||||
|
return fileCoverage.getUncoveredLines();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const totals = {
|
||||||
|
lines: { covered: 0, total: 0 },
|
||||||
|
statements: { covered: 0, total: 0 },
|
||||||
|
branches: { covered: 0, total: 0 },
|
||||||
|
functions: { covered: 0, total: 0 },
|
||||||
|
};
|
||||||
|
const fileSummaries = [];
|
||||||
|
|
||||||
|
if (summary) {
|
||||||
|
const totalEntry = summary.total ?? {};
|
||||||
|
['lines', 'statements', 'branches', 'functions'].forEach((key) => {
|
||||||
|
if (totalEntry[key]) {
|
||||||
|
totals[key].covered = totalEntry[key].covered ?? 0;
|
||||||
|
totals[key].total = totalEntry[key].total ?? 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(summary)
|
||||||
|
.filter(([file]) => file !== 'total')
|
||||||
|
.forEach(([file, data]) => {
|
||||||
|
fileSummaries.push({
|
||||||
|
file,
|
||||||
|
pct: data.lines?.pct ?? data.statements?.pct ?? 0,
|
||||||
|
lines: {
|
||||||
|
covered: data.lines?.covered ?? 0,
|
||||||
|
total: data.lines?.total ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (coverage) {
|
||||||
|
Object.entries(coverage).forEach(([file, entry]) => {
|
||||||
|
const fileCoverage = getFileCoverage(entry);
|
||||||
|
const lineHits = getLineHits(entry, fileCoverage);
|
||||||
|
const statementHits = entry.s ?? {};
|
||||||
|
const branchHits = entry.b ?? {};
|
||||||
|
const functionHits = entry.f ?? {};
|
||||||
|
|
||||||
|
const lineTotal = Object.keys(lineHits).length;
|
||||||
|
const lineCovered = Object.values(lineHits).filter((n) => n > 0).length;
|
||||||
|
|
||||||
|
const statementTotal = Object.keys(statementHits).length;
|
||||||
|
const statementCovered = Object.values(statementHits).filter((n) => n > 0).length;
|
||||||
|
|
||||||
|
const branchTotal = Object.values(branchHits).reduce((acc, branches) => acc + branches.length, 0);
|
||||||
|
const branchCovered = Object.values(branchHits).reduce(
|
||||||
|
(acc, branches) => acc + branches.filter((n) => n > 0).length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const functionTotal = Object.keys(functionHits).length;
|
||||||
|
const functionCovered = Object.values(functionHits).filter((n) => n > 0).length;
|
||||||
|
|
||||||
|
totals.lines.total += lineTotal;
|
||||||
|
totals.lines.covered += lineCovered;
|
||||||
|
totals.statements.total += statementTotal;
|
||||||
|
totals.statements.covered += statementCovered;
|
||||||
|
totals.branches.total += branchTotal;
|
||||||
|
totals.branches.covered += branchCovered;
|
||||||
|
totals.functions.total += functionTotal;
|
||||||
|
totals.functions.covered += functionCovered;
|
||||||
|
|
||||||
|
const pct = (covered, tot) => (tot > 0 ? (covered / tot) * 100 : 0);
|
||||||
|
|
||||||
|
fileSummaries.push({
|
||||||
|
file,
|
||||||
|
pct: pct(lineCovered || statementCovered, lineTotal || statementTotal),
|
||||||
|
lines: {
|
||||||
|
covered: lineCovered || statementCovered,
|
||||||
|
total: lineTotal || statementTotal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pct = (covered, tot) => (tot > 0 ? ((covered / tot) * 100).toFixed(2) : '0.00');
|
||||||
|
|
||||||
|
console.log('### Test Coverage Summary :test_tube:');
|
||||||
|
console.log('');
|
||||||
|
console.log('| Metric | Coverage | Covered / Total |');
|
||||||
|
console.log('|--------|----------|-----------------|');
|
||||||
|
console.log(`| Lines | ${pct(totals.lines.covered, totals.lines.total)}% | ${totals.lines.covered} / ${totals.lines.total} |`);
|
||||||
|
console.log(`| Statements | ${pct(totals.statements.covered, totals.statements.total)}% | ${totals.statements.covered} / ${totals.statements.total} |`);
|
||||||
|
console.log(`| Branches | ${pct(totals.branches.covered, totals.branches.total)}% | ${totals.branches.covered} / ${totals.branches.total} |`);
|
||||||
|
console.log(`| Functions | ${pct(totals.functions.covered, totals.functions.total)}% | ${totals.functions.covered} / ${totals.functions.total} |`);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('<details><summary>File coverage (lowest lines first)</summary>');
|
||||||
|
console.log('');
|
||||||
|
console.log('```');
|
||||||
|
fileSummaries
|
||||||
|
.sort((a, b) => (a.pct - b.pct) || (b.lines.total - a.lines.total))
|
||||||
|
.slice(0, 25)
|
||||||
|
.forEach(({ file, pct, lines }) => {
|
||||||
|
console.log(`${pct.toFixed(2)}%\t${lines.covered}/${lines.total}\t${file}`);
|
||||||
|
});
|
||||||
|
console.log('```');
|
||||||
|
console.log('</details>');
|
||||||
|
|
||||||
|
if (coverage) {
|
||||||
|
const pctValue = (covered, tot) => {
|
||||||
|
if (tot === 0) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return ((covered / tot) * 100)
|
||||||
|
.toFixed(2)
|
||||||
|
.replace(/\.?0+$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLineRanges = (lines) => {
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const ranges = [];
|
||||||
|
let start = lines[0];
|
||||||
|
let end = lines[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < lines.length; i += 1) {
|
||||||
|
const current = lines[i];
|
||||||
|
if (current === end + 1) {
|
||||||
|
end = current;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||||
|
start = current;
|
||||||
|
end = current;
|
||||||
|
}
|
||||||
|
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||||
|
return ranges.join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableTotals = {
|
||||||
|
statements: { covered: 0, total: 0 },
|
||||||
|
branches: { covered: 0, total: 0 },
|
||||||
|
functions: { covered: 0, total: 0 },
|
||||||
|
lines: { covered: 0, total: 0 },
|
||||||
|
};
|
||||||
|
const tableRows = Object.entries(coverage)
|
||||||
|
.map(([file, entry]) => {
|
||||||
|
const fileCoverage = getFileCoverage(entry);
|
||||||
|
const lineHits = getLineHits(entry, fileCoverage);
|
||||||
|
const statementHits = entry.s ?? {};
|
||||||
|
const branchHits = entry.b ?? {};
|
||||||
|
const functionHits = entry.f ?? {};
|
||||||
|
|
||||||
|
const lineTotal = Object.keys(lineHits).length;
|
||||||
|
const lineCovered = Object.values(lineHits).filter((n) => n > 0).length;
|
||||||
|
const statementTotal = Object.keys(statementHits).length;
|
||||||
|
const statementCovered = Object.values(statementHits).filter((n) => n > 0).length;
|
||||||
|
const branchTotal = Object.values(branchHits).reduce((acc, branches) => acc + branches.length, 0);
|
||||||
|
const branchCovered = Object.values(branchHits).reduce(
|
||||||
|
(acc, branches) => acc + branches.filter((n) => n > 0).length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const functionTotal = Object.keys(functionHits).length;
|
||||||
|
const functionCovered = Object.values(functionHits).filter((n) => n > 0).length;
|
||||||
|
|
||||||
|
tableTotals.lines.total += lineTotal;
|
||||||
|
tableTotals.lines.covered += lineCovered;
|
||||||
|
tableTotals.statements.total += statementTotal;
|
||||||
|
tableTotals.statements.covered += statementCovered;
|
||||||
|
tableTotals.branches.total += branchTotal;
|
||||||
|
tableTotals.branches.covered += branchCovered;
|
||||||
|
tableTotals.functions.total += functionTotal;
|
||||||
|
tableTotals.functions.covered += functionCovered;
|
||||||
|
|
||||||
|
const uncoveredLines = getUncoveredLines(entry, fileCoverage, lineHits);
|
||||||
|
|
||||||
|
const filePath = entry.path ?? file;
|
||||||
|
const relativePath = path.isAbsolute(filePath)
|
||||||
|
? path.relative(process.cwd(), filePath)
|
||||||
|
: filePath;
|
||||||
|
|
||||||
|
return {
|
||||||
|
file: relativePath || file,
|
||||||
|
statements: pctValue(statementCovered, statementTotal),
|
||||||
|
branches: pctValue(branchCovered, branchTotal),
|
||||||
|
functions: pctValue(functionCovered, functionTotal),
|
||||||
|
lines: pctValue(lineCovered, lineTotal),
|
||||||
|
uncovered: formatLineRanges(uncoveredLines),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.file.localeCompare(b.file));
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: 'file', header: 'File', align: 'left' },
|
||||||
|
{ key: 'statements', header: '% Stmts', align: 'right' },
|
||||||
|
{ key: 'branches', header: '% Branch', align: 'right' },
|
||||||
|
{ key: 'functions', header: '% Funcs', align: 'right' },
|
||||||
|
{ key: 'lines', header: '% Lines', align: 'right' },
|
||||||
|
{ key: 'uncovered', header: 'Uncovered Line #s', align: 'left' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const allFilesRow = {
|
||||||
|
file: 'All files',
|
||||||
|
statements: pctValue(tableTotals.statements.covered, tableTotals.statements.total),
|
||||||
|
branches: pctValue(tableTotals.branches.covered, tableTotals.branches.total),
|
||||||
|
functions: pctValue(tableTotals.functions.covered, tableTotals.functions.total),
|
||||||
|
lines: pctValue(tableTotals.lines.covered, tableTotals.lines.total),
|
||||||
|
uncovered: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowsForOutput = [allFilesRow, ...tableRows];
|
||||||
|
const formatRow = (row) => `| ${columns
|
||||||
|
.map(({ key }) => String(row[key] ?? ''))
|
||||||
|
.join(' | ')} |`;
|
||||||
|
const headerRow = `| ${columns.map(({ header }) => header).join(' | ')} |`;
|
||||||
|
const dividerRow = `| ${columns
|
||||||
|
.map(({ align }) => (align === 'right' ? '---:' : ':---'))
|
||||||
|
.join(' | ')} |`;
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('<details><summary>Vitest coverage table</summary>');
|
||||||
|
console.log('');
|
||||||
|
console.log(headerRow);
|
||||||
|
console.log(dividerRow);
|
||||||
|
rowsForOutput.forEach((row) => console.log(formatRow(row)));
|
||||||
|
console.log('</details>');
|
||||||
|
}
|
||||||
|
NODE
|
||||||
|
|
||||||
|
- name: Upload Coverage Artifact
|
||||||
|
if: steps.coverage-summary.outputs.has_coverage == 'true'
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: blob-report-${{ matrix.shardIndex }}
|
name: web-coverage-report
|
||||||
path: web/.vitest-reports/*
|
path: web/coverage
|
||||||
include-hidden-files: true
|
retention-days: 30
|
||||||
retention-days: 1
|
if-no-files-found: error
|
||||||
|
|
||||||
merge-reports:
|
web-build:
|
||||||
name: Merge Test Reports
|
name: Web Build
|
||||||
if: ${{ !cancelled() }}
|
runs-on: ubuntu-latest
|
||||||
needs: [test]
|
|
||||||
runs-on: depot-ubuntu-24.04-4
|
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Check changed files
|
||||||
uses: ./.github/actions/setup-web
|
id: changed-files
|
||||||
|
uses: tj-actions/changed-files@v47
|
||||||
- name: Download blob reports
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
with:
|
||||||
path: web/.vitest-reports
|
files: |
|
||||||
pattern: blob-report-*
|
web/**
|
||||||
merge-multiple: true
|
.github/workflows/web-tests.yml
|
||||||
|
|
||||||
- name: Merge reports
|
- name: Install pnpm
|
||||||
run: vp test --merge-reports --coverage --silent=passed-only
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Report coverage
|
|
||||||
if: ${{ env.CODECOV_TOKEN != '' }}
|
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
|
||||||
with:
|
with:
|
||||||
directory: web/coverage
|
package_json_file: web/package.json
|
||||||
flags: web
|
run_install: false
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
dify-ui-test:
|
- name: Setup NodeJS
|
||||||
name: dify-ui Tests
|
uses: actions/setup-node@v6
|
||||||
runs-on: depot-ubuntu-24.04-4
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
working-directory: ./packages/dify-ui
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Setup web environment
|
- name: Web dependencies
|
||||||
uses: ./.github/actions/setup-web
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Install Chromium for Browser Mode
|
- name: Web build check
|
||||||
run: vp exec playwright install --with-deps chromium
|
if: steps.changed-files.outputs.any_changed == 'true'
|
||||||
|
working-directory: ./web
|
||||||
- name: Run dify-ui tests
|
run: pnpm run build
|
||||||
run: vp test run --coverage --silent=passed-only
|
|
||||||
|
|
||||||
- name: Report coverage
|
|
||||||
if: ${{ env.CODECOV_TOKEN != '' }}
|
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
|
||||||
with:
|
|
||||||
directory: packages/dify-ui/coverage
|
|
||||||
flags: dify-ui
|
|
||||||
env:
|
|
||||||
CODECOV_TOKEN: ${{ env.CODECOV_TOKEN }}
|
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@ -203,32 +203,27 @@ 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/
|
|
||||||
api/.env.backup
|
api/.env.backup
|
||||||
/clickzetta
|
/clickzetta
|
||||||
|
|
||||||
@ -240,15 +235,6 @@ 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
|
||||||
|
|
||||||
# Code Agent Folder
|
|
||||||
.qoder/*
|
|
||||||
|
|
||||||
.eslintcache
|
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
#!/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
|
|
||||||
17
.vscode/launch.json.template
vendored
17
.vscode/launch.json.template
vendored
@ -2,10 +2,21 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: API (gevent)",
|
"name": "Python: Flask API",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/api/app.py",
|
"module": "flask",
|
||||||
|
"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",
|
||||||
@ -26,7 +37,7 @@
|
|||||||
"-c",
|
"-c",
|
||||||
"1",
|
"1",
|
||||||
"-Q",
|
"-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",
|
"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",
|
||||||
"--loglevel",
|
"--loglevel",
|
||||||
"INFO"
|
"INFO"
|
||||||
],
|
],
|
||||||
|
|||||||
@ -29,8 +29,8 @@ 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__`).
|
||||||
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check`, and avoid `any` types.
|
- **TypeScript**: Use the strict config, rely on ESLint (`pnpm lint:fix` preferred) plus `pnpm type-check:tsgo`, and avoid `any` types.
|
||||||
|
|
||||||
## General Practices
|
## General Practices
|
||||||
|
|
||||||
|
|||||||
44
Makefile
44
Makefile
@ -3,10 +3,6 @@ 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
|
||||||
@ -21,20 +17,15 @@ 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..."
|
||||||
@if [ ! -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
@cp -n docker/middleware.env.example docker/middleware.env 2>/dev/null || echo "Docker middleware.env already exists"
|
||||||
cp "$(DOCKER_MIDDLEWARE_ENV_EXAMPLE)" "$(DOCKER_MIDDLEWARE_ENV)"; \
|
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev up -d
|
||||||
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.local 2>/dev/null || echo "Web .env.local already exists"
|
@cp -n web/.env.example web/.env 2>/dev/null || echo "Web .env already exists"
|
||||||
@pnpm install
|
@cd web && pnpm install
|
||||||
@echo "✅ Web environment prepared (not started)"
|
@echo "✅ Web environment prepared (not started)"
|
||||||
|
|
||||||
# Step 3: Prepare API environment
|
# Step 3: Prepare API environment
|
||||||
@ -48,18 +39,12 @@ prepare-api:
|
|||||||
# Clean dev environment
|
# Clean dev environment
|
||||||
dev-clean:
|
dev-clean:
|
||||||
@echo "⚠️ Stopping Docker containers..."
|
@echo "⚠️ Stopping Docker containers..."
|
||||||
@if [ -f "$(DOCKER_MIDDLEWARE_ENV)" ]; then \
|
@cd docker && docker compose -f docker-compose.middleware.yaml --env-file middleware.env -p dify-middlewares-dev down
|
||||||
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"
|
||||||
|
|
||||||
@ -83,18 +68,12 @@ lint:
|
|||||||
@echo "✅ Linting complete"
|
@echo "✅ Linting complete"
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
@echo "📝 Running type checks (basedpyright + pyrefly + mypy)..."
|
@echo "📝 Running type checks (basedpyright + mypy + ty)..."
|
||||||
@./dev/basedpyright-check $(PATH_TO_CHECK)
|
@./dev/basedpyright-check $(PATH_TO_CHECK)
|
||||||
@./dev/pyrefly-check-local
|
@uv --directory api run mypy --exclude-gitignore --exclude 'tests/' --exclude 'migrations/' --check-untyped-defs --disable-error-code=import-untyped .
|
||||||
@uv --directory api run mypy --exclude-gitignore --exclude 'tests/' --exclude 'migrations/' --exclude 'dev/generate_swagger_specs.py' --check-untyped-defs --disable-error-code=import-untyped .
|
@cd api && uv run ty check
|
||||||
@echo "✅ Type checks complete"
|
@echo "✅ Type checks complete"
|
||||||
|
|
||||||
type-check-core:
|
|
||||||
@echo "📝 Running core type checks (basedpyright + mypy)..."
|
|
||||||
@./dev/basedpyright-check $(PATH_TO_CHECK)
|
|
||||||
@uv --directory api run mypy --exclude-gitignore --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 \
|
||||||
@ -108,7 +87,7 @@ test:
|
|||||||
# 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 -f web/Dockerfile -t $(WEB_IMAGE):$(VERSION) .
|
docker build -t $(WEB_IMAGE):$(VERSION) ./web
|
||||||
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
@echo "Web Docker image built successfully: $(WEB_IMAGE):$(VERSION)"
|
||||||
|
|
||||||
build-api:
|
build-api:
|
||||||
@ -147,14 +126,13 @@ 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 and remove dev data"
|
@echo " make dev-clean - Stop Docker middleware containers"
|
||||||
@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 type-check - Run type checks (basedpyright, mypy, ty)"
|
||||||
@echo " make type-check-core - Run core type checks (basedpyright, 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 ""
|
@echo ""
|
||||||
@echo "Docker Build Targets:"
|
@echo "Docker Build Targets:"
|
||||||
|
|||||||
27
README.md
27
README.md
@ -1,5 +1,9 @@
|
|||||||

|

|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
<a href="https://cloud.dify.ai">Dify Cloud</a> ·
|
||||||
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Self-hosting</a> ·
|
<a href="https://docs.dify.ai/getting-started/install-self-hosted">Self-hosting</a> ·
|
||||||
@ -53,14 +57,10 @@
|
|||||||
<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 platform for developing LLM applications. Its intuitive interface combines agentic AI workflows, RAG pipelines, agent capabilities, model management, observability features, and more—allowing you to quickly move from prototype to production.
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@ -137,7 +137,20 @@ Star Dify on GitHub and be instantly notified of new releases.
|
|||||||
|
|
||||||
### Custom configurations
|
### Custom configurations
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
@ -147,7 +160,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)
|
||||||
|
|||||||
124
api/.env.example
124
api/.env.example
@ -22,10 +22,10 @@ APP_WEB_URL=http://localhost:3000
|
|||||||
# Files URL
|
# Files URL
|
||||||
FILES_URL=http://localhost:5001
|
FILES_URL=http://localhost:5001
|
||||||
|
|
||||||
# INTERNAL_FILES_URL is used by services running in Docker to reach the API file endpoints.
|
# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
|
||||||
# For Docker Desktop (Mac/Windows), use http://host.docker.internal:5001 when the API runs on the host.
|
# Set this to the internal Docker service URL for proper plugin file access.
|
||||||
# For Docker Compose on Linux, use http://api:5001 when the API runs inside the Docker network.
|
# Example: INTERNAL_FILES_URL=http://api:5001
|
||||||
INTERNAL_FILES_URL=http://host.docker.internal:5001
|
INTERNAL_FILES_URL=http://127.0.0.1:5001
|
||||||
|
|
||||||
# TRIGGER URL
|
# TRIGGER URL
|
||||||
TRIGGER_URL=http://localhost:5001
|
TRIGGER_URL=http://localhost:5001
|
||||||
@ -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=true
|
ENABLE_COLLABORATION_MODE=false
|
||||||
|
|
||||||
# Access token expiration time in minutes
|
# Access token expiration time in minutes
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
ACCESS_TOKEN_EXPIRE_MINUTES=60
|
||||||
@ -45,8 +45,6 @@ REFRESH_TOKEN_EXPIRE_DAYS=30
|
|||||||
# redis configuration
|
# redis configuration
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
# Optional: limit total connections in connection pool (unset for default)
|
|
||||||
# REDIS_MAX_CONNECTIONS=200
|
|
||||||
REDIS_USERNAME=
|
REDIS_USERNAME=
|
||||||
REDIS_PASSWORD=difyai123456
|
REDIS_PASSWORD=difyai123456
|
||||||
REDIS_USE_SSL=false
|
REDIS_USE_SSL=false
|
||||||
@ -60,9 +58,6 @@ 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
|
||||||
@ -77,21 +72,10 @@ 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
|
||||||
@ -102,8 +86,6 @@ 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...
|
||||||
@ -121,7 +103,6 @@ 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
|
||||||
@ -147,8 +128,7 @@ 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
|
||||||
# Optional CloudBox ID for Aliyun OSS, DO NOT enable it if you are not using CloudBox.
|
ALIYUN_CLOUDBOX_ID=your-cloudbox-id
|
||||||
#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
|
||||||
@ -201,7 +181,7 @@ CONSOLE_CORS_ALLOW_ORIGINS=http://localhost:3000,*
|
|||||||
COOKIE_DOMAIN=
|
COOKIE_DOMAIN=
|
||||||
|
|
||||||
# Vector database configuration
|
# Vector database configuration
|
||||||
# Supported values are `weaviate`, `oceanbase`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`, `hologres`.
|
# Supported values are `weaviate`, `oceanbase`, `qdrant`, `milvus`, `myscale`, `relyt`, `pgvector`, `pgvecto-rs`, `chroma`, `opensearch`, `oracle`, `tencent`, `elasticsearch`, `elasticsearch-ja`, `analyticdb`, `couchbase`, `vikingdb`, `opengauss`, `tablestore`,`vastbase`,`tidb`,`tidb_on_qdrant`,`baidu`,`lindorm`,`huawei_cloud`,`upstash`, `matrixone`.
|
||||||
VECTOR_STORE=weaviate
|
VECTOR_STORE=weaviate
|
||||||
# Prefix used to create collection name in vector database
|
# Prefix used to create collection name in vector database
|
||||||
VECTOR_INDEX_NAME_PREFIX=Vector_index
|
VECTOR_INDEX_NAME_PREFIX=Vector_index
|
||||||
@ -209,6 +189,7 @@ VECTOR_INDEX_NAME_PREFIX=Vector_index
|
|||||||
# Weaviate configuration
|
# Weaviate configuration
|
||||||
WEAVIATE_ENDPOINT=http://localhost:8080
|
WEAVIATE_ENDPOINT=http://localhost:8080
|
||||||
WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
WEAVIATE_API_KEY=WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih
|
||||||
|
WEAVIATE_GRPC_ENABLED=false
|
||||||
WEAVIATE_BATCH_SIZE=100
|
WEAVIATE_BATCH_SIZE=100
|
||||||
WEAVIATE_TOKENIZATION=word
|
WEAVIATE_TOKENIZATION=word
|
||||||
|
|
||||||
@ -238,20 +219,6 @@ COUCHBASE_PASSWORD=password
|
|||||||
COUCHBASE_BUCKET_NAME=Embeddings
|
COUCHBASE_BUCKET_NAME=Embeddings
|
||||||
COUCHBASE_SCOPE_NAME=_default
|
COUCHBASE_SCOPE_NAME=_default
|
||||||
|
|
||||||
# Hologres configuration
|
|
||||||
# access_key_id is used as the PG username, access_key_secret is used as the PG password
|
|
||||||
HOLOGRES_HOST=
|
|
||||||
HOLOGRES_PORT=80
|
|
||||||
HOLOGRES_DATABASE=
|
|
||||||
HOLOGRES_ACCESS_KEY_ID=
|
|
||||||
HOLOGRES_ACCESS_KEY_SECRET=
|
|
||||||
HOLOGRES_SCHEMA=public
|
|
||||||
HOLOGRES_TOKENIZER=jieba
|
|
||||||
HOLOGRES_DISTANCE_METHOD=Cosine
|
|
||||||
HOLOGRES_BASE_QUANTIZATION_TYPE=rabitq
|
|
||||||
HOLOGRES_MAX_DEGREE=64
|
|
||||||
HOLOGRES_EF_CONSTRUCTION=400
|
|
||||||
|
|
||||||
# Milvus configuration
|
# Milvus configuration
|
||||||
MILVUS_URI=http://127.0.0.1:19530
|
MILVUS_URI=http://127.0.0.1:19530
|
||||||
MILVUS_TOKEN=
|
MILVUS_TOKEN=
|
||||||
@ -374,9 +341,6 @@ BAIDU_VECTOR_DB_SHARD=1
|
|||||||
BAIDU_VECTOR_DB_REPLICAS=3
|
BAIDU_VECTOR_DB_REPLICAS=3
|
||||||
BAIDU_VECTOR_DB_INVERTED_INDEX_ANALYZER=DEFAULT_ANALYZER
|
BAIDU_VECTOR_DB_INVERTED_INDEX_ANALYZER=DEFAULT_ANALYZER
|
||||||
BAIDU_VECTOR_DB_INVERTED_INDEX_PARSER_MODE=COARSE_MODE
|
BAIDU_VECTOR_DB_INVERTED_INDEX_PARSER_MODE=COARSE_MODE
|
||||||
BAIDU_VECTOR_DB_AUTO_BUILD_ROW_COUNT_INCREMENT=500
|
|
||||||
BAIDU_VECTOR_DB_AUTO_BUILD_ROW_COUNT_INCREMENT_RATIO=0.05
|
|
||||||
BAIDU_VECTOR_DB_REBUILD_INDEX_TIMEOUT_IN_SECONDS=300
|
|
||||||
|
|
||||||
# Upstash configuration
|
# Upstash configuration
|
||||||
UPSTASH_VECTOR_URL=your-server-url
|
UPSTASH_VECTOR_URL=your-server-url
|
||||||
@ -387,7 +351,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_SCHEME=http
|
VIKINGDB_SCHEMA=http
|
||||||
VIKINGDB_CONNECTION_TIMEOUT=30
|
VIKINGDB_CONNECTION_TIMEOUT=30
|
||||||
VIKINGDB_SOCKET_TIMEOUT=30
|
VIKINGDB_SOCKET_TIMEOUT=30
|
||||||
|
|
||||||
@ -438,6 +402,8 @@ 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
|
||||||
@ -718,6 +684,22 @@ 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,26 +729,52 @@ 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=
|
||||||
|
|
||||||
# Redis URL used for event bus between API and
|
# 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.
|
||||||
|
# Falls back to FILES_URL if not specified.
|
||||||
|
# For local development: http://localhost:5001
|
||||||
|
# For Docker deployment: http://api:5001
|
||||||
|
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 PubSub between API and
|
||||||
# celery worker
|
# celery worker
|
||||||
# defaults to url constructed from `REDIS_*`
|
# defaults to url constructed from `REDIS_*`
|
||||||
# configurations
|
# configurations
|
||||||
EVENT_BUS_REDIS_URL=
|
PUBSUB_REDIS_URL=
|
||||||
# Event transport type. Options are:
|
# Pub/sub channel type for streaming events.
|
||||||
|
# valid options are:
|
||||||
#
|
#
|
||||||
# - pubsub: normal Pub/Sub (at-most-once)
|
# - pubsub: for normal Pub/Sub
|
||||||
# - sharded: sharded Pub/Sub (at-most-once)
|
# - sharded: for sharded Pub/Sub
|
||||||
# - streams: Redis Streams (at-least-once, recommended to avoid subscriber races)
|
|
||||||
#
|
#
|
||||||
# Note: Before enabling 'streams' in production, estimate your expected event volume and retention needs.
|
# It's highly recommended to use sharded Pub/Sub AND redis cluster
|
||||||
# Configure Redis memory limits and stream trimming appropriately (e.g., MAXLEN and key expiry) to reduce
|
# for large deployments.
|
||||||
# the risk of data loss from Redis auto-eviction under memory pressure.
|
PUBSUB_REDIS_CHANNEL_TYPE=pubsub
|
||||||
# Also accepts ENV: EVENT_BUS_REDIS_CHANNEL_TYPE.
|
# Whether to use Redis cluster mode while running
|
||||||
EVENT_BUS_REDIS_CHANNEL_TYPE=pubsub
|
# PubSub.
|
||||||
# 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
|
PUBSUB_REDIS_USE_CLUSTERS=false
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@ -1,14 +1,420 @@
|
|||||||
[importlinter]
|
[importlinter]
|
||||||
root_packages =
|
root_packages =
|
||||||
core
|
core
|
||||||
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 =
|
||||||
|
core.workflow
|
||||||
|
ignore_imports =
|
||||||
|
core.workflow.nodes.base.node -> core.workflow.graph_events
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.workflow.graph_events
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.workflow.graph_events
|
||||||
|
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.app.workflow.node_factory
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.app.workflow.node_factory
|
||||||
|
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.workflow.graph_engine
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.workflow.graph
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.workflow.graph_engine.command_channels
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.workflow.graph_engine
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.workflow.graph
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.workflow.graph_engine.command_channels
|
||||||
|
# TODO(QuantumGhost): fix the import violation later
|
||||||
|
core.workflow.entities.pause_reason -> core.workflow.nodes.human_input.entities
|
||||||
|
|
||||||
|
[importlinter:contract:workflow-infrastructure-dependencies]
|
||||||
|
name = Workflow Infrastructure Dependencies
|
||||||
|
type = forbidden
|
||||||
|
source_modules =
|
||||||
|
core.workflow
|
||||||
|
forbidden_modules =
|
||||||
|
extensions.ext_database
|
||||||
|
extensions.ext_redis
|
||||||
|
allow_indirect_imports = True
|
||||||
|
ignore_imports =
|
||||||
|
core.workflow.nodes.agent.agent_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.file_saver -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.llm_utils -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.tool.tool_node -> extensions.ext_database
|
||||||
|
core.workflow.graph_engine.command_channels.redis_channel -> extensions.ext_redis
|
||||||
|
core.workflow.graph_engine.manager -> extensions.ext_redis
|
||||||
|
# TODO(QuantumGhost): use DI to avoid depending on global DB.
|
||||||
|
core.workflow.nodes.human_input.human_input_node -> extensions.ext_database
|
||||||
|
|
||||||
|
[importlinter:contract:workflow-external-imports]
|
||||||
|
name = Workflow External Imports
|
||||||
|
type = forbidden
|
||||||
|
source_modules =
|
||||||
|
core.workflow
|
||||||
|
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.model_manager
|
||||||
|
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 =
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.app.workflow.node_factory
|
||||||
|
core.workflow.graph_engine.command_channels.redis_channel -> extensions.ext_redis
|
||||||
|
core.workflow.workflow_entry -> core.app.workflow.layers.observability
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.model_manager
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.provider_manager
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.tools.tool_manager
|
||||||
|
core.workflow.nodes.code.code_node -> core.helper.code_executor.code_executor
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> models.model
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> models.tools
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> services.datasource_provider_service
|
||||||
|
core.workflow.nodes.document_extractor.node -> configs
|
||||||
|
core.workflow.nodes.document_extractor.node -> core.file.file_manager
|
||||||
|
core.workflow.nodes.document_extractor.node -> core.helper.ssrf_proxy
|
||||||
|
core.workflow.nodes.http_request.entities -> configs
|
||||||
|
core.workflow.nodes.http_request.executor -> configs
|
||||||
|
core.workflow.nodes.http_request.executor -> core.file.file_manager
|
||||||
|
core.workflow.nodes.http_request.node -> configs
|
||||||
|
core.workflow.nodes.http_request.node -> core.tools.tool_file_manager
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.app.workflow.node_factory
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> core.rag.index_processor.index_processor_factory
|
||||||
|
core.workflow.nodes.llm.llm_utils -> configs
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.file.models
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.model_manager
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.model_runtime.model_providers.__base.large_language_model
|
||||||
|
core.workflow.nodes.llm.llm_utils -> models.model
|
||||||
|
core.workflow.nodes.llm.llm_utils -> models.provider
|
||||||
|
core.workflow.nodes.llm.llm_utils -> services.credit_pool_service
|
||||||
|
core.workflow.nodes.llm.node -> core.tools.signature
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.callback_handler.workflow_tool_callback_handler
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.tools.tool_engine
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.tools.tool_manager
|
||||||
|
core.workflow.workflow_entry -> configs
|
||||||
|
core.workflow.workflow_entry -> models.workflow
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.agent.entities
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.agent.plugin_entities
|
||||||
|
core.workflow.nodes.base.node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.human_input.human_input_node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node -> core.app.app_config.entities
|
||||||
|
core.workflow.nodes.llm.node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.advanced_prompt_transform
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.simple_prompt_transform
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.model_runtime.model_providers.__base.large_language_model
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.prompt.advanced_prompt_transform
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.prompt.simple_prompt_transform
|
||||||
|
core.workflow.nodes.start.entities -> core.app.app_config.entities
|
||||||
|
core.workflow.nodes.start.start_node -> core.app.app_config.entities
|
||||||
|
core.workflow.workflow_entry -> core.app.apps.exc
|
||||||
|
core.workflow.workflow_entry -> core.app.entities.app_invoke_entities
|
||||||
|
core.workflow.workflow_entry -> core.app.workflow.node_factory
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.datasource.datasource_manager
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.datasource.utils.message_transformer
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.entities.provider_entities
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.model_manager
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.model_manager
|
||||||
|
core.workflow.node_events.node -> core.file
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.file
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.file
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.file.enums
|
||||||
|
core.workflow.nodes.document_extractor.node -> core.file
|
||||||
|
core.workflow.nodes.http_request.executor -> core.file.enums
|
||||||
|
core.workflow.nodes.http_request.node -> core.file
|
||||||
|
core.workflow.nodes.http_request.node -> core.file.file_manager
|
||||||
|
core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node -> core.file.models
|
||||||
|
core.workflow.nodes.list_operator.node -> core.file
|
||||||
|
core.workflow.nodes.llm.file_saver -> core.file
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.variables.segments
|
||||||
|
core.workflow.nodes.llm.node -> core.file
|
||||||
|
core.workflow.nodes.llm.node -> core.file.file_manager
|
||||||
|
core.workflow.nodes.llm.node -> core.file.models
|
||||||
|
core.workflow.nodes.loop.entities -> core.variables.types
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.file
|
||||||
|
core.workflow.nodes.protocols -> core.file
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.file.models
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.file
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.tools.utils.message_transformer
|
||||||
|
core.workflow.nodes.tool.tool_node -> models
|
||||||
|
core.workflow.nodes.trigger_webhook.node -> core.file
|
||||||
|
core.workflow.runtime.variable_pool -> core.file
|
||||||
|
core.workflow.runtime.variable_pool -> core.file.file_manager
|
||||||
|
core.workflow.system_variable -> core.file.models
|
||||||
|
core.workflow.utils.condition.processor -> core.file
|
||||||
|
core.workflow.utils.condition.processor -> core.file.file_manager
|
||||||
|
core.workflow.workflow_entry -> core.file.models
|
||||||
|
core.workflow.workflow_type_encoder -> core.file.models
|
||||||
|
core.workflow.nodes.agent.agent_node -> models.model
|
||||||
|
core.workflow.nodes.code.code_node -> core.helper.code_executor.code_node_provider
|
||||||
|
core.workflow.nodes.code.code_node -> core.helper.code_executor.javascript.javascript_code_provider
|
||||||
|
core.workflow.nodes.code.code_node -> core.helper.code_executor.python3.python3_code_provider
|
||||||
|
core.workflow.nodes.code.entities -> core.helper.code_executor.code_executor
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.variables.variables
|
||||||
|
core.workflow.nodes.http_request.executor -> core.helper.ssrf_proxy
|
||||||
|
core.workflow.nodes.http_request.node -> core.helper.ssrf_proxy
|
||||||
|
core.workflow.nodes.llm.file_saver -> core.helper.ssrf_proxy
|
||||||
|
core.workflow.nodes.llm.node -> core.helper.code_executor
|
||||||
|
core.workflow.nodes.template_transform.template_renderer -> core.helper.code_executor.code_executor
|
||||||
|
core.workflow.nodes.llm.node -> core.llm_generator.output_parser.errors
|
||||||
|
core.workflow.nodes.llm.node -> core.llm_generator.output_parser.structured_output
|
||||||
|
core.workflow.nodes.llm.node -> core.model_manager
|
||||||
|
core.workflow.nodes.agent.entities -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.llm.entities -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.llm.llm_utils -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.llm.node -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.llm.node -> core.prompt.utils.prompt_message_util
|
||||||
|
core.workflow.nodes.parameter_extractor.entities -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.prompt.utils.prompt_message_util
|
||||||
|
core.workflow.nodes.question_classifier.entities -> core.prompt.entities.advanced_prompt_entities
|
||||||
|
core.workflow.nodes.question_classifier.question_classifier_node -> core.prompt.utils.prompt_message_util
|
||||||
|
core.workflow.nodes.knowledge_index.entities -> core.rag.retrieval.retrieval_methods
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> core.rag.retrieval.retrieval_methods
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> models.dataset
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> services.summary_index_service
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> tasks.generate_summary_index_task
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> core.rag.index_processor.processor.paragraph_index_processor
|
||||||
|
core.workflow.nodes.llm.node -> models.dataset
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.tools.utils.message_transformer
|
||||||
|
core.workflow.nodes.llm.file_saver -> core.tools.signature
|
||||||
|
core.workflow.nodes.llm.file_saver -> core.tools.tool_file_manager
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.tools.errors
|
||||||
|
core.workflow.conversation_variable_updater -> core.variables
|
||||||
|
core.workflow.graph_engine.entities.commands -> core.variables.variables
|
||||||
|
core.workflow.nodes.agent.agent_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.answer.answer_node -> core.variables
|
||||||
|
core.workflow.nodes.code.code_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.code.code_node -> core.variables.types
|
||||||
|
core.workflow.nodes.code.entities -> core.variables.types
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.document_extractor.node -> core.variables
|
||||||
|
core.workflow.nodes.document_extractor.node -> core.variables.segments
|
||||||
|
core.workflow.nodes.http_request.executor -> core.variables.segments
|
||||||
|
core.workflow.nodes.http_request.node -> core.variables.segments
|
||||||
|
core.workflow.nodes.human_input.entities -> core.variables.consts
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.variables
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.iteration.iteration_node -> core.variables.variables
|
||||||
|
core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node -> core.variables
|
||||||
|
core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.list_operator.node -> core.variables
|
||||||
|
core.workflow.nodes.list_operator.node -> core.variables.segments
|
||||||
|
core.workflow.nodes.llm.node -> core.variables
|
||||||
|
core.workflow.nodes.loop.loop_node -> core.variables
|
||||||
|
core.workflow.nodes.parameter_extractor.entities -> core.variables.types
|
||||||
|
core.workflow.nodes.parameter_extractor.exc -> core.variables.types
|
||||||
|
core.workflow.nodes.parameter_extractor.parameter_extractor_node -> core.variables.types
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.tool.tool_node -> core.variables.variables
|
||||||
|
core.workflow.nodes.trigger_webhook.node -> core.variables.types
|
||||||
|
core.workflow.nodes.trigger_webhook.node -> core.variables.variables
|
||||||
|
core.workflow.nodes.variable_aggregator.entities -> core.variables.types
|
||||||
|
core.workflow.nodes.variable_aggregator.variable_aggregator_node -> core.variables.segments
|
||||||
|
core.workflow.nodes.variable_assigner.common.helpers -> core.variables
|
||||||
|
core.workflow.nodes.variable_assigner.common.helpers -> core.variables.consts
|
||||||
|
core.workflow.nodes.variable_assigner.common.helpers -> core.variables.types
|
||||||
|
core.workflow.nodes.variable_assigner.v1.node -> core.variables
|
||||||
|
core.workflow.nodes.variable_assigner.v2.helpers -> core.variables
|
||||||
|
core.workflow.nodes.variable_assigner.v2.node -> core.variables
|
||||||
|
core.workflow.nodes.variable_assigner.v2.node -> core.variables.consts
|
||||||
|
core.workflow.runtime.graph_runtime_state_protocol -> core.variables.segments
|
||||||
|
core.workflow.runtime.read_only_wrappers -> core.variables.segments
|
||||||
|
core.workflow.runtime.variable_pool -> core.variables
|
||||||
|
core.workflow.runtime.variable_pool -> core.variables.consts
|
||||||
|
core.workflow.runtime.variable_pool -> core.variables.segments
|
||||||
|
core.workflow.runtime.variable_pool -> core.variables.variables
|
||||||
|
core.workflow.utils.condition.processor -> core.variables
|
||||||
|
core.workflow.utils.condition.processor -> core.variables.segments
|
||||||
|
core.workflow.variable_loader -> core.variables
|
||||||
|
core.workflow.variable_loader -> core.variables.consts
|
||||||
|
core.workflow.workflow_type_encoder -> core.variables
|
||||||
|
core.workflow.graph_engine.manager -> extensions.ext_redis
|
||||||
|
core.workflow.nodes.agent.agent_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.datasource.datasource_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.knowledge_index.knowledge_index_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.file_saver -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.llm_utils -> extensions.ext_database
|
||||||
|
core.workflow.nodes.llm.node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.tool.tool_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.human_input.human_input_node -> extensions.ext_database
|
||||||
|
core.workflow.nodes.human_input.human_input_node -> core.repositories.human_input_repository
|
||||||
|
core.workflow.workflow_entry -> extensions.otel.runtime
|
||||||
|
core.workflow.nodes.agent.agent_node -> models
|
||||||
|
core.workflow.nodes.base.node -> models.enums
|
||||||
|
core.workflow.nodes.llm.llm_utils -> models.provider_ids
|
||||||
|
core.workflow.nodes.llm.node -> models.model
|
||||||
|
core.workflow.workflow_entry -> models.enums
|
||||||
|
core.workflow.nodes.agent.agent_node -> services
|
||||||
|
core.workflow.nodes.tool.tool_node -> services
|
||||||
|
|
||||||
|
[importlinter:contract:model-runtime-no-internal-imports]
|
||||||
|
name = Model Runtime Internal Imports
|
||||||
|
type = forbidden
|
||||||
|
source_modules =
|
||||||
|
core.model_runtime
|
||||||
|
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.model_manager
|
||||||
|
core.moderation
|
||||||
|
core.ops
|
||||||
|
core.plugin
|
||||||
|
core.prompt
|
||||||
|
core.provider_manager
|
||||||
|
core.rag
|
||||||
|
core.repositories
|
||||||
|
core.schemas
|
||||||
|
core.tools
|
||||||
|
core.trigger
|
||||||
|
core.variables
|
||||||
|
core.workflow
|
||||||
|
ignore_imports =
|
||||||
|
core.model_runtime.model_providers.__base.ai_model -> configs
|
||||||
|
core.model_runtime.model_providers.__base.ai_model -> extensions.ext_redis
|
||||||
|
core.model_runtime.model_providers.__base.large_language_model -> configs
|
||||||
|
core.model_runtime.model_providers.__base.text_embedding_model -> core.entities.embedding_type
|
||||||
|
core.model_runtime.model_providers.model_provider_factory -> configs
|
||||||
|
core.model_runtime.model_providers.model_provider_factory -> extensions.ext_redis
|
||||||
|
core.model_runtime.model_providers.model_provider_factory -> models.provider_ids
|
||||||
|
|
||||||
|
[importlinter:contract:rsc]
|
||||||
|
name = RSC
|
||||||
|
type = layers
|
||||||
|
layers =
|
||||||
|
graph_engine
|
||||||
|
response_coordinator
|
||||||
|
containers =
|
||||||
|
core.workflow.graph_engine
|
||||||
|
|
||||||
|
[importlinter:contract:worker]
|
||||||
|
name = Worker
|
||||||
|
type = layers
|
||||||
|
layers =
|
||||||
|
graph_engine
|
||||||
|
worker
|
||||||
|
containers =
|
||||||
|
core.workflow.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 =
|
||||||
|
core.workflow.graph_engine
|
||||||
|
|
||||||
|
[importlinter:contract:domain-isolation]
|
||||||
|
name = Domain Model Isolation
|
||||||
|
type = forbidden
|
||||||
|
source_modules =
|
||||||
|
core.workflow.graph_engine.domain
|
||||||
|
forbidden_modules =
|
||||||
|
core.workflow.graph_engine.worker_management
|
||||||
|
core.workflow.graph_engine.command_channels
|
||||||
|
core.workflow.graph_engine.layers
|
||||||
|
core.workflow.graph_engine.protocols
|
||||||
|
|
||||||
|
[importlinter:contract:worker-management]
|
||||||
|
name = Worker Management
|
||||||
|
type = forbidden
|
||||||
|
source_modules =
|
||||||
|
core.workflow.graph_engine.worker_management
|
||||||
|
forbidden_modules =
|
||||||
|
core.workflow.graph_engine.orchestration
|
||||||
|
core.workflow.graph_engine.command_processing
|
||||||
|
core.workflow.graph_engine.event_management
|
||||||
|
|
||||||
|
|
||||||
|
[importlinter:contract:graph-traversal-components]
|
||||||
|
name = Graph Traversal Components
|
||||||
|
type = layers
|
||||||
|
layers =
|
||||||
|
edge_processor
|
||||||
|
skip_propagator
|
||||||
|
containers =
|
||||||
|
core.workflow.graph_engine.graph_traversal
|
||||||
|
|
||||||
|
[importlinter:contract:command-channels]
|
||||||
|
name = Command Channels Independence
|
||||||
|
type = independence
|
||||||
|
modules =
|
||||||
|
core.workflow.graph_engine.command_channels.in_memory_channel
|
||||||
|
core.workflow.graph_engine.command_channels.redis_channel
|
||||||
|
|||||||
@ -69,6 +69,8 @@ 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
|
||||||
@ -82,6 +84,7 @@ 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
|
||||||
@ -90,22 +93,38 @@ 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
|
||||||
]
|
]
|
||||||
|
"core/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"]
|
|
||||||
18
api/.vscode/launch.json.example
vendored
18
api/.vscode/launch.json.example
vendored
@ -3,21 +3,29 @@
|
|||||||
"compounds": [
|
"compounds": [
|
||||||
{
|
{
|
||||||
"name": "Launch Flask and Celery",
|
"name": "Launch Flask and Celery",
|
||||||
"configurations": ["Python: API (gevent)", "Python: Celery"]
|
"configurations": ["Python: Flask", "Python: Celery"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python: API (gevent)",
|
"name": "Python: Flask",
|
||||||
"consoleName": "API",
|
"consoleName": "Flask",
|
||||||
"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",
|
||||||
"program": "${workspaceFolder}/app.py",
|
"module": "flask",
|
||||||
"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",
|
||||||
|
|||||||
@ -62,23 +62,7 @@ This is the default standard for backend code in this repo. Follow it for new co
|
|||||||
|
|
||||||
- Code should usually include type annotations that match the repo’s current Python version (avoid untyped public APIs and “mystery” values).
|
- Code should usually include type annotations that match the repo’s current Python version (avoid untyped public APIs and “mystery” values).
|
||||||
- Prefer modern typing forms (e.g. `list[str]`, `dict[str, int]`) and avoid `Any` unless there’s a strong reason.
|
- Prefer modern typing forms (e.g. `list[str]`, `dict[str, int]`) and avoid `Any` unless there’s a strong reason.
|
||||||
- For dictionary-like data with known keys and value types, prefer `TypedDict` over `dict[...]` or `Mapping[...]`.
|
- For classes, declare member variables at the top of the class body (before `__init__`) so the class shape is obvious at a glance:
|
||||||
- For optional keys in typed payloads, use `NotRequired[...]` (or `total=False` when most fields are optional).
|
|
||||||
- Keep `dict[...]` / `Mapping[...]` for truly dynamic key spaces where the key set is unknown.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import NotRequired, TypedDict
|
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(TypedDict):
|
|
||||||
user_id: str
|
|
||||||
email: str
|
|
||||||
created_at: datetime
|
|
||||||
nickname: NotRequired[str]
|
|
||||||
```
|
|
||||||
|
|
||||||
- For classes, declare all member variables explicitly with types at the top of the class body (before `__init__`), even when the class is not a dataclass or Pydantic model, so the class shape is obvious at a glance:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -193,10 +177,6 @@ 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 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 with `ResponseModel.model_validate(...).model_dump(...)`,
|
|
||||||
and avoid adding new legacy `ns.model(...)`, `@marshal_with(...)`, or GET `@ns.expect(...)` patterns.
|
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
|
|||||||
@ -21,9 +21,8 @@ RUN apt-get update \
|
|||||||
# for building gmpy2
|
# for building gmpy2
|
||||||
libmpfr-dev libmpc-dev
|
libmpfr-dev libmpc-dev
|
||||||
|
|
||||||
# Install Python dependencies (workspace members under providers/vdb/)
|
# Install Python dependencies
|
||||||
COPY pyproject.toml uv.lock ./
|
COPY pyproject.toml uv.lock ./
|
||||||
COPY providers ./providers
|
|
||||||
RUN uv sync --locked --no-dev
|
RUN uv sync --locked --no-dev
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
@ -98,7 +97,7 @@ ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
|
|||||||
|
|
||||||
# Download nltk data
|
# Download nltk data
|
||||||
RUN mkdir -p /usr/local/share/nltk_data \
|
RUN mkdir -p /usr/local/share/nltk_data \
|
||||||
&& NLTK_DATA=/usr/local/share/nltk_data python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger'); nltk.download('stopwords')" \
|
&& NLTK_DATA=/usr/local/share/nltk_data python -c "import nltk; from unstructured.nlp.tokenize import download_nltk_packages; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger'); nltk.download('stopwords'); download_nltk_packages()" \
|
||||||
&& chmod -R 755 /usr/local/share/nltk_data
|
&& chmod -R 755 /usr/local/share/nltk_data
|
||||||
|
|
||||||
ENV TIKTOKEN_CACHE_DIR=/app/api/.tiktoken_cache
|
ENV TIKTOKEN_CACHE_DIR=/app/api/.tiktoken_cache
|
||||||
|
|||||||
@ -40,11 +40,9 @@ 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. Optional: start the worker service (async tasks, runs from `api`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./dev/start-worker
|
./dev/start-worker
|
||||||
@ -56,6 +54,86 @@ 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]
|
||||||
@ -101,11 +179,3 @@ The scripts resolve paths relative to their location, so you can run them from a
|
|||||||
uv run ruff format ./ # Format code
|
uv run ruff format ./ # Format code
|
||||||
uv run basedpyright . # Type checking
|
uv run basedpyright . # 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
|
|
||||||
|
|||||||
9
api/agent-notes/configs/feature/__init__.py.md
Normal file
9
api/agent-notes/configs/feature/__init__.py.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
9
api/agent-notes/controllers/files/__init__.py.md
Normal file
9
api/agent-notes/controllers/files/__init__.py.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
14
api/agent-notes/controllers/files/app_assets_download.py.md
Normal file
14
api/agent-notes/controllers/files/app_assets_download.py.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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.
|
||||||
13
api/agent-notes/controllers/files/app_assets_upload.py.md
Normal file
13
api/agent-notes/controllers/files/app_assets_upload.py.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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.
|
||||||
14
api/agent-notes/controllers/files/sandbox_archive.py.md
Normal file
14
api/agent-notes/controllers/files/sandbox_archive.py.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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.
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
14
api/agent-notes/core/app_assets/builder/skill_builder.py.md
Normal file
14
api/agent-notes/core/app_assets/builder/skill_builder.py.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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.
|
||||||
9
api/agent-notes/core/app_assets/converters.py.md
Normal file
9
api/agent-notes/core/app_assets/converters.py.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
14
api/agent-notes/core/app_assets/packager/zip_packager.py.md
Normal file
14
api/agent-notes/core/app_assets/packager/zip_packager.py.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# 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.
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
20
api/agent-notes/core/app_assets/storage.py.md
Normal file
20
api/agent-notes/core/app_assets/storage.py.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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.
|
||||||
10
api/agent-notes/core/app_bundle/source_zip_extractor.py.md
Normal file
10
api/agent-notes/core/app_bundle/source_zip_extractor.py.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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.
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
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.
|
||||||
9
api/agent-notes/core/sandbox/sandbox.py.md
Normal file
9
api/agent-notes/core/sandbox/sandbox.py.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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.
|
||||||
2
api/agent-notes/core/sandbox/security/__init__.py.md
Normal file
2
api/agent-notes/core/sandbox/security/__init__.py.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Summary:
|
||||||
|
- Sandbox security helper modules.
|
||||||
13
api/agent-notes/core/sandbox/security/archive_signer.py.md
Normal file
13
api/agent-notes/core/sandbox/security/archive_signer.py.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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.
|
||||||
12
api/agent-notes/core/sandbox/storage/archive_storage.py.md
Normal file
12
api/agent-notes/core/sandbox/storage/archive_storage.py.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Summary:
|
||||||
|
- Manages sandbox archive uploads/downloads for workspace persistence.
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
- Archive storage key is sandbox/<tenant_id>/<sandbox_id>.tar.gz.
|
||||||
|
- Signed URLs are tenant-scoped and use external files URL.
|
||||||
|
|
||||||
|
Edge Cases:
|
||||||
|
- Missing archive skips mount.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Covered indirectly via sandbox integration tests.
|
||||||
9
api/agent-notes/core/skill/skill_manager.py.md
Normal file
9
api/agent-notes/core/skill/skill_manager.py.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Summary:
|
||||||
|
Summary:
|
||||||
|
- Loads/saves skill bundles to app asset storage.
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
- Skill bundles use AppAssetStorage refs and JSON serialization.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Covered by skill bundle build/load unit tests.
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
# E2B Sandbox Provider Notes
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
- Implements the E2B-backed `VirtualEnvironment` provider and bootstraps sandbox metadata, file I/O, and command execution.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
- Sandbox metadata is gathered during `_construct_environment` using the E2B SDK before returning `Metadata`.
|
||||||
|
- Architecture/OS detection uses a single `uname -m -s` call split by whitespace to reduce round-trips.
|
||||||
|
- Command execution streams stdout/stderr through `QueueTransportReadCloser`; stdin is unsupported.
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
- `release_environment` raises when sandbox termination fails.
|
||||||
|
- `execute_command` runs in a background thread; consumers must read stdout/stderr until EOF.
|
||||||
|
|
||||||
|
## Tests/Verification
|
||||||
|
- None yet. Add targeted service tests when behavior changes.
|
||||||
14
api/agent-notes/services/app_asset_service.py.md
Normal file
14
api/agent-notes/services/app_asset_service.py.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
Summary:
|
||||||
|
- App asset CRUD, publish/build pipeline, and presigned URL generation.
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
- Asset storage access goes through AppAssetStorage + AssetPath, using app_asset_storage singleton.
|
||||||
|
- Tree operations require tenant/app scoping and lock for mutation.
|
||||||
|
- Asset zips are packaged via raw storage with storage keys from AppAssetStorage.
|
||||||
|
|
||||||
|
Edge Cases:
|
||||||
|
- File nodes larger than preview limit are rejected.
|
||||||
|
- Deletion runs asynchronously; storage failures are logged.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Unit tests for storage URL generation and publish/build flows.
|
||||||
10
api/agent-notes/services/app_bundle_service.py.md
Normal file
10
api/agent-notes/services/app_bundle_service.py.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Summary:
|
||||||
|
Summary:
|
||||||
|
- Imports app bundles, including asset extraction into app asset storage.
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
- Asset imports respect zip security checks and tenant/app scoping.
|
||||||
|
- Draft asset packaging uses AppAssetStorage for key mapping.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Bundle import unit tests and zip validation coverage.
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
Summary:
|
||||||
|
Summary:
|
||||||
|
- Unit tests for AppAssetStorage ref validation, key mapping, and signing.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Covers valid/invalid refs, signature verify, expiration handling, and proxy URL generation.
|
||||||
22
api/app.py
22
api/app.py
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
@ -10,25 +10,12 @@ if TYPE_CHECKING:
|
|||||||
celery: Celery
|
celery: Celery
|
||||||
|
|
||||||
|
|
||||||
HOST = "0.0.0.0"
|
|
||||||
PORT = 5001
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def is_db_command() -> bool:
|
def is_db_command() -> bool:
|
||||||
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
|
if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db":
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def log_startup_banner(host: str, port: int) -> None:
|
|
||||||
debugger_attached = sys.gettrace() is not None
|
|
||||||
logger.info("Serving Dify API via gevent WebSocket server")
|
|
||||||
logger.info("Bound to http://%s:%s", host, port)
|
|
||||||
logger.info("Debugger attached: %s", "on" if debugger_attached else "off")
|
|
||||||
logger.info("Press CTRL+C to quit")
|
|
||||||
|
|
||||||
|
|
||||||
# create app
|
# create app
|
||||||
flask_app = None
|
flask_app = None
|
||||||
socketio_app = None
|
socketio_app = None
|
||||||
@ -51,12 +38,13 @@ else:
|
|||||||
|
|
||||||
socketio_app, flask_app = create_app()
|
socketio_app, flask_app = create_app()
|
||||||
app = flask_app
|
app = flask_app
|
||||||
celery = cast("Celery", app.extensions["celery"])
|
celery = cast("Celery", flask_app.extensions["celery"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from gevent import pywsgi
|
from gevent import pywsgi
|
||||||
from geventwebsocket.handler import WebSocketHandler # type: ignore[reportMissingTypeStubs]
|
from geventwebsocket.handler import WebSocketHandler # type: ignore[reportMissingTypeStubs]
|
||||||
|
|
||||||
log_startup_banner(HOST, PORT)
|
host = os.environ.get("HOST", "0.0.0.0")
|
||||||
server = pywsgi.WSGIServer((HOST, PORT), socketio_app, handler_class=WebSocketHandler)
|
port = int(os.environ.get("PORT", 5001))
|
||||||
|
server = pywsgi.WSGIServer((host, port), socketio_app, handler_class=WebSocketHandler)
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|||||||
@ -2,46 +2,17 @@ import logging
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import socketio # type: ignore[reportMissingTypeStubs]
|
import socketio # type: ignore[reportMissingTypeStubs]
|
||||||
from flask import request
|
|
||||||
from opentelemetry.trace import get_current_span
|
from opentelemetry.trace import get_current_span
|
||||||
from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID
|
from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from contexts.wrapper import RecyclableContextVar
|
from contexts.wrapper import RecyclableContextVar
|
||||||
from controllers.console.error import UnauthorizedAndForceLogout
|
|
||||||
from core.logging.context import init_request_context
|
from core.logging.context import init_request_context
|
||||||
from dify_app import DifyApp
|
from dify_app import DifyApp
|
||||||
from extensions.ext_socketio import sio
|
from extensions.ext_socketio import sio
|
||||||
from services.enterprise.enterprise_service import EnterpriseService
|
|
||||||
from services.feature_service import LicenseStatus
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Console bootstrap APIs exempt from license check.
|
|
||||||
# Defined at module level to avoid per-request tuple construction.
|
|
||||||
# - system-features: license status for expiry UI (GlobalPublicStoreProvider)
|
|
||||||
# - setup: install/setup status check (AppInitializer)
|
|
||||||
# - init: init password validation for fresh install (InitPasswordPopup)
|
|
||||||
# - login: auto-login after setup completion (InstallForm)
|
|
||||||
# - features: billing/plan features (ProviderContextProvider)
|
|
||||||
# - account/profile: login check + user profile (AppContextProvider, useIsLogin)
|
|
||||||
# - workspaces/current: workspace + model providers (AppContextProvider)
|
|
||||||
# - version: version check (AppContextProvider)
|
|
||||||
# - activate/check: invitation link validation (signin page)
|
|
||||||
# Without these exemptions, the signin page triggers location.reload()
|
|
||||||
# on unauthorized_and_force_logout, causing an infinite loop.
|
|
||||||
_CONSOLE_EXEMPT_PREFIXES = (
|
|
||||||
"/console/api/system-features",
|
|
||||||
"/console/api/setup",
|
|
||||||
"/console/api/init",
|
|
||||||
"/console/api/login",
|
|
||||||
"/console/api/features",
|
|
||||||
"/console/api/account/profile",
|
|
||||||
"/console/api/workspaces/current",
|
|
||||||
"/console/api/version",
|
|
||||||
"/console/api/activate/check",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Application Factory Function
|
# Application Factory Function
|
||||||
@ -62,39 +33,6 @@ def create_flask_app_with_configs() -> DifyApp:
|
|||||||
init_request_context()
|
init_request_context()
|
||||||
RecyclableContextVar.increment_thread_recycles()
|
RecyclableContextVar.increment_thread_recycles()
|
||||||
|
|
||||||
# Enterprise license validation for API endpoints (both console and webapp)
|
|
||||||
# When license expires, block all API access except bootstrap endpoints needed
|
|
||||||
# for the frontend to load the license expiration page without infinite reloads.
|
|
||||||
if dify_config.ENTERPRISE_ENABLED:
|
|
||||||
is_console_api = request.path.startswith("/console/api/")
|
|
||||||
is_webapp_api = request.path.startswith("/api/")
|
|
||||||
|
|
||||||
if is_console_api or is_webapp_api:
|
|
||||||
if is_console_api:
|
|
||||||
is_exempt = any(request.path.startswith(p) for p in _CONSOLE_EXEMPT_PREFIXES)
|
|
||||||
else: # webapp API
|
|
||||||
is_exempt = request.path.startswith("/api/system-features")
|
|
||||||
|
|
||||||
if not is_exempt:
|
|
||||||
try:
|
|
||||||
# Check license status (cached — see EnterpriseService for TTL details)
|
|
||||||
license_status = EnterpriseService.get_cached_license_status()
|
|
||||||
if license_status in (LicenseStatus.INACTIVE, LicenseStatus.EXPIRED, LicenseStatus.LOST):
|
|
||||||
raise UnauthorizedAndForceLogout(
|
|
||||||
f"Enterprise license is {license_status}. Please contact your administrator."
|
|
||||||
)
|
|
||||||
if license_status is None:
|
|
||||||
raise UnauthorizedAndForceLogout(
|
|
||||||
"Unable to verify enterprise license. Please contact your administrator."
|
|
||||||
)
|
|
||||||
except UnauthorizedAndForceLogout:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to check enterprise license status")
|
|
||||||
raise UnauthorizedAndForceLogout(
|
|
||||||
"Unable to verify enterprise license. Please contact your administrator."
|
|
||||||
)
|
|
||||||
|
|
||||||
# add after request hook for injecting trace headers from OpenTelemetry span context
|
# add after request hook for injecting trace headers from OpenTelemetry span context
|
||||||
# Only adds headers when OTEL is enabled and has valid context
|
# Only adds headers when OTEL is enabled and has valid context
|
||||||
@dify_app.after_request
|
@dify_app.after_request
|
||||||
@ -149,7 +87,6 @@ def initialize_extensions(app: DifyApp):
|
|||||||
ext_commands,
|
ext_commands,
|
||||||
ext_compress,
|
ext_compress,
|
||||||
ext_database,
|
ext_database,
|
||||||
ext_enterprise_telemetry,
|
|
||||||
ext_fastopenapi,
|
ext_fastopenapi,
|
||||||
ext_forward_refs,
|
ext_forward_refs,
|
||||||
ext_hosting_provider,
|
ext_hosting_provider,
|
||||||
@ -181,6 +118,7 @@ def initialize_extensions(app: DifyApp):
|
|||||||
ext_import_modules,
|
ext_import_modules,
|
||||||
ext_orjson,
|
ext_orjson,
|
||||||
ext_forward_refs,
|
ext_forward_refs,
|
||||||
|
ext_set_secretkey,
|
||||||
ext_compress,
|
ext_compress,
|
||||||
ext_code_based_extension,
|
ext_code_based_extension,
|
||||||
ext_database,
|
ext_database,
|
||||||
@ -188,7 +126,6 @@ def initialize_extensions(app: DifyApp):
|
|||||||
ext_migrate,
|
ext_migrate,
|
||||||
ext_redis,
|
ext_redis,
|
||||||
ext_storage,
|
ext_storage,
|
||||||
ext_set_secretkey,
|
|
||||||
ext_logstore, # Initialize logstore after storage, before celery
|
ext_logstore, # Initialize logstore after storage, before celery
|
||||||
ext_celery,
|
ext_celery,
|
||||||
ext_login,
|
ext_login,
|
||||||
@ -200,7 +137,6 @@ def initialize_extensions(app: DifyApp):
|
|||||||
ext_commands,
|
ext_commands,
|
||||||
ext_fastopenapi,
|
ext_fastopenapi,
|
||||||
ext_otel,
|
ext_otel,
|
||||||
ext_enterprise_telemetry,
|
|
||||||
ext_request_logging,
|
ext_request_logging,
|
||||||
ext_session_factory,
|
ext_session_factory,
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
api/bin/dify-cli-darwin-amd64
Executable file
BIN
api/bin/dify-cli-darwin-amd64
Executable file
Binary file not shown.
BIN
api/bin/dify-cli-darwin-arm64
Executable file
BIN
api/bin/dify-cli-darwin-arm64
Executable file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user